From b1e933b3daa4f5bb3cb460db8c534dee373f1b94 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 14 Apr 2020 10:27:18 +0200 Subject: [PATCH 01/12] Add feature option_static_remotekey --- eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 5 +++++ .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 8 ++++++++ 2 files changed, 13 insertions(+) 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 42ab1af9ce..f81c4a9258 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -69,6 +69,11 @@ object Features { val mandatory = 10 } + case object StaticRemoteKey extends Feature { + val rfcName = "staticremotekey" + val mandatory = 12 + } + case object PaymentSecret extends Feature { val rfcName = "payment_secret" val mandatory = 14 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 d7afda99d0..d36afacef6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -51,6 +51,14 @@ class FeaturesSpec extends AnyFunSuite { assert(hasFeature(hex"0200", VariableLengthOnion, Some(FeatureSupport.Optional))) } + test("'option_static_remotekey' feature") { + assert(hasFeature(hex"1000", StaticRemoteKey)) + assert(hasFeature(hex"1000", StaticRemoteKey, Some(FeatureSupport.Mandatory))) + assert(hasFeature(hex"2000", StaticRemoteKey, None)) + assert(hasFeature(hex"2000", StaticRemoteKey, Some(FeatureSupport.Optional))) + } + + test("features dependencies") { val testCases = Map( bin" " -> true, From 1877f90cf8304e4ea1ec07c845e0aba532b4165c Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 14 Apr 2020 10:36:37 +0200 Subject: [PATCH 02/12] Add channelVersion for option_static_remotekey --- .../src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala | 3 +++ .../test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala | 3 ++- .../test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index d2a6dddb61..73539bb713 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -265,11 +265,14 @@ object ChannelVersion { val LENGTH_BITS = 4 * 8 val ZEROES = ChannelVersion(bin"00000000000000000000000000000000") val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0 + val USE_STATIC_REMOTEKEY_BIT = 1 def fromBit(bit: Int) = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse) val USE_PUBKEY_KEYPATH = fromBit(USE_PUBKEY_KEYPATH_BIT) + val USE_STATIC_REMOTEKEY = fromBit(USE_STATIC_REMOTEKEY_BIT) val STANDARD = ZEROES | USE_PUBKEY_KEYPATH + val STATIC_REMOTEKEY = STANDARD | USE_STATIC_REMOTEKEY // USE_PUBKEY_KEYPATH + USE_STATIC_REMOTEKEY } // @formatter:on diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala index 9dd30fb18f..c773926362 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -4,7 +4,8 @@ import org.scalatest.funsuite.AnyFunSuite class ChannelTypesSpec extends AnyFunSuite { test("standard channel features include deterministic channel key path") { - assert(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) assert(!ChannelVersion.ZEROES.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + assert(ChannelVersion.STANDARD.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) + assert(ChannelVersion.STATIC_REMOTEKEY.isSet(ChannelVersion.USE_PUBKEY_KEYPATH_BIT)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index bb55477ba8..68b3301156 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -74,13 +74,16 @@ class ChannelCodecsSpec extends AnyFunSuite { val legacy03 = hex"03d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" val current02 = hex"010000000102a06ea3081f0f7a8ce31eb4f0822d10d2da120d5a1b1451f0727f51c7372f0f9b" val current03 = hex"010000000103d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" + val current04 = hex"010000000303d5c030835d6a6248b2d1d4cac60813838011b995a66b6f78dcc9fb8b5c40c3f3" assert(channelVersionCodec.decode(legacy02.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy02.bits))) assert(channelVersionCodec.decode(legacy03.bits) === Attempt.successful(DecodeResult(ChannelVersion.ZEROES, legacy03.bits))) assert(channelVersionCodec.decode(current02.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current02.drop(5).bits))) assert(channelVersionCodec.decode(current03.bits) === Attempt.successful(DecodeResult(ChannelVersion.STANDARD, current03.drop(5).bits))) + assert(channelVersionCodec.decode(current04.bits) === Attempt.successful(DecodeResult(ChannelVersion.STATIC_REMOTEKEY, current04.drop(5).bits))) assert(channelVersionCodec.encode(ChannelVersion.STANDARD) === Attempt.successful(hex"0100000001".bits)) + assert(channelVersionCodec.encode(ChannelVersion.STATIC_REMOTEKEY) === Attempt.successful(hex"0100000003".bits)) } test("encode/decode localparams") { From 8f73c28eacb278e63f76bb4149d28166eee4e62a Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 14 Apr 2020 11:04:55 +0200 Subject: [PATCH 03/12] Add localPaymentBasepoint to LocalParams --- .../acinq/eclair/channel/ChannelTypes.scala | 4 +-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 11 +++--- .../fr/acinq/eclair/wire/ChannelCodecs.scala | 34 ++++++++++--------- .../scala/fr/acinq/eclair/TestConstants.scala | 17 ++++++---- .../eclair/channel/CommitmentsSpec.scala | 2 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 29 +++++++++++++--- 6 files changed, 62 insertions(+), 35 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 73539bb713..1535a77ec5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -214,8 +214,7 @@ final case class DATA_CLOSING(commitments: Commitments, nextRemoteCommitPublished: Option[RemoteCommitPublished] = None, futureRemoteCommitPublished: Option[RemoteCommitPublished] = None, revokedCommitPublished: List[RevokedCommitPublished] = Nil) extends Data with HasCommitments { - val spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx) - require(spendingTxes.nonEmpty, "there must be at least one tx published in this state") + def spendingTxes = mutualClosePublished ::: localCommitPublished.map(_.commitTx).toList ::: remoteCommitPublished.map(_.commitTx).toList ::: nextRemoteCommitPublished.map(_.commitTx).toList ::: futureRemoteCommitPublished.map(_.commitTx).toList ::: revokedCommitPublished.map(_.commitTx) } final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments @@ -230,6 +229,7 @@ final case class LocalParams(nodeId: PublicKey, maxAcceptedHtlcs: Int, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, + localPaymentBasepoint: Option[PublicKey], features: ByteVector) final case class RemoteParams(nodeId: PublicKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index fdd277b1c6..1c30140799 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -269,7 +269,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, switchboard: Act def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) - val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, funder, fundingAmount) + val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, None, funder, fundingAmount) val channel = spawnChannel(nodeParams, origin_opt) (channel, localParams) } @@ -374,13 +374,13 @@ object Peer { // @formatter:on - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, localPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi): LocalParams = { // we make sure that funder and fundee key path end differently val fundingKeyPath = nodeParams.keyManager.newFundingKeyPath(isFunder) - makeChannelParams(nodeParams, defaultFinalScriptPubKey, isFunder, fundingAmount, fundingKeyPath) + makeChannelParams(nodeParams, defaultFinalScriptPubkey, localPaymentBasepoint, isFunder, fundingAmount, fundingKeyPath) } - def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubKey: ByteVector, isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { + def makeChannelParams(nodeParams: NodeParams, defaultFinalScriptPubkey: ByteVector, localPaymentBasepoint: Option[PublicKey], isFunder: Boolean, fundingAmount: Satoshi, fundingKeyPath: DeterministicWallet.KeyPath): LocalParams = { LocalParams( nodeParams.nodeId, fundingKeyPath, @@ -390,8 +390,9 @@ object Peer { htlcMinimum = nodeParams.htlcMinimum, toSelfDelay = nodeParams.toRemoteDelayBlocks, // we choose their delay maxAcceptedHtlcs = nodeParams.maxAcceptedHtlcs, - defaultFinalScriptPubKey = defaultFinalScriptPubKey, isFunder = isFunder, + defaultFinalScriptPubKey = defaultFinalScriptPubkey, + localPaymentBasepoint = localPaymentBasepoint, features = nodeParams.features) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala index bf691eb787..4b1e2cf135 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/ChannelCodecs.scala @@ -60,7 +60,7 @@ object ChannelCodecs extends Logging { // field and don't support additional features which is why all bits are set to 0. ) - val localParamsCodec: Codec[LocalParams] = ( + def localParamsCodec(channelVersion: ChannelVersion): Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: @@ -71,6 +71,7 @@ object ChannelCodecs extends Logging { ("maxAcceptedHtlcs" | uint16) :: ("isFunder" | bool) :: ("defaultFinalScriptPubKey" | varsizebinarydata) :: + ("localPaymentBasepoint" | optional(provide(channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] val remoteParamsCodec: Codec[RemoteParams] = ( @@ -213,21 +214,22 @@ object ChannelCodecs extends Logging { ) val commitmentsCodec: Codec[Commitments] = ( - ("channelVersion" | channelVersionCodec) :: - ("localParams" | localParamsCodec) :: - ("remoteParams" | remoteParamsCodec) :: - ("channelFlags" | byte) :: - ("localCommit" | localCommitCodec) :: - ("remoteCommit" | remoteCommitCodec) :: - ("localChanges" | localChangesCodec) :: - ("remoteChanges" | remoteChangesCodec) :: - ("localNextHtlcId" | uint64overflow) :: - ("remoteNextHtlcId" | uint64overflow) :: - ("originChannels" | originsMapCodec) :: - ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) :: - ("commitInput" | inputInfoCodec) :: - ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: - ("channelId" | bytes32)).as[Commitments] + ("channelVersion" | channelVersionCodec) >>:~ { channelVersion => + ("localParams" | localParamsCodec(channelVersion)) :: + ("remoteParams" | remoteParamsCodec) :: + ("channelFlags" | byte) :: + ("localCommit" | localCommitCodec) :: + ("remoteCommit" | remoteCommitCodec) :: + ("localChanges" | localChangesCodec) :: + ("remoteChanges" | remoteChangesCodec) :: + ("localNextHtlcId" | uint64overflow) :: + ("remoteNextHtlcId" | uint64overflow) :: + ("originChannels" | originsMapCodec) :: + ("remoteNextCommitInfo" | either(bool, waitingForRevocationCodec, publicKey)) :: + ("commitInput" | inputInfoCodec) :: + ("remotePerCommitmentSecrets" | ShaChain.shaChainCodec) :: + ("channelId" | bytes32) + }).as[Commitments] val closingTxProposedCodec: Codec[ClosingTxProposed] = ( ("unsignedTx" | txCodec) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 9d2814ccd4..9e1300d8aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -134,10 +134,12 @@ object TestConstants { ) def channelParams = Peer.makeChannelParams( - nodeParams = nodeParams, - defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), - isFunder = true, - fundingSatoshis).copy( + nodeParams, + Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), + None, + true, + fundingSatoshis + ).copy( channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment ) } @@ -216,9 +218,10 @@ object TestConstants { ) def channelParams = Peer.makeChannelParams( - nodeParams = nodeParams, - defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), - isFunder = false, + nodeParams, + Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), + None, + false, fundingSatoshis).copy( channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment ) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index ba1381ee32..6d789ecb42 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -489,7 +489,7 @@ class CommitmentsSpec extends TestkitBaseClass with StateTestsHelperMethods { object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: Long = 0, dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = { - val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, ByteVector.empty) + val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, ByteVector.empty) val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey) Commitments( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 68b3301156..c38ae48497 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -23,7 +23,7 @@ import akka.actor.ActorSystem import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, Script, Transaction} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain} @@ -97,16 +97,36 @@ class ChannelCodecsSpec extends AnyFunSuite { toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), + localPaymentBasepoint = None, isFunder = Random.nextBoolean(), features = randomBytes(256)) - val encoded = localParamsCodec.encode(o).require - val decoded = localParamsCodec.decode(encoded).require + val encoded = localParamsCodec(ChannelVersion.ZEROES).encode(o).require + val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require + assert(o.localPaymentBasepoint.isEmpty) assert(o === decoded.value) // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" - val withGlobalFeaturesDecoded = localParamsCodec.decode(withGlobalFeatures.bits).require.value + val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value assert(withGlobalFeaturesDecoded.features === hex"0a8a") + + val o1 = LocalParams( + nodeId = randomKey.publicKey, + fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), + dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), + maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), + channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), + toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), + maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), + defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), + localPaymentBasepoint = Some(PrivateKey(randomBytes32).publicKey), + isFunder = Random.nextBoolean(), + features = randomBytes(256)) + val encoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).encode(o1).require + val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require + assert(o1.localPaymentBasepoint.isDefined) + assert(o1 === decoded1.value) } test("encode/decode remoteparams") { @@ -398,6 +418,7 @@ object ChannelCodecsSpec { toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, defaultFinalScriptPubKey = ByteVector.empty, + localPaymentBasepoint = None, isFunder = true, features = hex"deadbeef") From 3ab42237034acd4aebfc8687b4484de3a6c610fd Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 14 Apr 2020 11:52:41 +0200 Subject: [PATCH 04/12] Add getReceivePubkey to EclairWallet --- .../main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../main/scala/fr/acinq/eclair/Features.scala | 5 ++ .../main/scala/fr/acinq/eclair/Setup.scala | 2 +- .../eclair/blockchain/EclairWallet.scala | 5 +- .../bitcoind/BitcoinCoreWallet.scala | 8 +++- .../electrum/ElectrumEclairWallet.scala | 6 ++- .../fr/acinq/eclair/channel/Helpers.scala | 48 ++++++++++++++----- .../main/scala/fr/acinq/eclair/io/Peer.scala | 32 +++++++++---- .../acinq/eclair/blockchain/TestWallet.scala | 8 +++- .../bitcoind/BitcoinCoreWalletSpec.scala | 28 +++++++++-- 10 files changed, 112 insertions(+), 32 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index b73d10e430..e74b211f35 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -204,7 +204,7 @@ class EclairImpl(appKit: Kit) extends Eclair { override def newAddress(): Future[String] = { appKit.wallet match { - case w: BitcoinCoreWallet => w.getFinalAddress + case w: BitcoinCoreWallet => w.getReceiveAddress case _ => Future.failed(new IllegalArgumentException("this call is only available with a bitcoin core backend")) } } 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 f81c4a9258..d3b2a69fc3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -131,6 +131,11 @@ object Features { def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support) + /** returns true if both have at least optional support */ + def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = { + hasFeature(localFeatures, feature, support) && hasFeature(remoteFeatures, feature, support) + } + /** * Check that the features that we understand are correctly specified, and that there are no mandatory features that * we don't understand (even bits). diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 37e5f47b48..de53663853 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -270,7 +270,7 @@ class Setup(datadir: File, implicit val timeout = Timeout(30 seconds) new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash) } - _ = wallet.getFinalAddress.map { + _ = wallet.getReceiveAddress.map { case address => logger.info(s"initial wallet address=$address") } // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala index a2e2973284..ce484be138 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/EclairWallet.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.blockchain +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Satoshi, Transaction} import scodec.bits.ByteVector @@ -28,7 +29,9 @@ trait EclairWallet { def getBalance: Future[Satoshi] - def getFinalAddress: Future[String] + def getReceiveAddress: Future[String] + + def getReceivePubkey(receiveAddress: Option[String] = None): Future[PublicKey] def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala index cdbdbfe4cf..80fa75e96b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWallet.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.blockchain.bitcoind +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -67,10 +68,15 @@ class BitcoinCoreWallet(rpcClient: BitcoinJsonRPCClient)(implicit ec: ExecutionC override def getBalance: Future[Satoshi] = rpcClient.invoke("getbalance") collect { case JDecimal(balance) => Satoshi(balance.bigDecimal.scaleByPowerOfTen(8).longValue) } - override def getFinalAddress: Future[String] = for { + override def getReceiveAddress: Future[String] = for { JString(address) <- rpcClient.invoke("getnewaddress") } yield address + override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = for { + address <- receiveAddress.map(Future.successful).getOrElse(getReceiveAddress) + JString(rawKey) <- rpcClient.invoke("getaddressinfo", address).map(_ \ "pubkey") + } yield PublicKey(ByteVector.fromValidHex(rawKey)) + private def signTransactionOrUnlock(tx: Transaction): Future[SignTransactionResponse] = { val f = signTransaction(tx) // if signature fails (e.g. because wallet is encrypted) we need to unlock the utxos diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala index ed8dea4e01..c7bc71f7b4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain.electrum import akka.actor.{ActorRef, ActorSystem} import akka.pattern.ask -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, Transaction, TxOut} +import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.addressToPublicKeyScript import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ @@ -32,7 +32,9 @@ class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implic override def getBalance = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => balance.confirmed + balance.unconfirmed) - override def getFinalAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address) + override def getReceiveAddress = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address) + + override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.failed(new RuntimeException("Not implemented")) def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse] 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 86229cabf8..9769bcf608 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 @@ -32,6 +32,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _} +import fr.acinq.eclair.channel.ChannelVersion.USE_STATIC_REMOTEKEY_BIT import scodec.bits.ByteVector import scala.compat.Platform @@ -244,11 +245,15 @@ object Helpers { def getFinalScriptPubKey(wallet: EclairWallet, chainHash: ByteVector32): ByteVector = { import scala.concurrent.duration._ - val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds) + val finalAddress = Await.result(wallet.getReceiveAddress, 40 seconds) Script.write(addressToPublicKeyScript(finalAddress, chainHash)) } + def getWalletPaymentBasepoint(wallet: EclairWallet): PublicKey = { + Await.result(wallet.getReceivePubkey(), 40 seconds) + } + object Funding { def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { @@ -581,7 +586,8 @@ object Helpers { } /** - * Claim all the HTLCs that we've received from their current commit tx + * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey + * we don't claim our main output. * * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) @@ -630,14 +636,25 @@ object Helpers { } }.toSeq.flatten - claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( - claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, - claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } - ) + channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => + RemoteCommitPublished( + commitTx = tx, + claimMainOutputTx = None, + claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, + claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx }, + irrevocablySpent = Map.empty + ) + case _ => + claimRemoteCommitMainOutput(keyManager, commitments, remoteCommit.remotePerCommitmentPoint, tx, feeEstimator, feeTargets).copy( + claimHtlcSuccessTxs = txes.toList.collect { case c: ClaimHtlcSuccessTx => c.tx }, + claimHtlcTimeoutTxs = txes.toList.collect { case c: ClaimHtlcTimeoutTx => c.tx } + ) + } } /** - * Claim our Main output only + * Claim our Main output only, not used if option_static_remotekey was negotiated * * @param commitments either our current commitment data in case of usual remote uncooperative closing * or our outdated commitment data in case of data loss protection procedure; in any case it is used only @@ -649,7 +666,6 @@ object Helpers { def claimRemoteCommitMainOutput(keyManager: KeyManager, commitments: Commitments, remotePerCommitmentPoint: PublicKey, tx: Transaction, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): RemoteCommitPublished = { val channelKeyPath = keyManager.channelKeyPath(commitments.localParams, commitments.channelVersion) val localPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) - val feeratePerKwMain = feeEstimator.getFeeratePerKw(feeTargets.claimMainBlockTarget) val mainTx = generateTx("claim-p2wpkh-output") { @@ -682,8 +698,12 @@ object Helpers { require(tx.txIn.size == 1, "commitment tx should have 1 input") val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val obscuredTxNumber = Transactions.decodeTxNumber(tx.txIn.head.sequence, tx.lockTime) + val localPaymentPoint = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => keyManager.paymentPoint(channelKeyPath).publicKey + } // this tx has been published by remote, so we need to invert local/remote params - val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey) + val txnumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !localParams.isFunder, remoteParams.paymentBasepoint, localPaymentPoint) require(txnumber <= 0xffffffffffffL, "txnumber must be lesser than 48 bits long") log.warning(s"a revoked commit has been published with txnumber=$txnumber") // now we know what commit number this tx is referring to, we can derive the commitment point from the shachain @@ -702,11 +722,15 @@ object Helpers { val feeratePerKwPenalty = feeEstimator.getFeeratePerKw(target = 2) // first we will claim our main output right away - val mainTx = generateTx("claim-p2wpkh-output") { - Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { + val mainTx = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => + log.info(s"channel uses option_static_remotekey, not claiming our p2wpkh output") + None + case _ => generateTx("claim-p2wpkh-output") { + Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) Transactions.addSigs(claimMain, localPaymentPubkey, sig) - }) + })} } // then we punish them by stealing their main output diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 1c30140799..fb35f74c97 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -24,11 +24,13 @@ import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, Satoshi} -import fr.acinq.eclair.Features.Wumbo +import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, Satoshi, Script} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.{StaticRemoteKey, Wumbo, canUseFeature} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ +import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} @@ -138,20 +140,28 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, switchboard: Act sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay } else { - val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis, origin_opt = Some(sender)) + val channelVersion = canUseFeature(d.localInit.features, d.remoteInit.features, StaticRemoteKey) match { + case false => ChannelVersion.STANDARD + case true => ChannelVersion.STATIC_REMOTEKEY + } + val (channel, localParams) = createNewChannel(nodeParams, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) val temporaryChannelId = randomBytes32 val channelFeeratePerKw = nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), ChannelVersion.STANDARD) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelVersion) stay using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } case Event(msg: wire.OpenChannel, d: ConnectedData) => d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { case None => - val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) + val channelVersion = canUseFeature(d.localInit.features, d.remoteInit.features, StaticRemoteKey) match { + case false => ChannelVersion.STANDARD + case true => ChannelVersion.STATIC_REMOTEKEY + } + val (channel, localParams) = createNewChannel(nodeParams, funder = false, fundingAmount = msg.fundingSatoshis, origin_opt = None, channelVersion) val temporaryChannelId = msg.temporaryChannelId log.info(s"accepting a new channel with temporaryChannelId=$temporaryChannelId localParams=$localParams") channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit) @@ -267,9 +277,15 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, switchboard: Act s(e) } - def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef]): (ActorRef, LocalParams) = { - val defaultFinalScriptPubKey = Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash) - val localParams = makeChannelParams(nodeParams, defaultFinalScriptPubKey, None, funder, fundingAmount) + def createNewChannel(nodeParams: NodeParams, funder: Boolean, fundingAmount: Satoshi, origin_opt: Option[ActorRef], channelVersion: ChannelVersion): (ActorRef, LocalParams) = { + val (finalScript, localPaymentBasepoint) = channelVersion match { + case v if v.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT) => + val walletKey = Helpers.getWalletPaymentBasepoint(wallet) + (Script.write(Script.pay2wpkh(walletKey)), Some(walletKey)) + case _ => + (Helpers.getFinalScriptPubKey(wallet, nodeParams.chainHash), None) + } + val localParams = makeChannelParams(nodeParams, finalScript, localPaymentBasepoint, funder, fundingAmount) val channel = spawnChannel(nodeParams, origin_opt) (channel, localParams) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala index 9e0baf76f9..349b04db89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/TestWallet.scala @@ -16,9 +16,11 @@ package fr.acinq.eclair.blockchain -import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{Base58, ByteVector32, Crypto, OutPoint, Satoshi, Transaction, TxIn, TxOut} import fr.acinq.eclair.LongToBtcAmount import scodec.bits.ByteVector +import scodec.bits._ import scala.concurrent.Future @@ -31,7 +33,9 @@ class TestWallet extends EclairWallet { override def getBalance: Future[Satoshi] = ??? - override def getFinalAddress: Future[String] = Future.successful("2MsRZ1asG6k94m6GYUufDGaZJMoJ4EV5JKs") + override def getReceiveAddress: Future[String] = Future.successful("bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux") + + override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.successful(PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12")) override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: Long): Future[MakeFundingTxResponse] = Future.successful(TestWallet.makeDummyFundingTx(pubkeyScript, amount, feeRatePerKw)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala index 35dc445f8d..dd5112fb2a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreWalletSpec.scala @@ -21,7 +21,8 @@ import akka.actor.Status.Failure import akka.pattern.pipe import akka.testkit.{TestKit, TestProbe} import com.typesafe.config.ConfigFactory -import fr.acinq.bitcoin.{Block, ByteVector32, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, MilliBtc, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} @@ -113,7 +114,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe val sender = TestProbe() // create a transaction that spends UTXOs that don't exist - wallet.getFinalAddress.pipeTo(sender.ref) + wallet.getReceiveAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] val unknownTxids = Seq( ByteVector32.fromValidHex("01" * 32), @@ -148,7 +149,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe wallet.getBalance.pipeTo(sender.ref) assert(sender.expectMsgType[Satoshi] > 0.sat) - wallet.getFinalAddress.pipeTo(sender.ref) + wallet.getReceiveAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] assert(Try(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)).isSuccess) @@ -208,7 +209,7 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe wallet.getBalance.pipeTo(sender.ref) assert(sender.expectMsgType[Satoshi] > 0.sat) - wallet.getFinalAddress.pipeTo(sender.ref) + wallet.getReceiveAddress.pipeTo(sender.ref) val address = sender.expectMsgType[String] assert(Try(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)).isSuccess) @@ -282,4 +283,23 @@ class BitcoinCoreWalletSpec extends TestKit(ActorSystem("test")) with BitcoindSe sender.expectMsg(true) } + test("getReceivePubkey should return the raw pubkey for the receive address") { + val bitcoinClient = new BasicBitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport")) + val wallet = new BitcoinCoreWallet(bitcoinClient) + + val sender = TestProbe() + + wallet.getReceiveAddress.pipeTo(sender.ref) + val address = sender.expectMsgType[String] + + wallet.getReceivePubkey(receiveAddress = Some(address)).pipeTo(sender.ref) + val receiveKey = sender.expectMsgType[PublicKey] + + assert(addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash) === Script.pay2wpkh(receiveKey)) + } + } From 1bf76182a91d5e12d9e20c2c1c65c83eda9a67a6 Mon Sep 17 00:00:00 2001 From: Andrea Date: Tue, 14 Apr 2020 16:50:44 +0200 Subject: [PATCH 05/12] Use a static pubkey as local_payment_basepoint if option_static_remotekey was negotiated --- .../fr/acinq/eclair/channel/Channel.scala | 34 ++++++-- .../fr/acinq/eclair/channel/Commitments.scala | 25 +++++- .../states/StateTestsHelperMethods.scala | 16 +++- .../channel/states/e/NormalStateSpec.scala | 54 +++++++++++- .../channel/states/h/ClosingStateSpec.scala | 82 ++++++++++++++++++- .../eclair/integration/IntegrationSpec.scala | 77 +++++++++++++++-- 6 files changed, 259 insertions(+), 29 deletions(-) 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 01702ba26a..d7c7439378 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 @@ -21,6 +21,7 @@ import akka.event.Logging.MDC import akka.pattern.pipe import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, OutPoint, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ @@ -34,6 +35,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import scodec.bits.ByteVector +import ChannelVersion._ import scala.collection.immutable.Queue import scala.compat.Platform @@ -183,7 +185,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubKey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + paymentBasepoint = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => keyManager.paymentPoint(channelKeyPath).publicKey + }, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), @@ -297,7 +302,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Success(_) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelVersion = ChannelVersion.STANDARD + val channelVersion = Features.canUseFeature(localParams.features, remoteInit.features, StaticRemoteKey) match { + case false => ChannelVersion.STANDARD + case true => ChannelVersion.STATIC_REMOTEKEY + } val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) // TODO: maybe also check uniqueness of temporary channel id val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis) @@ -311,7 +319,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubkey, revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = keyManager.paymentPoint(channelKeyPath).publicKey, + paymentBasepoint = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => keyManager.paymentPoint(channelKeyPath).publicKey + }, delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), @@ -2089,12 +2100,17 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = { log.warning(s"they published their future commit (because we asked them to) in txid=${commitTx.txid}") - // if we are in this state, then this field is defined - val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint - val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - - goto(CLOSING) using nextData storing() calling (doPublish(remoteCommitPublished)) + d.commitments.channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => + val remoteCommitPublished = RemoteCommitPublished(commitTx, None, List.empty, List.empty, Map.empty) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + goto(CLOSING) using nextData storing() + case _ => + val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint + val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) + goto(CLOSING) using nextData storing() calling(doPublish(remoteCommitPublished)) + } } def handleRemoteSpentNext(commitTx: Transaction, d: HasCommitments) = { 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 d9a6b9f691..20a5bb0267 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 @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter +import fr.acinq.eclair.channel.ChannelVersion._ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} @@ -62,6 +63,8 @@ case class Commitments(channelVersion: ChannelVersion, commitInput: InputInfo, remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) { + require(!channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) || (channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) && localParams.localPaymentBasepoint.isDefined), s"localParams.localPaymentBasepoint must be defined for commitments with version=$channelVersion") + def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] = { @@ -598,11 +601,18 @@ object Commitments { val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) val localDelayedPaymentPubkey = Generators.derivePubKey(keyManager.delayedPaymentPoint(channelKeyPath).publicKey, localPerCommitmentPoint) val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, localPerCommitmentPoint) - val remotePaymentPubkey = Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) + val remotePaymentPubkey = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => remoteParams.paymentBasepoint + case _ => Generators.derivePubKey(remoteParams.paymentBasepoint, localPerCommitmentPoint) + } val remoteHtlcPubkey = Generators.derivePubKey(remoteParams.htlcBasepoint, localPerCommitmentPoint) val localRevocationPubkey = Generators.revocationPubKey(remoteParams.revocationBasepoint, localPerCommitmentPoint) + val localPaymentBasepoint = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => keyManager.paymentPoint(channelKeyPath).publicKey + } val outputs = makeCommitTxOutputs(localParams.isFunder, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, remotePaymentPubkey, localHtlcPubkey, remoteHtlcPubkey, spec) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, keyManager.paymentPoint(channelKeyPath).publicKey, remoteParams.paymentBasepoint, localParams.isFunder, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isFunder, outputs) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feeratePerKw, outputs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } @@ -614,13 +624,20 @@ object Commitments { remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec): (CommitTx, Seq[HtlcTimeoutTx], Seq[HtlcSuccessTx]) = { val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion) - val localPaymentPubkey = Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + val localPaymentBasepoint = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => keyManager.paymentPoint(channelKeyPath).publicKey + } + val localPaymentPubkey = channelVersion match { + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get + case _ => Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + } val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) 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, spec) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, keyManager.paymentPoint(channelKeyPath).publicKey, !localParams.isFunder, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentBasepoint, !localParams.isFunder, outputs) val (htlcTimeoutTxs, htlcSuccessTxs) = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feeratePerKw, outputs) (commitTx, htlcTimeoutTxs, htlcSuccessTxs) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 0d9eac5f77..71f97588e4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ import fr.acinq.eclair.{NodeParams, TestConstants, randomBytes32, _} import org.scalatest.{FixtureTestSuite, ParallelTestExecution} +import scodec.bits._ import scala.concurrent.duration._ @@ -49,7 +50,8 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par router: TestProbe, relayerA: TestProbe, relayerB: TestProbe, - channelUpdateListener: TestProbe) { + channelUpdateListener: TestProbe, + wallet: EclairWallet) { def currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } @@ -70,16 +72,22 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par val router = TestProbe() val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref), alicePeer.ref) val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayerB.ref), bobPeer.ref) - SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener) + SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet) } def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = { import setup._ - val channelVersion = ChannelVersion.STANDARD + val channelVersion = if(tags.contains("static_remotekey")) ChannelVersion.STATIC_REMOTEKEY else ChannelVersion.STANDARD val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat - val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams) + val (aliceParams, bobParams) = if(tags.contains("static_remotekey")) { + (Alice.channelParams.copy(features = hex"2000", localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))), + Bob.channelParams.copy(features = hex"2000", localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet)))) + } else { + (Alice.channelParams, Bob.channelParams) + } + val aliceInit = Init(aliceParams.features) val bobInit = Init(bobParams.features) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelVersion) 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 b41a0e952f..9b85df4fe7 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 @@ -22,7 +22,7 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{Bech32, ByteVector32, ByteVector64, Crypto, Script, ScriptFlags, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ @@ -43,6 +43,7 @@ import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32, _} import org.scalatest.{Outcome, Tag} import scodec.bits._ +import scala.concurrent.Future import scala.concurrent.duration._ /** @@ -56,7 +57,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging override def withFixture(test: OneArgTest): Outcome = { - val setup = init() + val testWallet = if(test.tags.contains("static_remotekey")){ + val randomKey = PrivateKey(randomBytes32).publicKey + new TestWallet{ + override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.successful(randomKey) + override def getReceiveAddress: Future[String] = Future.successful({ + val scriptPubKey = Script.write(Script.pay2wpkh(randomKey)) + Bech32.encodeWitnessAddress("bcrt", 0, scriptPubKey) + }) + } + } else { + new TestWallet + } + val setup = init(wallet = testWallet) import setup._ within(30 seconds) { reachNormal(setup, test.tags) @@ -1102,6 +1115,43 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(forward.htlc === htlc) } + test("recv RevokeAndAck (one htlc sent, static_remotekey)", Tag("static_remotekey")) { f => + import f._ + val sender = TestProbe() + + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") + + def aliceToRemoteScript() = { + val toRemoteAmount = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toRemote + val Some(toRemoteOut) = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx.txOut.find(_.amount == toRemoteAmount.truncateToSatoshi) + toRemoteOut.publicKeyScript + } + + val initialToRemoteScript = aliceToRemoteScript() + + addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) + + sender.send(alice, CMD_SIGN) + sender.expectMsg(ChannelCommandResponse.Ok) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[RevokeAndAck] + bob2alice.forward(alice) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + alice2bob.expectMsgType[RevokeAndAck] + alice2bob.forward(bob) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) + + awaitCond(alice.stateName == NORMAL) + // using option_static_remotekey alice's view of bob toRemote script stays the same across commitments + assert(initialToRemoteScript == aliceToRemoteScript()) + } + test("recv RevocationTimeout") { f => import f._ val sender = TestProbe() 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 0028315a89..f862e9e59c 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 @@ -87,7 +87,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { } } else { within(30 seconds) { - reachNormal(setup) + reachNormal(setup, test.tags) val bobCommitTxes: List[PublishableTxs] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -722,6 +722,22 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(100 millis) } + test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 2) // two main outputs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + + // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) + assert(alice.stateName == CLOSING) + // once the remote commit is confirmed the channel is definitively closed + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + awaitCond(alice.stateName == CLOSED) + } + test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => import f._ val sender = TestProbe() @@ -775,6 +791,49 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } + + test("recv BITCOIN_TX_CONFIRMED (future remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => + import f._ + val sender = TestProbe() + val oldStateData = alice.stateData + val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) + crossSign(alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) + crossSign(bob, alice, bob2alice, alice2bob) + // we simulate a disconnection + sender.send(alice, INPUT_DISCONNECTED) + sender.send(bob, INPUT_DISCONNECTED) + awaitCond(alice.stateName == OFFLINE) + awaitCond(bob.stateName == OFFLINE) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) + // then we manually replace alice's state with an older one + alice.setState(OFFLINE, oldStateData) + // then we reconnect them + val aliceInit = Init(Alice.nodeParams.features) + val bobInit = Init(Bob.nodeParams.features) + sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) + sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) + // peers exchange channel_reestablish messages + alice2bob.expectMsgType[ChannelReestablish] + bob2alice.expectMsgType[ChannelReestablish] + // alice then realizes it has an old state... + bob2alice.forward(alice) + // ... and ask bob to publish its current commitment + val error = alice2bob.expectMsgType[Error] + assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) + // alice now waits for bob to publish its commitment + 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.publishableTxs.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + // using option_static_remotekey alice doesn't need to swipe her output + awaitCond(alice.stateName == CLOSING, 10 seconds) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + // after the commit tx is confirmed the channel is closed, no claim transactions needed + awaitCond(alice.stateName == CLOSED, 10 seconds) + } + test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) @@ -797,6 +856,27 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } + + test("recv BITCOIN_FUNDING_SPENT (one revoked tx, option_static_remotekey)", Tag("static_remotekey")) { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] + // bob publishes one of his revoked txes + val bobRevokedTx = bobCommitTxes.head.commitTx.tx + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) + + // alice publishes and watches the penalty tx, but she won't claim her main output (claim-main) + alice2blockchain.expectMsgType[PublishAsap] // main-penalty + alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty + alice2blockchain.expectMsgType[WatchConfirmed] // revoked commit + alice2blockchain.expectMsgType[WatchSpent] // main-penalty + alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty + alice2blockchain.expectNoMsg(1 second) + + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) + } + test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { f => import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) 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 74dd3fcf53..d739f7c39a 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 @@ -152,7 +152,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService test("starting 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.features" -> "028a8a", "eclair.channel-flags" -> 0).asJava).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.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).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.features" -> "0a8a8a", "eclair.max-funding-satoshis" -> 500000000, "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15).asJava).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.features" -> "0aaa8a", "eclair.max-funding-satoshis" -> 500000000, "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15).asJava).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.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).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(commonConfig)) instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.features" -> "0a8a8a", "eclair.max-funding-satoshis" -> 500000000).asJava).withFallback(commonConfig)) @@ -160,7 +160,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.expiry-delta-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088).asJava).withFallback(commonConfig)) instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.expiry-delta-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089).asJava).withFallback(commonConfig)) - instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("F6", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F6", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.features" -> "2a8a").asJava).withFallback(commonConfig)) // supports optional option_static_remotekey + instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 141, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) // by default C has a normal payment handler, but this can be overriden in tests val paymentHandlerC = nodes("C").system.actorOf(PaymentHandler.props(nodes("C").nodeParams, nodes("C").commandBuffer)) @@ -190,7 +191,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // / \ // A---B ------- C ==== D // \ / \ - // '--E--' F{1,2,3,4,5} + // '--E--' F{1,2,3,4,5,6} val sender = TestProbe() val eventListener = TestProbe() @@ -207,10 +208,11 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService connect(nodes("C"), nodes("F3"), 5000000 sat, 0 msat) connect(nodes("C"), nodes("F4"), 5000000 sat, 0 msat) connect(nodes("C"), nodes("F5"), 5000000 sat, 0 msat) + connect(nodes("C"), nodes("F6"), 5000000 sat, 0 msat) connect(nodes("B"), nodes("G"), 16000000 sat, 0 msat) connect(nodes("G"), nodes("C"), 16000000 sat, 0 msat) - val numberOfChannels = 13 + val numberOfChannels = 14 val channelEndpointsCount = 2 * numberOfChannels // we make sure all channels have set up their WatchConfirmed for the funding tx @@ -259,8 +261,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // A requires private channels, as a consequence: // - only A and B know about channel A-B (and there is no channel_announcement) // - A is not announced (no node_announcement) - awaitAnnouncements(nodes.filterKeys(key => List("A", "B").contains(key)).toMap, 10, 12, 26) - awaitAnnouncements(nodes.filterKeys(key => !List("A", "B").contains(key)).toMap, 10, 12, 24) + awaitAnnouncements(nodes.filterKeys(key => List("A", "B").contains(key)).toMap, 11, 13, 28) + awaitAnnouncements(nodes.filterKeys(key => !List("A", "B").contains(key)).toMap, 11, 13, 26) } test("open a wumbo channel and wait for longer than the default min_depth") { @@ -340,7 +342,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService fundeeState == NORMAL && funderState == NORMAL }) - awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 10, 13, 28) + awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 11, 14, 30) } test("send an HTLC A->D") { @@ -824,6 +826,62 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(outgoingPayments.forall(p => p.status.isInstanceOf[OutgoingPaymentStatus.Failed]), outgoingPayments) } + test("send payments and close the channel C -> F6 with option_static_remotekey") { + // initially all the balance is on C side and F6 doesn't have an output + val sender = TestProbe() + sender.send(nodes("F6").register, 'channelsTo) + // retrieve the channelId of C <--> F6 + val Some(channelId) = sender.expectMsgType[Map[ByteVector32, PublicKey]].find(_._2 == nodes("C").nodeParams.nodeId).map(_._1) + + sender.send(nodes("F6").register, Forward(channelId, CMD_GETSTATEDATA)) + val initialStateDataF6 = sender.expectMsgType[DATA_NORMAL] + val initialCommitmentIndex = initialStateDataF6.commitments.localCommit.index + + // the 'to remote' address is a simple P2WPKH spending to the remote payment basepoint + val toRemoteAddress = Script.pay2wpkh(initialStateDataF6.commitments.remoteParams.paymentBasepoint) + + // toRemote output of C as seen by F6 + val Some(toRemoteOutC) = initialStateDataF6.commitments.localCommit.publishableTxs.commitTx.tx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) + + // let's make a payment to advance the commit index + val amountMsat = 4200000.msat + sender.send(nodes("F6").paymentHandler, ReceivePayment(Some(amountMsat), "1 coffee")) + val pr = sender.expectMsgType[PaymentRequest] + + // then we make the actual payment + sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F6").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) + val paymentId = sender.expectMsgType[UUID](5 seconds) + val ps = sender.expectMsgType[PaymentSent](5 seconds) + assert(ps.id == paymentId) + + sender.send(nodes("F6").register, Forward(channelId, CMD_GETSTATEDATA)) + val stateDataF6 = sender.expectMsgType[DATA_NORMAL] + val commitmentIndex = stateDataF6.commitments.localCommit.index + val commitTx = stateDataF6.commitments.localCommit.publishableTxs.commitTx.tx + val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == Script.write(toRemoteAddress)) + + // there is a new commitment index in the channel state + assert(commitmentIndex == initialCommitmentIndex + 1) + + // script pubkeys of toRemote output remained the same across commitments + assert(toRemoteOutC.publicKeyScript == toRemoteOutCNew.publicKeyScript) + + // now let's force close the channel and check the toRemote is what we had at the beginning + sender.send(nodes("F6").register, Forward(channelId, CMD_FORCECLOSE)) + sender.expectMsg(ChannelCommandResponse.Ok) + // we then wait for C to detect the unilateral close and go to CLOSING state + awaitCond({ + sender.send(nodes("C").register, Forward(channelId, CMD_GETSTATE)) + sender.expectMsgType[State] == CLOSING + }, max = 20 seconds, interval = 1 second) + + sender.send(bitcoincli, BitcoinReq("getrawtransaction", commitTx.txid.toHex)) + val JString(rawTx) = sender.expectMsgType[JValue](10 seconds) + + // the unilateral close contains the static toRemote output + assert(Transaction.read(rawTx).txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + } + /** * We currently use p2pkh script Helpers.getFinalScriptPubKey */ @@ -990,7 +1048,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService generateBlocks(bitcoincli, 2, Some(address)) // and we wait for C'channel to close awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSED, max = 30 seconds) - awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 9, 11, 24) + awaitAnnouncements(nodes.filterKeys(_ == "A").toMap, 9, 11, 24) } test("propagate a failure upstream when a downstream htlc times out (local commit)") { @@ -1264,7 +1322,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService } awaitCond({ sender.send(nodes("D").router, 'channels) - sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 8 // 8 remaining channels because D->F{1-5} have disappeared + sender.expectMsgType[Iterable[ChannelAnnouncement]](5 seconds).size == channels.size + 8 // 8 remaining channels because D->F{1-6} have disappeared }, max = 120 seconds, interval = 1 second) } @@ -1284,6 +1342,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService logger.info(s"F3 -> ${nodes("F3").nodeParams.nodeId}") logger.info(s"F4 -> ${nodes("F4").nodeParams.nodeId}") logger.info(s"F5 -> ${nodes("F5").nodeParams.nodeId}") + logger.info(s"F6 -> ${nodes("F6").nodeParams.nodeId}") logger.info(s"G -> ${nodes("G").nodeParams.nodeId}") val channels1 = sender.expectMsgType[Relayer.OutgoingChannels] From 045d4e1d1d5b71db2182cd1418b9c1646ac36a44 Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 15 Apr 2020 14:10:32 +0200 Subject: [PATCH 06/12] Address PR feedback --- .../main/scala/fr/acinq/eclair/Features.scala | 6 +- .../fr/acinq/eclair/channel/Channel.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 4 +- .../fr/acinq/eclair/channel/Helpers.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 44 +++--- .../channel/states/h/ClosingStateSpec.scala | 126 ++++++++---------- .../eclair/integration/IntegrationSpec.scala | 12 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 21 ++- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 12 +- 9 files changed, 125 insertions(+), 104 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 d3b2a69fc3..a2ed4f93e4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -70,7 +70,7 @@ object Features { } case object StaticRemoteKey extends Feature { - val rfcName = "staticremotekey" + val rfcName = "option_static_remotekey" val mandatory = 12 } @@ -132,8 +132,8 @@ object Features { def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support) /** returns true if both have at least optional support */ - def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = { - hasFeature(localFeatures, feature, support) && hasFeature(remoteFeatures, feature, support) + def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature): Boolean = { + hasFeature(localFeatures, feature) && hasFeature(remoteFeatures, feature) } /** 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 d7c7439378..4d1c8a2e36 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 @@ -2104,7 +2104,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => val remoteCommitPublished = RemoteCommitPublished(commitTx, None, List.empty, List.empty, Map.empty) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address case _ => val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) 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 20a5bb0267..1ead8cec9f 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 @@ -629,8 +629,8 @@ object Commitments { case _ => keyManager.paymentPoint(channelKeyPath).publicKey } val localPaymentPubkey = channelVersion match { - case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get - case _ => Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localPaymentBasepoint + case _ => Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint) } val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) 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 9769bcf608..b525283a4f 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 @@ -587,7 +587,7 @@ object Helpers { /** * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey - * we don't claim our main output. + * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. * * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) 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 9b85df4fe7..b85587f5da 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 @@ -22,7 +22,7 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Bech32, ByteVector32, ByteVector64, Crypto, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ @@ -43,7 +43,6 @@ import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32, _} import org.scalatest.{Outcome, Tag} import scodec.bits._ -import scala.concurrent.Future import scala.concurrent.duration._ /** @@ -57,19 +56,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging override def withFixture(test: OneArgTest): Outcome = { - val testWallet = if(test.tags.contains("static_remotekey")){ - val randomKey = PrivateKey(randomBytes32).publicKey - new TestWallet{ - override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.successful(randomKey) - override def getReceiveAddress: Future[String] = Future.successful({ - val scriptPubKey = Script.write(Script.pay2wpkh(randomKey)) - Bech32.encodeWitnessAddress("bcrt", 0, scriptPubKey) - }) - } - } else { - new TestWallet - } - val setup = init(wallet = testWallet) + val setup = init() import setup._ within(30 seconds) { reachNormal(setup, test.tags) @@ -1119,7 +1106,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") def aliceToRemoteScript() = { @@ -1169,8 +1156,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { peer.expectMsg(Peer.Disconnect(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId)) } - test("recv CMD_FULFILL_HTLC") { f => + private def testReceiveCmdFulfillHtlc(f: FixtureParam): Unit = { import f._ + val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -1185,6 +1173,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) } + test("recv CMD_FULFILL_HTLC") { testReceiveCmdFulfillHtlc _ } + + test("recv CMD_FULFILL_HTLC (static_remotekey)", Tag("static_remotekey")) { testReceiveCmdFulfillHtlc _ } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1219,7 +1211,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42)) } - test("recv UpdateFulfillHtlc") { f => + private def testUpdateFulfillHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1239,6 +1231,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(forward.htlc === htlc) } + test("recv UpdateFulfillHtlc") { testUpdateFulfillHtlc _ } + + test("recv UpdateFulfillHtlc (static_remotekey)", Tag("(static_remotekey)")) { testUpdateFulfillHtlc _ } + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() @@ -1294,7 +1290,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_FAIL_HTLC") { f => + private def testCmdFailHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1308,8 +1304,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData == initialState.copy( commitments = initialState.commitments.copy( localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) + } + test("recv CMD_FAIL_HTLC") { testCmdFailHtlc _ } + + test("recv CMD_FAIL_HTLC (static_remotekey)", Tag("static_remotekey")) { testCmdFailHtlc _ } + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1377,7 +1378,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42)) } - test("recv UpdateFailHtlc") { f => + private def testUpdateFailHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1395,6 +1396,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg() } + test("recv UpdateFailHtlc") { testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (static_remotekey)", Tag("static_remotekey")) { testUpdateFailHtlc _ } + test("recv UpdateFailMalformedHtlc") { f => import f._ val sender = TestProbe() 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 f862e9e59c..7dd9faca34 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 @@ -548,6 +548,22 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 2) // two main outputs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + + // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) + assert(alice.stateName == CLOSING) + // once the remote commit is confirmed the channel is definitively closed + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + awaitCond(alice.stateName == CLOSED) + } + test("recv BITCOIN_TX_CONFIRMED (remote commit with multiple htlcs for the same payment)") { f => import f._ @@ -632,7 +648,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(100 millis) } - test("recv BITCOIN_TX_CONFIRMED (next remote commit)") { f => + private def testNextRemoteCommitTxConfirmed(f: FixtureParam): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { import f._ // alice sends a first htlc to bob @@ -655,7 +671,12 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTimeoutTxs.length === 3) + (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) + } + test("recv BITCOIN_TX_CONFIRMED (next remote commit)") { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimMainOutputTx.get), 45, 0, closingState.claimMainOutputTx.get) relayerA.expectNoMsg(100 millis) @@ -667,7 +688,26 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(250 millis) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(2)), 203, 1, closingState.claimHtlcTimeoutTxs(2)) val forwardedFail3 = relayerA.expectMsgType[ForwardOnChainFail].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === Set(htlca1, htlca2, htlca3)) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + relayerA.expectNoMsg(250 millis) + awaitCond(alice.stateName == CLOSED) + } + + test("recv BITCOIN_TX_CONFIRMED (next remote commit, static_remotekey)", Tag("static_remotekey")) { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) + assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output + relayerA.expectNoMsg(100 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs.head), 201, 0, closingState.claimHtlcTimeoutTxs.head) + val forwardedFail1 = relayerA.expectMsgType[ForwardOnChainFail].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(1)), 202, 0, closingState.claimHtlcTimeoutTxs(1)) + val forwardedFail2 = relayerA.expectMsgType[ForwardOnChainFail].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(2)), 203, 1, closingState.claimHtlcTimeoutTxs(2)) + val forwardedFail3 = relayerA.expectMsgType[ForwardOnChainFail].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) relayerA.expectNoMsg(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -722,23 +762,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(100 millis) } - test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => - import f._ - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxes.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - - // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) - assert(alice.stateName == CLOSING) - // once the remote commit is confirmed the channel is definitively closed - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) - awaitCond(alice.stateName == CLOSED) - } - - test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => + private def testFutureRemoteCommitTxConfirmed(f: FixtureParam): Transaction = { import f._ val sender = TestProbe() val oldStateData = alice.stateData @@ -777,6 +801,12 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + bobCommitTx + } + + test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => + import f._ + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -791,42 +821,9 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (future remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - val sender = TestProbe() - val oldStateData = alice.stateData - val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) - // then we manually replace alice's state with an older one - alice.setState(OFFLINE, oldStateData) - // then we reconnect them - val aliceInit = Init(Alice.nodeParams.features) - val bobInit = Init(Bob.nodeParams.features) - sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - bob2alice.expectMsgType[ChannelReestablish] - // alice then realizes it has an old state... - bob2alice.forward(alice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) - // alice now waits for bob to publish its commitment - 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.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) // using option_static_remotekey alice doesn't need to swipe her output awaitCond(alice.stateName == CLOSING, 10 seconds) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) @@ -834,7 +831,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED, 10 seconds) } - test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => + private def testFundingSpentRevokedTx(f: FixtureParam) = { import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -842,6 +839,13 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobRevokedTx = bobCommitTxes.head.commitTx.tx alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) + } + + test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => + import f._ + testFundingSpentRevokedTx(f) // alice publishes and watches the penalty tx alice2blockchain.expectMsgType[PublishAsap] // claim-main alice2blockchain.expectMsgType[PublishAsap] // main-penalty @@ -851,20 +855,11 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchSpent] // main-penalty alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } - test("recv BITCOIN_FUNDING_SPENT (one revoked tx, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // bob publishes one of his revoked txes - val bobRevokedTx = bobCommitTxes.head.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) - + testFundingSpentRevokedTx(f) // alice publishes and watches the penalty tx, but she won't claim her main output (claim-main) alice2blockchain.expectMsgType[PublishAsap] // main-penalty alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty @@ -872,9 +867,6 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchSpent] // main-penalty alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { f => 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 d739f7c39a..c83c81e371 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 @@ -849,7 +849,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F6").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F6").nodeParams.nodeId, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID](5 seconds) val ps = sender.expectMsgType[PaymentSent](5 seconds) assert(ps.id == paymentId) @@ -864,7 +864,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(commitmentIndex == initialCommitmentIndex + 1) // script pubkeys of toRemote output remained the same across commitments - assert(toRemoteOutC.publicKeyScript == toRemoteOutCNew.publicKeyScript) + assert(toRemoteOutCNew.publicKeyScript == toRemoteOutC.publicKeyScript) + assert(toRemoteOutCNew.amount < toRemoteOutC.amount) // now let's force close the channel and check the toRemote is what we had at the beginning sender.send(nodes("F6").register, Forward(channelId, CMD_FORCECLOSE)) @@ -880,6 +881,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // the unilateral close contains the static toRemote output assert(Transaction.read(rawTx).txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + + // bury the unilateral close in a block, since there are no outputs to claim the channel can go to CLOSED state + generateBlocks(bitcoincli, 2) + awaitCond({ + sender.send(nodes("C").register, Forward(channelId, CMD_GETSTATE)) + sender.expectMsgType[State] == CLOSED + }, max = 20 seconds, interval = 1 second) } /** 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 857bc427eb..0881ec8a46 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 @@ -21,13 +21,13 @@ import java.net.{InetAddress, ServerSocket} import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Btc +import fr.acinq.bitcoin.{Btc, Script} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.channel.{Channel, ChannelCreated, HasCommitments} +import fr.acinq.eclair.channel.{CMD_GETINFO, Channel, ChannelCreated, ChannelVersion, DATA_NORMAL, HasCommitments, NORMAL, RES_GETINFO} import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, NodeAddress, NodeAnnouncement} import org.scalatest.{Outcome, Tag} @@ -53,6 +53,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams + .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(hex"2200") .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -220,4 +221,20 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(channelCreated.initialFeeratePerKw == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.commitmentBlockTarget)) assert(channelCreated.fundingTxFeeratePerKw.get == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.fundingBlockTarget)) } + + test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => + import f._ + + val probe = TestProbe() + connect(remoteNodeId, switchboard, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey + peer.stateData.channels.foreach { case (_, channelRef) => + probe.send(channelRef, CMD_GETINFO) + val info = probe.expectMsgType[RES_GETINFO] + assert(info.state == NORMAL) + val commitments = info.data.asInstanceOf[DATA_NORMAL].commitments + assert(commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) + assert(commitments.localParams.localPaymentBasepoint.isDefined) + assert(commitments.localParams.defaultFinalScriptPubKey === Script.pay2wpkh(commitments.localParams.localPaymentBasepoint.get)) + } + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index c38ae48497..9410d5a951 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -101,9 +101,9 @@ class ChannelCodecsSpec extends AnyFunSuite { isFunder = Random.nextBoolean(), features = randomBytes(256)) val encoded = localParamsCodec(ChannelVersion.ZEROES).encode(o).require - val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require - assert(o.localPaymentBasepoint.isEmpty) - assert(o === decoded.value) + val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require.value + assert(decoded.localPaymentBasepoint.isEmpty) + assert(o === decoded) // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" @@ -124,9 +124,9 @@ class ChannelCodecsSpec extends AnyFunSuite { isFunder = Random.nextBoolean(), features = randomBytes(256)) val encoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).encode(o1).require - val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require - assert(o1.localPaymentBasepoint.isDefined) - assert(o1 === decoded1.value) + val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require.value + assert(decoded1.localPaymentBasepoint.isDefined) + assert(o1 === decoded1) } test("encode/decode remoteparams") { From 819ec935c846668a9a81632c46ea14f0ecc4d1bf Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 16 Apr 2020 09:15:58 +0200 Subject: [PATCH 07/12] fixup! Correct test for static_remotekey in PeerSpec --- .../scala/fr/acinq/eclair/channel/Helpers.scala | 7 ++++--- .../fr/acinq/eclair/channel/CommitmentsSpec.scala | 2 +- .../eclair/channel/states/h/ClosingStateSpec.scala | 2 +- .../test/scala/fr/acinq/eclair/io/PeerSpec.scala | 14 ++++++++------ 4 files changed, 14 insertions(+), 11 deletions(-) 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 b525283a4f..a861182334 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 @@ -728,9 +728,10 @@ object Helpers { None case _ => generateTx("claim-p2wpkh-output") { Transactions.makeClaimP2WPKHOutputTx(tx, localParams.dustLimit, localPaymentPubkey, localParams.defaultFinalScriptPubKey, feeratePerKwMain).right.map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) - Transactions.addSigs(claimMain, localPaymentPubkey, sig) - })} + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint) + Transactions.addSigs(claimMain, localPaymentPubkey, sig) + }) + } } // then we punish them by stealing their main output diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 6d789ecb42..5b227161c0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -511,7 +511,7 @@ object CommitmentsSpec { } def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = { - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, ByteVector.empty) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, ByteVector.empty) val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey) Commitments( 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 7dd9faca34..7598610cc1 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 @@ -824,7 +824,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv BITCOIN_TX_CONFIRMED (future remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) - // using option_static_remotekey alice doesn't need to swipe her output + // using option_static_remotekey alice doesn't need to sweep her output awaitCond(alice.stateName == CLOSING, 10 seconds) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) // after the commit tx is confirmed the channel is closed, no claim transactions needed 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 0881ec8a46..5541dade96 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 @@ -27,7 +27,7 @@ import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.channel.{CMD_GETINFO, Channel, ChannelCreated, ChannelVersion, DATA_NORMAL, HasCommitments, NORMAL, RES_GETINFO} +import fr.acinq.eclair.channel.{CMD_GETINFO, Channel, ChannelCreated, ChannelVersion, DATA_WAIT_FOR_ACCEPT_CHANNEL, HasCommitments, RES_GETINFO, WAIT_FOR_ACCEPT_CHANNEL} import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, NodeAddress, NodeAnnouncement} import org.scalatest.{Outcome, Tag} @@ -227,14 +227,16 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val probe = TestProbe() connect(remoteNodeId, switchboard, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None)) + awaitCond(peer.stateData.channels.nonEmpty) peer.stateData.channels.foreach { case (_, channelRef) => probe.send(channelRef, CMD_GETINFO) val info = probe.expectMsgType[RES_GETINFO] - assert(info.state == NORMAL) - val commitments = info.data.asInstanceOf[DATA_NORMAL].commitments - assert(commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) - assert(commitments.localParams.localPaymentBasepoint.isDefined) - assert(commitments.localParams.defaultFinalScriptPubKey === Script.pay2wpkh(commitments.localParams.localPaymentBasepoint.get)) + assert(info.state == WAIT_FOR_ACCEPT_CHANNEL) + val inputInit = info.data.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder + assert(inputInit.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) + assert(inputInit.localParams.localPaymentBasepoint.isDefined) + assert(inputInit.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(inputInit.localParams.localPaymentBasepoint.get))) } } } From 781b784aaace678c62edcabee1acac51d8c1765f Mon Sep 17 00:00:00 2001 From: Andrea Date: Wed, 6 May 2020 16:01:18 +0200 Subject: [PATCH 08/12] fixup! Remove deprecated parameter in `connect` call in PeerSpec --- eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 372a48ad1e..a68bc2032b 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 @@ -287,7 +287,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe import f._ val probe = TestProbe() - connect(remoteNodeId, switchboard, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey + connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None)) awaitCond(peer.stateData.channels.nonEmpty) peer.stateData.channels.foreach { case (_, channelRef) => From a71c0df5056c44bda6bd4bf11528b9cdfa13a72e Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Wed, 6 May 2020 15:58:08 +0200 Subject: [PATCH 09/12] Merge with 'master' --- BUILD.md | 34 +- eclair-core/pom.xml | 1 + eclair-core/src/main/resources/reference.conf | 29 +- .../main/scala/fr/acinq/eclair/Eclair.scala | 10 +- .../main/scala/fr/acinq/eclair/Features.scala | 190 ++++++--- .../scala/fr/acinq/eclair/NodeParams.scala | 19 +- .../scala/fr/acinq/eclair/PimpKamon.scala | 2 +- .../main/scala/fr/acinq/eclair/Setup.scala | 20 +- .../blockchain/fee/FallbackFeeProvider.scala | 2 +- .../eclair/blockchain/fee/FeeProvider.scala | 19 +- .../acinq/eclair/channel/ChannelTypes.scala | 6 +- .../fr/acinq/eclair/channel/Helpers.scala | 6 +- .../fr/acinq/eclair/crypto/KeyManager.scala | 5 +- .../acinq/eclair/crypto/LocalKeyManager.scala | 4 +- .../eclair/crypto/TransportHandler.scala | 5 +- .../fr/acinq/eclair/db/BackupHandler.scala | 4 +- .../scala/fr/acinq/eclair/db/PaymentsDb.scala | 4 +- .../scala/fr/acinq/eclair/io/Client.scala | 30 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 49 ++- .../fr/acinq/eclair/io/PeerConnection.scala | 57 +-- .../fr/acinq/eclair/io/ReconnectionTask.scala | 8 +- .../fr/acinq/eclair/payment/Monitoring.scala | 2 +- .../acinq/eclair/payment/PaymentEvents.scala | 56 ++- .../acinq/eclair/payment/PaymentPacket.scala | 7 +- .../acinq/eclair/payment/PaymentRequest.scala | 38 +- .../payment/receive/MultiPartHandler.scala | 4 +- .../eclair/payment/relay/NodeRelayer.scala | 6 +- .../relay/PostRestartHtlcCleaner.scala | 6 +- .../send/MultiPartPaymentLifecycle.scala | 15 +- .../eclair/payment/send/PaymentError.scala | 4 +- .../payment/send/PaymentInitiator.scala | 21 +- .../payment/send/PaymentLifecycle.scala | 98 ++--- .../acinq/eclair/router/Announcements.scala | 12 +- .../scala/fr/acinq/eclair/router/Graph.scala | 362 ++++++++--------- .../eclair/router/RouteCalculation.scala | 30 +- .../scala/fr/acinq/eclair/router/Router.scala | 10 +- .../fr/acinq/eclair/router/Validation.scala | 17 +- .../eclair/wire/LightningMessageCodecs.scala | 19 +- .../eclair/wire/LightningMessageTypes.scala | 8 +- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 195 +++++++-- .../scala/fr/acinq/eclair/StartupSpec.scala | 54 ++- .../scala/fr/acinq/eclair/TestConstants.scala | 11 +- .../eclair/channel/CommitmentsSpec.scala | 8 +- .../fr/acinq/eclair/channel/FuzzyPipe.scala | 3 +- .../states/StateTestsHelperMethods.scala | 6 +- .../a/WaitForAcceptChannelStateSpec.scala | 8 +- .../a/WaitForOpenChannelStateSpec.scala | 6 +- .../channel/states/e/NormalStateSpec.scala | 5 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 12 +- .../eclair/db/SqlitePaymentsDbSpec.scala | 9 +- .../eclair/integration/IntegrationSpec.scala | 51 ++- .../acinq/eclair/io/PeerConnectionSpec.scala | 32 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 18 +- .../eclair/io/ReconnectionTaskSpec.scala | 4 +- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 4 +- .../eclair/payment/MultiPartHandlerSpec.scala | 28 +- .../MultiPartPaymentLifecycleSpec.scala | 8 +- .../eclair/payment/NodeRelayerSpec.scala | 12 +- .../eclair/payment/PaymentInitiatorSpec.scala | 41 +- .../eclair/payment/PaymentLifecycleSpec.scala | 100 +++-- .../eclair/payment/PaymentPacketSpec.scala | 39 +- .../eclair/payment/PaymentRequestSpec.scala | 42 +- .../AnnouncementsBatchValidationSpec.scala | 10 +- .../eclair/router/AnnouncementsSpec.scala | 6 +- .../acinq/eclair/router/BaseRouterSpec.scala | 24 +- .../fr/acinq/eclair/router/GraphSpec.scala | 22 + .../eclair/router/RouteCalculationSpec.scala | 383 +++++++++++------- .../fr/acinq/eclair/router/RouterSpec.scala | 147 ++++--- .../acinq/eclair/router/RoutingSyncSpec.scala | 6 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 20 +- .../wire/LightningMessageCodecsSpec.scala | 8 +- .../src/main/resources/eclair-node-gui.sh | 6 +- .../scala/fr/acinq/eclair/JavafxBoot.scala | 7 +- .../scala/fr/acinq/eclair/gui/FxApp.scala | 5 +- .../fr/acinq/eclair/gui/GUIUpdater.scala | 2 +- .../src/main/resources/application.conf | 32 -- eclair-node/src/main/resources/eclair-node.sh | 6 +- .../src/main/scala/fr/acinq/eclair/Boot.scala | 19 +- .../fr/acinq/eclair/api/JsonSerializers.scala | 26 +- eclair-node/src/test/resources/api/getinfo | 2 +- .../src/test/resources/api/received-expired | 2 +- .../src/test/resources/api/received-pending | 2 +- .../src/test/resources/api/received-success | 2 +- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- .../eclair/api/JsonSerializersSpec.scala | 4 +- pom.xml | 4 +- 86 files changed, 1650 insertions(+), 1014 deletions(-) diff --git a/BUILD.md b/BUILD.md index 21524a3f7b..dac668e252 100644 --- a/BUILD.md +++ b/BUILD.md @@ -8,7 +8,7 @@ ## Build -Eclair is packaged as a compressed archive with a launcher script, the archives are built deterministically +Eclair is packaged as a compressed archive with a launcher script. The archives are built deterministically so it's possible to reproduce the build and verify its equality byte-by-byte. To build the exact same artifacts that we release, you must use the build environment (OS, JDK, maven...) that we specify in our release notes. @@ -18,23 +18,29 @@ To build the project and run the tests, simply run: mvn package ``` -NB: some of the tests use [Docker](https://www.docker.com/), so make sure your docker daemon is running. +Notes: +- This command will build all modules (core, node, gui). +- If the build fails, you may need to clean previously built artifacts with the `mvn clean` command. +- Some of the tests use [Docker](https://www.docker.com/), so make sure your docker daemon is running. +- Archives can be found in the `target` folder for each module. -### Other build options +### Skip tests -To skip all tests, run: +Running tests takes time. If you want to skip them, use `-DskipTests`: ```shell mvn package -DskipTests ``` -To only build the `eclair-node` module, run: +You can even skip the tests compilation with `maven.test.skip`: ```shell -mvn package -pl eclair-node -am -DskipTests +mvn package -Dmaven.test.skip=true ``` -To run the tests, run: +### Run tests + +To only run the tests, run: ```shell mvn test @@ -46,6 +52,20 @@ To run tests for a specific class, run: mvn test -Dsuites=* ``` +### Build specific module + +To only build the `eclair-node` module, run: + +```shell +mvn package -pl eclair-node -am -Dmaven.test.skip=true +``` + +To install `eclair-core` into your local maven repository and use it in another project, run: + +```shell +mvn clean install -pl eclair-core -am -Dmaven.test.skip=true +``` + ## Build the API documentation ### Slate diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index f1278dcc3a..16abb57321 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -43,6 +43,7 @@ wget + ${maven.test.skip} ${bitcoind.url} true ${project.build.directory} diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8a356cf400..f67b0d4779 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -38,11 +38,18 @@ eclair { node-color = "49daaa" trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag - features = "0a8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries + option_channel_range_queries_ex + variable_length_onion + // see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md + features { + initial_routing_sync = optional + option_data_loss_protect = optional + gossip_queries = optional + gossip_queries_ex = optional + var_onion_optin = optional + } override-features = [ // optional per-node features # { # nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - # features = "" + # features { } # } ] sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers @@ -179,4 +186,22 @@ eclair { executor = "thread-pool-executor" type = PinnedDispatcher } +} + +akka { + io { + tcp { + # The maximum number of bytes delivered by a `Received` message. Before + # more data is read from the network the connection actor will try to + # do other work. + # The purpose of this setting is to impose a smaller limit than the + # configured receive buffer size. When using value 'unlimited' it will + # try to read all from the receive buffer. + # As per BOLT#8 lightning messages are at most 2 + 16 + 65535 + 16 = 65569bytes + # Currently the largest message is update_add_htlc (~1500b). + # As a tradeoff to reduce the RAM consumption, in conjunction with tcp pull mode, + # the default value is chosen to allow for a decent number of messages to be prefetched. + max-received-message-size = 16384b + } + } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index e74b211f35..953961e883 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats} import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} -import fr.acinq.eclair.io.{NodeURI, Peer} +import fr.acinq.eclair.io.{NodeURI, Peer, PeerConnection} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance} @@ -42,7 +42,7 @@ import scodec.bits.ByteVector import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) +case class GetInfoResponse(version: String, nodeId: PublicKey, alias: String, color: String, features: Features, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress]) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) @@ -128,8 +128,8 @@ class EclairImpl(appKit: Kit) extends Eclair { private val externalIdMaxLength = 66 override def connect(target: Either[NodeURI, PublicKey])(implicit timeout: Timeout): Future[String] = target match { - case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[String] - case Right(pubKey) => (appKit.switchboard ? Peer.Connect(pubKey, None)).mapTo[String] + case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[PeerConnection.ConnectionResult].map(_.toString) + case Right(pubKey) => (appKit.switchboard ? Peer.Connect(pubKey, None)).mapTo[PeerConnection.ConnectionResult].map(_.toString) } override def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] = { @@ -317,7 +317,7 @@ class EclairImpl(appKit: Kit) extends Eclair { GetInfoResponse( version = Kit.getVersionLong, color = appKit.nodeParams.color.toString, - features = appKit.nodeParams.features.toHex, + features = appKit.nodeParams.features, nodeId = appKit.nodeParams.nodeId, alias = appKit.nodeParams.alias, chainHash = appKit.nodeParams.chainHash, 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 a2ed4f93e4..dd78e447df 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -16,6 +16,9 @@ package fr.acinq.eclair +import com.typesafe.config.Config +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{BasicMultiPartPayment, PaymentSecret} import scodec.bits.{BitVector, ByteVector} /** @@ -26,23 +29,115 @@ sealed trait FeatureSupport // @formatter:off object FeatureSupport { - case object Mandatory extends FeatureSupport - case object Optional extends FeatureSupport + case object Mandatory extends FeatureSupport { override def toString: String = "mandatory" } + case object Optional extends FeatureSupport { override def toString: String = "optional" } } -// @formatter:on sealed trait Feature { - def rfcName: String + def rfcName: String def mandatory: Int - def optional: Int = mandatory + 1 + def supportBit(support: FeatureSupport): Int = support match { + case FeatureSupport.Mandatory => mandatory + case FeatureSupport.Optional => optional + } + override def toString = rfcName + +} +// @formatter:on + +case class ActivatedFeature(feature: Feature, support: FeatureSupport) + +case class UnknownFeature(bitIndex: Int) + +case class Features(activated: Set[ActivatedFeature], unknown: Set[UnknownFeature] = Set.empty) { + + def hasFeature(feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match { + case Some(s) => activated.contains(ActivatedFeature(feature, s)) + case None => hasFeature(feature, Some(Optional)) || hasFeature(feature, Some(Mandatory)) + } + + def toByteVector: ByteVector = { + val activatedFeatureBytes = toByteVectorFromIndex(activated.map { case ActivatedFeature(f, s) => f.supportBit(s) }) + val unknownFeatureBytes = toByteVectorFromIndex(unknown.map(_.bitIndex)) + val maxSize = activatedFeatureBytes.size.max(unknownFeatureBytes.size) + activatedFeatureBytes.padLeft(maxSize) | unknownFeatureBytes.padLeft(maxSize) + } + + private def toByteVectorFromIndex(indexes: Set[Int]): ByteVector = { + if (indexes.isEmpty) return ByteVector.empty + // When converting from BitVector to ByteVector, scodec pads right instead of left, so we make sure we pad to bytes *before* setting feature bits. + var buf = BitVector.fill(indexes.max + 1)(high = false).bytes.bits + indexes.foreach { i => + buf = buf.set(i) + } + buf.reverse.bytes + } + + /** + * Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask + * off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue. + * We use a long enough mask to account for future features. + * TODO: remove that once eclair-mobile is patched. + */ + def maskFeaturesForEclairMobile(): Features = { + Features( + activated = activated.filter { + case ActivatedFeature(PaymentSecret, _) => false + case ActivatedFeature(BasicMultiPartPayment, _) => false + case _ => true + }, + unknown = unknown + ) + } + } object Features { + def empty = Features(Set.empty[ActivatedFeature]) + + def apply(features: Set[ActivatedFeature]): Features = Features(activated = features) + + def apply(bytes: ByteVector): Features = apply(bytes.bits) + + def apply(bits: BitVector): Features = { + val all = bits.toIndexedSeq.reverse.zipWithIndex.collect { + case (true, idx) if knownFeatures.exists(_.optional == idx) => Right(ActivatedFeature(knownFeatures.find(_.optional == idx).get, Optional)) + case (true, idx) if knownFeatures.exists(_.mandatory == idx) => Right(ActivatedFeature(knownFeatures.find(_.mandatory == idx).get, Mandatory)) + case (true, idx) => Left(UnknownFeature(idx)) + } + Features( + activated = all.collect { case Right(af) => af }.toSet, + unknown = all.collect { case Left(inf) => inf }.toSet + ) + } + + /** expects to have a top level config block named "features" */ + def fromConfiguration(config: Config): Features = Features( + knownFeatures.flatMap { + feature => + getFeature(config, feature.rfcName) match { + case Some(support) => Some(ActivatedFeature(feature, support)) + case _ => None + } + }) + + /** tries to extract the given feature name from the config, if successful returns its feature support */ + private def getFeature(config: Config, name: String): Option[FeatureSupport] = { + if (!config.hasPath(s"features.$name")) None + else { + config.getString(s"features.$name") match { + case support if support == Mandatory.toString => Some(Mandatory) + case support if support == Optional.toString => Some(Optional) + case wrongSupport => throw new IllegalArgumentException(s"Wrong support specified ($wrongSupport)") + } + } + } + case object OptionDataLossProtect extends Feature { val rfcName = "option_data_loss_protect" val mandatory = 0 @@ -85,7 +180,7 @@ object Features { } case object Wumbo extends Feature { - val rfcName = "large_channel_support" + val rfcName = "option_support_large_channel" val mandatory = 18 } @@ -97,6 +192,29 @@ object Features { val mandatory = 50 } + val knownFeatures: Set[Feature] = Set( + OptionDataLossProtect, + InitialRoutingSync, + ChannelRangeQueries, + VariableLengthOnion, + ChannelRangeQueriesExtended, + PaymentSecret, + BasicMultiPartPayment, + Wumbo, + TrampolinePayment, + StaticRemoteKey + ) + + private val supportedMandatoryFeatures: Set[Feature] = Set( + OptionDataLossProtect, + ChannelRangeQueries, + VariableLengthOnion, + ChannelRangeQueriesExtended, + PaymentSecret, + BasicMultiPartPayment, + Wumbo + ) + // Features may depend on other features, as specified in Bolt 9. private val featuresDependency = Map( ChannelRangeQueriesExtended -> (ChannelRangeQueries :: Nil), @@ -109,59 +227,25 @@ object Features { case class FeatureException(message: String) extends IllegalArgumentException(message) - def validateFeatureGraph(features: BitVector): Option[FeatureException] = featuresDependency.collectFirst { - case (feature, dependencies) if hasFeature(features, feature) && dependencies.exists(d => !hasFeature(features, d)) => - FeatureException(s"${features.toBin} sets $feature but is missing a dependency (${dependencies.filter(d => !hasFeature(features, d)).mkString(" and ")})") - } - - def validateFeatureGraph(features: ByteVector): Option[FeatureException] = validateFeatureGraph(features.bits) - - // Note that BitVector indexes from left to right whereas the specification indexes from right to left. - // This is why we have to reverse the bits to check if a feature is set. - - private def hasFeature(features: BitVector, bit: Int): Boolean = features.sizeGreaterThan(bit) && features.reverse.get(bit) - - def hasFeature(features: BitVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = support match { - case Some(FeatureSupport.Mandatory) => hasFeature(features, feature.mandatory) - case Some(FeatureSupport.Optional) => hasFeature(features, feature.optional) - case None => hasFeature(features, feature.optional) || hasFeature(features, feature.mandatory) - } - - def hasFeature(features: ByteVector, feature: Feature): Boolean = hasFeature(features.bits, feature) - - def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support) - - /** returns true if both have at least optional support */ - def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature): Boolean = { - hasFeature(localFeatures, feature) && hasFeature(remoteFeatures, feature) + def validateFeatureGraph(features: Features): Option[FeatureException] = featuresDependency.collectFirst { + case (feature, dependencies) if features.hasFeature(feature) && dependencies.exists(d => !features.hasFeature(d)) => + FeatureException(s"$feature is set but is missing a dependency (${dependencies.filter(d => !features.hasFeature(d)).mkString(" and ")})") } /** - * Check that the features that we understand are correctly specified, and that there are no mandatory features that - * we don't understand (even bits). + * A feature set is supported if all even bits are supported. + * We just ignore unknown odd bits. */ - def areSupported(features: BitVector): Boolean = { - val supportedMandatoryFeatures = Set( - OptionDataLossProtect, - ChannelRangeQueries, - VariableLengthOnion, - ChannelRangeQueriesExtended, - PaymentSecret, - BasicMultiPartPayment, - Wumbo - ).map(_.mandatory.toLong) - val reversed = features.reverse - for (i <- 0L until reversed.length by 2) { - if (reversed.get(i) && !supportedMandatoryFeatures.contains(i)) return false + def areSupported(features: Features): Boolean = { + !features.unknown.exists(_.bitIndex % 2 == 0) && features.activated.forall { + case ActivatedFeature(_, Optional) => true + case ActivatedFeature(feature, Mandatory) => supportedMandatoryFeatures.contains(feature) } - - true } - /** - * A feature set is supported if all even bits are supported. - * We just ignore unknown odd bits. - */ - def areSupported(features: ByteVector): Boolean = areSupported(features.bits) + /** returns true if both have at least optional support */ + def canUseFeature(localFeatures: Features, remoteFeatures: Features, feature: Feature): Boolean = { + localFeatures.hasFeature(feature) && remoteFeatures.hasFeature(feature) + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 07b94be9ca..cca678cfa9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -22,7 +22,7 @@ import java.nio.file.Files import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong -import com.typesafe.config.{Config, ConfigFactory} +import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} import fr.acinq.eclair.NodeParams.WatcherType @@ -46,8 +46,8 @@ case class NodeParams(keyManager: KeyManager, alias: String, color: Color, publicAddresses: List[NodeAddress], - features: ByteVector, - overrideFeatures: Map[PublicKey, ByteVector], + features: Features, + overrideFeatures: Map[PublicKey, Features], syncWhitelist: Set[PublicKey], dustLimit: Satoshi, onChainFeeConf: OnChainFeeConf, @@ -105,10 +105,9 @@ object NodeParams { * 3) Optionally provided config * 4) Default values in reference.conf */ - def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) = + def loadConfiguration(datadir: File) = ConfigFactory.parseProperties(System.getProperties) .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) - .withFallback(overrideDefaults) .withFallback(ConfigFactory.load()) def getSeed(datadir: File): ByteVector = { @@ -147,6 +146,10 @@ object NodeParams { case (old, new_) => require(!config.hasPath(old), s"configuration key '$old' has been replaced by '$new_'") } + // since v0.4.1 features cannot be a byte vector (hex string) + val isFeatureByteVector = config.getValue("features").valueType() == ConfigValueType.STRING + require(!isFeatureByteVector, "configuration key 'features' have moved from bytevector to human readable (ex: 'feature-name' = optional/mandatory)") + val chain = config.getString("chain") val chainHash = makeChainHash(chain) @@ -180,13 +183,13 @@ object NodeParams { val nodeAlias = config.getString("node-alias") require(nodeAlias.getBytes("UTF-8").length <= 32, "invalid alias, too long (max allowed 32 bytes)") - val features = ByteVector.fromValidHex(config.getString("features")) + val features = Features.fromConfiguration(config) val featuresErr = Features.validateFeatureGraph(features) require(featuresErr.isEmpty, featuresErr.map(_.message)) - val overrideFeatures: Map[PublicKey, ByteVector] = config.getConfigList("override-features").asScala.map { e => + val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-features").asScala.map { e => val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid"))) - val f = ByteVector.fromValidHex(e.getString("features")) + val f = Features.fromConfiguration(e) p -> f }.toMap diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala index 07c44df14d..6ae751c623 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala @@ -45,7 +45,7 @@ object KamonExt { */ def failSpan(span: Span, failure: PaymentFailure) = { failure match { - case LocalFailure(t) => span.fail("local failure", t) + case LocalFailure(_, t) => span.fail("local failure", t) case RemoteFailure(_, e) => span.fail(s"remote failure: origin=${e.originNode} error=${e.failureMessage}") case UnreadableRemoteFailure(_) => span.fail("unreadable remote failure") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 868ef19dc7..c4a0828a45 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -57,6 +57,7 @@ import scodec.bits.ByteVector import scala.concurrent._ import scala.concurrent.duration._ +import scala.util.{Failure, Success} /** * Setup eclair from a data directory. @@ -64,12 +65,10 @@ import scala.concurrent.duration._ * Created by PM on 25/01/2016. * * @param datadir directory where eclair-core will write/read its data. - * @param overrideDefaults use this parameter to programmatically override the node configuration . * @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file. * @param db optional databases to use, if not set eclair will create the necessary databases */ class Setup(datadir: File, - overrideDefaults: Config = ConfigFactory.empty(), seed_opt: Option[ByteVector] = None, db: Option[Databases] = None)(implicit system: ActorSystem) extends Logging { @@ -86,8 +85,7 @@ class Setup(datadir: File, secureRandom.nextInt() datadir.mkdirs() - val appConfig = NodeParams.loadConfiguration(datadir, overrideDefaults) - val config = appConfig.getConfig("eclair") + val config = system.settings.config.getConfig("eclair") val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) val chain = config.getString("chain") val chaindir = new File(datadir, chain) @@ -234,19 +232,23 @@ class Setup(datadir: File, smoothFeerateWindow = config.getInt("smooth-feerate-window") readTimeout = FiniteDuration(config.getDuration("feerate-provider-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) feeProvider = (nodeParams.chainHash, bitcoin) match { - case (Block.RegtestGenesisBlock.hash, _) => new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) + case (Block.RegtestGenesisBlock.hash, _) => + new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) case (_, Bitcoind(bitcoinClient)) => - new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash, readTimeout), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(readTimeout), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! + new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash, readTimeout), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(readTimeout), smoothFeerateWindow) :: Nil, minFeeratePerByte) // order matters! case _ => - new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash, readTimeout), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(readTimeout), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! + new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash, readTimeout), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(readTimeout), smoothFeerateWindow) :: Nil, minFeeratePerByte) // order matters! } - _ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.map { - case feerates: FeeratesPerKB => + _ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.onComplete { + case Success(feerates) => feeratesPerKB.set(feerates) feeratesPerKw.set(FeeratesPerKw(feerates)) system.eventStream.publish(CurrentFeerates(feeratesPerKw.get)) logger.info(s"current feeratesPerKB=${feeratesPerKB.get()} feeratesPerKw=${feeratesPerKw.get()}") feeratesRetrieved.trySuccess(Done) + case Failure(exception) => + logger.warn(s"cannot retrieve feerates: ${exception.getMessage}") + feeratesRetrieved.tryFailure(CannotRetrieveFeerates) }) _ <- feeratesRetrieved.future diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala index a6a7c25da4..3738fd3f90 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala @@ -28,7 +28,7 @@ import scala.concurrent.{ExecutionContext, Future} */ class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: Long)(implicit ec: ExecutionContext) extends FeeProvider with Logging { - require(providers.size >= 1, "need at least one fee provider") + require(providers.nonEmpty, "need at least one fee provider") require(minFeeratePerByte > 0, "minimum fee rate must be strictly greater than 0") def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerKB] = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala index 35e7b75169..0faba06759 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeProvider.scala @@ -21,19 +21,19 @@ import fr.acinq.eclair._ import scala.concurrent.Future /** - * Created by PM on 09/07/2017. - */ + * Created by PM on 09/07/2017. + */ trait FeeProvider { - def getFeerates: Future[FeeratesPerKB] - } +case object CannotRetrieveFeerates extends RuntimeException("cannot retrieve feerates: channels may be at risk") + // stores fee rate in satoshi/kb (1 kb = 1000 bytes) case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long, blocks_144: Long) { require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0 && blocks_144 > 0, "all feerates must be strictly greater than 0") - def feePerBlock(target: Int) = target match { + def feePerBlock(target: Int): Long = target match { case 1 => block_1 case 2 => blocks_2 case t if t <= 6 => blocks_6 @@ -48,7 +48,7 @@ case class FeeratesPerKB(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_1 case class FeeratesPerKw(block_1: Long, blocks_2: Long, blocks_6: Long, blocks_12: Long, blocks_36: Long, blocks_72: Long, blocks_144: Long) { require(block_1 > 0 && blocks_2 > 0 && blocks_6 > 0 && blocks_12 > 0 && blocks_36 > 0 && blocks_72 > 0 && blocks_144 > 0, "all feerates must be strictly greater than 0") - def feePerBlock(target: Int) = target match { + def feePerBlock(target: Int): Long = target match { case 1 => block_1 case 2 => blocks_2 case t if t <= 6 => blocks_6 @@ -69,12 +69,7 @@ object FeeratesPerKw { blocks_72 = feerateKB2Kw(feerates.blocks_72), blocks_144 = feerateKB2Kw(feerates.blocks_144)) - /** - * Used in tests - * - * @param feeratePerKw - * @return - */ + /** Used in tests */ def single(feeratePerKw: Long): FeeratesPerKw = FeeratesPerKw( block_1 = feeratePerKw, blocks_2 = feeratePerKw, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 1535a77ec5..4db24fc2d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, T import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.{BitVector, ByteVector} /** @@ -230,7 +230,7 @@ final case class LocalParams(nodeId: PublicKey, isFunder: Boolean, defaultFinalScriptPubKey: ByteVector, localPaymentBasepoint: Option[PublicKey], - features: ByteVector) + features: Features) final case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, @@ -244,7 +244,7 @@ final case class RemoteParams(nodeId: PublicKey, paymentBasepoint: PublicKey, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, - features: ByteVector) + features: Features) object ChannelFlags { val AnnounceChannel = 0x01.toByte 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 0d20f83421..e8bb409627 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 @@ -20,7 +20,6 @@ import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, ripemd160, sha256} import fr.acinq.bitcoin.Script._ import fr.acinq.bitcoin._ -import fr.acinq.eclair.Features.{Wumbo, hasFeature} import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets} import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL @@ -35,7 +34,6 @@ import fr.acinq.eclair.{NodeParams, ShortChannelId, addressToPublicKeyScript, _} import fr.acinq.eclair.channel.ChannelVersion.USE_STATIC_REMOTEKEY_BIT import scodec.bits.ByteVector -import scala.compat.Platform import scala.concurrent.Await import scala.concurrent.duration._ import scala.util.{Failure, Success, Try} @@ -106,7 +104,7 @@ object Helpers { if (open.fundingSatoshis < nodeParams.minFundingSatoshis || open.fundingSatoshis > nodeParams.maxFundingSatoshis) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, nodeParams.maxFundingSatoshis) // BOLT #2: Channel funding limits - if (open.fundingSatoshis >= Channel.MAX_FUNDING && !hasFeature(nodeParams.features, Wumbo)) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING) + if (open.fundingSatoshis >= Channel.MAX_FUNDING && !nodeParams.features.hasFeature(Features.Wumbo)) throw InvalidFundingAmount(open.temporaryChannelId, open.fundingSatoshis, nodeParams.minFundingSatoshis, Channel.MAX_FUNDING) // BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000. if (open.pushMsat > open.fundingSatoshis) throw InvalidPushAmount(open.temporaryChannelId, open.pushMsat, open.fundingSatoshis.toMilliSatoshi) @@ -220,7 +218,7 @@ object Helpers { } def makeAnnouncementSignatures(nodeParams: NodeParams, commitments: Commitments, shortChannelId: ShortChannelId): AnnouncementSignatures = { - val features = ByteVector.empty // empty features for now + val features = Features.empty // empty features for now val fundingPubKey = nodeParams.keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath) val (localNodeSig, localBitcoinSig) = nodeParams.keyManager.signChannelAnnouncement(fundingPubKey.path, nodeParams.chainHash, shortChannelId, commitments.remoteParams.nodeId, commitments.remoteParams.fundingPubKey, features) AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala index 9745d319c0..597beb0c0f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/KeyManager.scala @@ -22,10 +22,9 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.DeterministicWallet.ExtendedPublicKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, DeterministicWallet, Protocol} -import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.{Features, ShortChannelId} import fr.acinq.eclair.channel.{ChannelVersion, LocalParams} import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo -import scodec.bits.ByteVector trait KeyManager { def nodeKey: DeterministicWallet.ExtendedPrivateKey @@ -106,7 +105,7 @@ trait KeyManager { * @return a (nodeSig, bitcoinSig) pair. nodeSig is the signature of the channel announcement with our node's * private key, bitcoinSig is the signature of the channel announcement with our funding private key */ - def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) + def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) } object KeyManager { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala index 3373c74270..9f514905b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/LocalKeyManager.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Deterministi import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.TransactionWithInputInfo -import fr.acinq.eclair.{ShortChannelId, secureRandom} +import fr.acinq.eclair.{Features, ShortChannelId, secureRandom} import scodec.bits.ByteVector object LocalKeyManager { @@ -143,7 +143,7 @@ class LocalKeyManager(seed: ByteVector, chainHash: ByteVector32) extends KeyMana Transactions.sign(tx, currentKey) } - override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { + override def signChannelAnnouncement(fundingKeyPath: DeterministicWallet.KeyPath, chainHash: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) = { val localNodeSecret = nodeKey.privateKey val localFundingPrivKey = privateKeys.get(fundingKeyPath).privateKey Announcements.signChannelAnnouncement(chainHash, shortChannelId, localNodeSecret, remoteNodeId, localFundingPrivKey, remoteFundingKey, features) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index b3d5dca88b..271d0b5321 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -268,7 +268,10 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co } } - override def aroundPostStop(): Unit = connection ! Tcp.Close // attempts to gracefully close the connection when dying + onTermination { + case _: StopEvent => + connection ! Tcp.Close // attempts to gracefully close the connection when dying + } initialize() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala index 4b10b5bd1c..48a88c2c23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/BackupHandler.scala @@ -64,14 +64,14 @@ class BackupHandler private(databases: Databases, backupFile: File, backupScript // publish a notification that we have updated our backup context.system.eventStream.publish(BackupCompleted) - log.info(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms") + log.debug(s"database backup triggered by channelId=${persisted.channelId} took ${end - start}ms") backupScript_opt.foreach(backupScript => { Try { // run the script in the current thread and wait until it terminates Process(backupScript).! } match { - case Success(exitCode) => log.info(s"backup notify script $backupScript returned $exitCode") + case Success(exitCode) => log.debug(s"backup notify script $backupScript returned $exitCode") case Failure(cause) => log.warning(s"cannot start backup notify script $backupScript: $cause") } }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala index c57bc8ab6b..3ee89e8962 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala @@ -25,8 +25,6 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} -import scala.compat.Platform - trait PaymentsDb extends IncomingPaymentsDb with OutgoingPaymentsDb with PaymentsOverviewDb with Closeable trait IncomingPaymentsDb { @@ -209,7 +207,7 @@ object FailureType extends Enumeration { object FailureSummary { def apply(f: PaymentFailure): FailureSummary = f match { - case LocalFailure(t) => FailureSummary(FailureType.LOCAL, t.getMessage, Nil) + case LocalFailure(route, t) => FailureSummary(FailureType.LOCAL, t.getMessage, route.map(h => HopSummary(h)).toList) case RemoteFailure(route, e) => FailureSummary(FailureType.REMOTE, e.failureMessage.message, route.map(h => HopSummary(h)).toList) case UnreadableRemoteFailure(route) => FailureSummary(FailureType.UNREADABLE_REMOTE, "could not decrypt failure onion", route.map(h => HopSummary(h)).toList) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 52e074afe4..bfd3ff21e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -24,7 +24,6 @@ import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.Logs.LogCategory -import fr.acinq.eclair.io.Client.ConnectionFailed import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected, Socks5Error} import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams} import fr.acinq.eclair.{Logs, NodeParams} @@ -60,7 +59,7 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re case Tcp.CommandFailed(c: Tcp.Connect) => val peerOrProxyAddress = c.remoteAddress log.info(s"connection failed to ${str(peerOrProxyAddress)}") - origin_opt.foreach(_ ! Status.Failure(ConnectionFailed(remoteAddress))) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) context stop self case Tcp.Connected(peerOrProxyAddress, _) => @@ -75,24 +74,28 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re context become { case Tcp.CommandFailed(_: Socks5Connect) => log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") - origin_opt.foreach(_ ! Status.Failure(ConnectionFailed(remoteAddress))) + origin_opt.foreach(_ ! PeerConnection.ConnectionResult.ConnectionFailed(remoteAddress)) context stop self case Socks5Connected(_) => log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") - auth(proxy) - context become connected(proxy) + context unwatch proxy + val peerConnection = auth(proxy) + context watch peerConnection + context become connected(peerConnection) + case Terminated(actor) if actor == proxy => + context stop self } case None => val peerAddress = peerOrProxyAddress log.info(s"connected to ${str(peerAddress)}") - auth(connection) - context watch connection - context become connected(connection) + val peerConnection = auth(connection) + context watch peerConnection + context become connected(peerConnection) } } - def connected(connection: ActorRef): Receive = { - case Terminated(actor) if actor == connection => + def connected(peerConnection: ActorRef): Receive = { + case Terminated(actor) if actor == peerConnection => context stop self } @@ -100,7 +103,7 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re log.warning(s"unhandled message=$message") } - // we should not restart a failing socks client + // we should not restart a failing socks client or transport handler override val supervisorStrategy = OneForOneStrategy(loggingEnabled = false) { case t => Logs.withMdc(log)(Logs.mdc(remoteNodeId_opt = Some(remoteNodeId))) { @@ -116,13 +119,14 @@ class Client(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, re private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}" - def auth(connection: ActorRef) = { + def auth(connection: ActorRef): ActorRef = { val peerConnection = context.actorOf(PeerConnection.props( nodeParams = nodeParams, switchboard = switchboard, router = router )) peerConnection ! PeerConnection.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) + peerConnection } } @@ -130,6 +134,4 @@ object Client { def props(nodeParams: NodeParams, switchboard: ActorRef, router: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, switchboard, router, address, remoteNodeId, origin_opt)) - case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index d8ea5b4fea..add29bf534 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -68,22 +68,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe reconnectionTask forward p stay - case Event(PeerConnection.ConnectionReady(peerConnection, remoteNodeId1, address, outgoing, localInit, remoteInit), d: DisconnectedData) => - require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1") - log.debug("got authenticated connection to address {}:{}", address.getHostString, address.getPort) - - context watch peerConnection - - if (outgoing) { - // we store the node address upon successful outgoing connection, so we can reconnect later - // any previous address is overwritten - NodeAddress.fromParts(address.getHostString, address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress)) - } - - // let's bring existing/requested channels online - d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(peerConnection, localInit, remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) - - goto(CONNECTED) using ConnectedData(address, peerConnection, localInit, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) }) + case Event(connectionReady: PeerConnection.ConnectionReady, d: DisconnectedData) => + gotoConnected(connectionReady, d.channels.map { case (k: ChannelId, v) => (k, v) }) case Event(Terminated(actor), d: DisconnectedData) if d.channels.exists(_._2 == actor) => val h = d.channels.filter(_._2 == actor).keys @@ -102,7 +88,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe when(CONNECTED) { dropStaleMessages { case Event(_: Peer.Connect, _) => - sender ! "already connected" + sender ! PeerConnection.ConnectionResult.AlreadyConnected stay case Event(Channel.OutgoingMessage(msg, peerConnection), d: ConnectedData) if peerConnection == d.peerConnection => // this is an outgoing message, but we need to make sure that this is for the current active connection @@ -125,10 +111,10 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe stay case Event(c: Peer.OpenChannel, d: ConnectedData) => - if (c.fundingSatoshis >= Channel.MAX_FUNDING && !Features.hasFeature(nodeParams.features, Wumbo)) { + if (c.fundingSatoshis >= Channel.MAX_FUNDING && !nodeParams.features.hasFeature(Wumbo)) { sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)")) stay - } else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !Features.hasFeature(d.remoteInit.features, Wumbo)) { + } else if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.remoteInit.features.hasFeature(Wumbo)) { sender ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo")) stay } else if (c.fundingSatoshis > nodeParams.maxFundingSatoshis) { @@ -220,8 +206,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe context unwatch d.peerConnection d.peerConnection ! PoisonPill d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) - self forward connectionReady // we preserve the origin - goto(DISCONNECTED) using DisconnectedData(d.channels.collect { case (k: FinalChannelId, v) => (k, v) }) + gotoConnected(connectionReady, d.channels) case Event(unhandledMsg: LightningMessage, _) => log.warning("ignoring message {}", unhandledMsg) @@ -251,9 +236,11 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe } onTransition { - case _ -> CONNECTED => + case DISCONNECTED -> CONNECTED => Metrics.PeersConnected.withoutTags().increment() context.system.eventStream.publish(PeerConnected(self, remoteNodeId)) + case CONNECTED -> CONNECTED => // connection switch + context.system.eventStream.publish(PeerConnected(self, remoteNodeId)) case CONNECTED -> DISCONNECTED => Metrics.PeersConnected.withoutTags().decrement() context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId)) @@ -266,6 +253,24 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, watcher: ActorRe context.system.eventStream.publish(PeerDisconnected(self, remoteNodeId)) } + def gotoConnected(connectionReady: PeerConnection.ConnectionReady, channels: Map[ChannelId, ActorRef]): State = { + require(remoteNodeId == connectionReady.remoteNodeId, s"invalid nodeid: $remoteNodeId != ${connectionReady.remoteNodeId}") + log.debug("got authenticated connection to address {}:{}", connectionReady.address.getHostString, connectionReady.address.getPort) + + context watch connectionReady.peerConnection + + if (connectionReady.outgoing) { + // we store the node address upon successful outgoing connection, so we can reconnect later + // any previous address is overwritten + NodeAddress.fromParts(connectionReady.address.getHostString, connectionReady.address.getPort).map(nodeAddress => nodeParams.db.peers.addOrUpdatePeer(remoteNodeId, nodeAddress)) + } + + // let's bring existing/requested channels online + channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) + + goto(CONNECTED) using ConnectedData(connectionReady.address, connectionReady.peerConnection, connectionReady.localInit, connectionReady.remoteInit, channels) + } + /** * We need to ignore [[LightningMessage]] not sent by the current [[PeerConnection]]. This may happen if we switch * between connections. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 0199d71707..66e30e1440 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} import scodec.Attempt -import scodec.bits.{BitVector, ByteVector} +import scodec.bits.ByteVector import scala.concurrent.duration._ import scala.util.Random @@ -89,8 +89,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto switchboard ! Authenticated(self, remoteNodeId) goto(BEFORE_INIT) using BeforeInitData(remoteNodeId, d.pendingAuth, d.transport) - case Event(AuthTimeout, _) => + case Event(AuthTimeout, d: AuthenticatingData) => log.warning(s"authentication timed out after ${nodeParams.authTimeout}") + d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("authentication timed out")) stop(FSM.Normal) } @@ -100,23 +101,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment() val localFeatures = nodeParams.overrideFeatures.get(d.remoteNodeId) match { case Some(f) => f - case None => - // Eclair-mobile thinks feature bit 15 (payment_secret) is gossip_queries_ex which creates issues, so we mask - // off basic_mpp and payment_secret. As long as they're provided in the invoice it's not an issue. - // We use a long enough mask to account for future features. - // TODO: remove that once eclair-mobile is patched. - val tweakedFeatures = BitVector.bits(nodeParams.features.bits.reverse.toIndexedSeq.zipWithIndex.map { - // we disable those bits if they are set... - case (true, 14) => false - case (true, 15) => false - case (true, 16) => false - case (true, 17) => false - // ... and leave the others untouched - case (value, _) => value - }).reverse.bytes.dropWhile(_ == 0) - tweakedFeatures + case None => nodeParams.features.maskFeaturesForEclairMobile() } - log.info(s"using features=${localFeatures.toBin}") + log.info(s"using features=$localFeatures") val localInit = wire.Init(localFeatures, TlvStream(InitTlv.Networks(nodeParams.chainHash :: Nil))) d.transport ! localInit setTimer(INIT_TIMER, InitTimeout, nodeParams.initTimeout) @@ -129,27 +116,27 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto cancelTimer(INIT_TIMER) d.transport ! TransportHandler.ReadAck(remoteInit) - log.info(s"peer is using features=${remoteInit.features.toBin}, networks=${remoteInit.networks.mkString(",")}") + log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}") if (remoteInit.networks.nonEmpty && !remoteInit.networks.contains(d.nodeParams.chainHash)) { log.warning(s"incompatible networks (${remoteInit.networks}), disconnecting") - d.pendingAuth.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible networks"))) + d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("incompatible networks")) d.transport ! PoisonPill stay } else if (!Features.areSupported(remoteInit.features)) { log.warning("incompatible features, disconnecting") - d.pendingAuth.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features"))) + d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("incompatible features")) d.transport ! PoisonPill stay } else { Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initialized).increment() d.peer ! ConnectionReady(self, d.remoteNodeId, d.pendingAuth.address, d.pendingAuth.outgoing, d.localInit, remoteInit) - d.pendingAuth.origin_opt.foreach(origin => origin ! "connected") + d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.Connected) - def localHasFeature(f: Feature): Boolean = Features.hasFeature(d.localInit.features, f) + def localHasFeature(f: Feature): Boolean = d.localInit.features.hasFeature(f) - def remoteHasFeature(f: Feature): Boolean = Features.hasFeature(remoteInit.features, f) + def remoteHasFeature(f: Feature): Boolean = remoteInit.features.hasFeature(f) val canUseChannelRangeQueries = localHasFeature(Features.ChannelRangeQueries) && remoteHasFeature(Features.ChannelRangeQueries) val canUseChannelRangeQueriesEx = localHasFeature(Features.ChannelRangeQueriesExtended) && remoteHasFeature(Features.ChannelRangeQueriesExtended) @@ -177,8 +164,9 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto goto(CONNECTED) using ConnectedData(d.nodeParams, d.remoteNodeId, d.transport, d.peer, d.localInit, remoteInit, rebroadcastDelay) } - case Event(InitTimeout, _) => + case Event(InitTimeout, d: InitializingData) => log.warning(s"initialization timed out after ${nodeParams.initTimeout}") + d.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("initialization timed out")) stop(FSM.Normal) } } @@ -382,6 +370,12 @@ class PeerConnection(nodeParams: NodeParams, switchboard: ActorRef, router: Acto Logs.withMdc(diagLog)(Logs.mdc(category_opt = Some(Logs.LogCategory.CONNECTION))) { log.info("transport died, stopping") } + d match { + case a: AuthenticatingData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.AuthenticationFailed("connection aborted while authenticating")) + case a: BeforeInitData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("connection aborted while initializing")) + case a: InitializingData => a.pendingAuth.origin_opt.foreach(_ ! ConnectionResult.InitializationFailed("connection aborted while initializing")) + case _ => () + } stop(FSM.Normal) case Event(_: GossipDecision.Accepted, _) => stay // for now we don't do anything with those events @@ -500,6 +494,19 @@ object PeerConnection { case class InitializeConnection(peer: ActorRef) case class ConnectionReady(peerConnection: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, localInit: wire.Init, remoteInit: wire.Init) + sealed trait ConnectionResult + object ConnectionResult { + sealed trait Success extends ConnectionResult + sealed trait Failure extends ConnectionResult + + case object NoAddressFound extends ConnectionResult.Failure { override def toString: String = "no address found" } + case class ConnectionFailed(address: InetSocketAddress) extends ConnectionResult.Failure { override def toString: String = s"connection failed to $address" } + case class AuthenticationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } + case class InitializationFailed(reason: String) extends ConnectionResult.Failure { override def toString: String = reason } + case object AlreadyConnected extends ConnectionResult.Failure { override def toString: String = "already connected" } + case object Connected extends ConnectionResult.Success { override def toString: String = "connected" } + } + case class DelayedRebroadcast(rebroadcast: Rebroadcast) case class Behavior(fundingTxAlreadySpentCount: Int = 0, ignoreNetworkAnnouncement: Boolean = false) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index 3aa5795a94..81e09644fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -50,7 +50,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends startWith(IDLE, IdleData(Nothing)) when(CONNECTING) { - case Event(Status.Failure(_: Client.ConnectionFailed), d: ConnectingData) => + case Event(_: PeerConnection.ConnectionResult.ConnectionFailed, d: ConnectingData) => log.info(s"connection failed, next reconnection in ${d.nextReconnectionDelay.toSeconds} seconds") setReconnectTimer(d.nextReconnectionDelay) goto(WAITING) using WaitingData(nextReconnectionDelay(d.nextReconnectionDelay, nodeParams.maxReconnectInterval)) @@ -121,9 +121,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends } whenUnhandled { - case Event("connected", _) => stay - - case Event(Status.Failure(_: Client.ConnectionFailed), _) => stay + case Event(_: PeerConnection.ConnectionResult, _) => stay case Event(TickReconnect, _) => stay @@ -135,7 +133,7 @@ class ReconnectionTask(nodeParams: NodeParams, remoteNodeId: PublicKey) extends .map(hostAndPort2InetSocketAddress) .orElse(getPeerAddressFromDb(nodeParams.db.peers, nodeParams.db.network, remoteNodeId)) match { case Some(address) => connect(address, origin = sender) - case None => sender ! "no address found" + case None => sender ! PeerConnection.ConnectionResult.NoAddressFound } stay } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala index 5f4852b1a6..ad2a86437e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Monitoring.scala @@ -92,7 +92,7 @@ object Monitoring { } def apply(pf: PaymentFailure): String = pf match { - case LocalFailure(t) => t.getClass.getSimpleName + case LocalFailure(_, t) => t.getClass.getSimpleName case RemoteFailure(_, e) => e.failureMessage.getClass.getSimpleName case UnreadableRemoteFailure(_) => "UnreadableRemoteFailure" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index 1fea6c6c40..fe3a433f1d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -22,9 +22,9 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.router.Router.Hop - -import scala.compat.Platform +import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop} +import fr.acinq.eclair.wire.Node /** * Created by PM on 01/02/2017. @@ -112,10 +112,12 @@ object PaymentReceived { case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = System.currentTimeMillis) extends PaymentEvent -sealed trait PaymentFailure +sealed trait PaymentFailure { + def route: Seq[Hop] +} /** A failure happened locally, preventing the payment from being sent (e.g. no route found). */ -case class LocalFailure(t: Throwable) extends PaymentFailure +case class LocalFailure(route: Seq[Hop], t: Throwable) extends PaymentFailure /** A remote node failed the payment and we were able to decrypt the onion failure packet. */ case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure @@ -142,10 +144,10 @@ object PaymentFailure { */ def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = { failures.map { - case LocalFailure(AddHtlcFailed(_, _, t, _, _, _)) => LocalFailure(t) // we're interested in the error which caused the add-htlc to fail + case LocalFailure(hops, AddHtlcFailed(_, _, t, _, _, _)) => LocalFailure(hops, t) // we're interested in the error which caused the add-htlc to fail case other => other } match { - case previousFailures :+ LocalFailure(RouteNotFound) if previousFailures.nonEmpty => previousFailures + case previousFailures :+ LocalFailure(_, RouteNotFound) if previousFailures.nonEmpty => previousFailures case other => other } } @@ -159,4 +161,44 @@ object PaymentFailure { .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update } .isDefined + /** Update the set of nodes and channels to ignore in retries depending on the failure we received. */ + def updateIgnored(failure: PaymentFailure, ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]): (Set[PublicKey], Set[ChannelDesc]) = failure match { + case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) if nodeId == hops.last.nextNodeId => + // The failure came from the final recipient: the payment should be aborted without penalizing anyone in the route. + (ignoreNodes, ignoreChannels) + case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, _: Node)) => + (ignoreNodes + nodeId, ignoreChannels) + case RemoteFailure(_, Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => + if (Announcements.checkSig(failureMessage.update, nodeId)) { + // We were using an outdated channel update, we should retry with the new one and nobody should be penalized. + (ignoreNodes, ignoreChannels) + } else { + // This node is fishy, it gave us a bad signature, so let's filter it out. + (ignoreNodes + nodeId, ignoreChannels) + } + case RemoteFailure(hops, Sphinx.DecryptedFailurePacket(nodeId, _)) => + // Let's ignore the channel outgoing from nodeId. + hops.collectFirst { + case hop: ChannelHop if hop.nodeId == nodeId => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + } match { + case Some(faultyChannel) => (ignoreNodes, ignoreChannels + faultyChannel) + case None => (ignoreNodes, ignoreChannels) + } + case UnreadableRemoteFailure(hops) => + // We don't know which node is sending garbage, let's blacklist all nodes except the one we are directly connected to and the final recipient. + val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) + (ignoreNodes ++ blacklist, ignoreChannels) + case LocalFailure(hops, _) => hops.headOption match { + case Some(hop: ChannelHop) => + val faultyChannel = ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId) + (ignoreNodes, ignoreChannels + faultyChannel) + case _ => (ignoreNodes, ignoreChannels) + } + } + + /** Update the set of nodes and channels to ignore in retries depending on the failures we received. */ + def updateIgnored(failures: Seq[PaymentFailure], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]): (Set[PublicKey], Set[ChannelDesc]) = { + failures.foldLeft((ignoreNodes, ignoreChannels)) { case ((nodes, channels), failure) => updateIgnored(failure, nodes, channels) } + } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index ee6e2e1ace..314cfe5630 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.payment import akka.event.LoggingAdapter import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.Features.VariableLengthOnion import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} @@ -54,11 +55,11 @@ object IncomingPacket { case class DecodedOnionPacket[T <: Onion.PacketType](payload: T, next: OnionRoutingPacket) - private def decryptOnion[T <: Onion.PacketType : ClassTag](add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] = + private def decryptOnion[T <: Onion.PacketType : ClassTag](add: UpdateAddHtlc, privateKey: PrivateKey, features: Features)(packet: OnionRoutingPacket, packetType: Sphinx.OnionRoutingPacket[T])(implicit log: LoggingAdapter): Either[FailureMessage, DecodedOnionPacket[T]] = packetType.peel(privateKey, add.paymentHash, packet) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => OnionCodecs.perHopPayloadCodecByPacketType(packetType, p.isLastPacket).decode(payload.bits) match { - case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !Features.hasFeature(features, Features.VariableLengthOnion) => Left(InvalidRealm) + case Attempt.Successful(DecodeResult(_: Onion.TlvFormat, _)) if !features.hasFeature(VariableLengthOnion) => Left(InvalidRealm) case Attempt.Successful(DecodeResult(perHopPayload: T, remainder)) => if (remainder.nonEmpty) { log.warning(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") @@ -84,7 +85,7 @@ object IncomingPacket { * @param features this node's supported features * @return whether the payment is to be relayed or if our node is the final recipient (or an error). */ - def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPacket] = { + def decrypt(add: UpdateAddHtlc, privateKey: PrivateKey, features: Features)(implicit log: LoggingAdapter): Either[FailureMessage, IncomingPacket] = { decryptOnion(add, privateKey, features)(add.onionRoutingPacket, Sphinx.PaymentPacket) match { case Left(failure) => Left(failure) // NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala index 53c2d3895a..f24c98540f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentRequest.scala @@ -18,14 +18,12 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto} -import fr.acinq.eclair.Features.{PaymentSecret => PaymentSecretF, _} import fr.acinq.eclair.payment.PaymentRequest._ -import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32} +import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32} import scodec.bits.{BitVector, ByteOrdering, ByteVector} import scodec.codecs.{list, ubyte} import scodec.{Codec, Err} -import scala.compat.Platform import scala.concurrent.duration._ import scala.util.Try @@ -45,7 +43,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam amount.foreach(a => require(a > 0.msat, s"amount is not valid")) require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag") require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag") - private val featuresErr = validateFeatureGraph(features.bitmask) + private val featuresErr = Features.validateFeatureGraph(features.features) require(featuresErr.isEmpty, featuresErr.map(_.message)) if (features.allowPaymentSecret) { require(tags.collect { case _: PaymentRequest.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set") @@ -86,7 +84,7 @@ case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestam case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta } - lazy val features: Features = tags.collectFirst { case f: Features => f }.getOrElse(Features(BitVector.empty)) + lazy val features: PaymentRequestFeatures = tags.collectFirst { case f: PaymentRequestFeatures => f }.getOrElse(PaymentRequestFeatures(BitVector.empty)) def isExpired: Boolean = expiry match { case Some(expiryTime) => timestamp + expiryTime <= System.currentTimeMillis.milliseconds.toSeconds @@ -129,7 +127,7 @@ object PaymentRequest { def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey, description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L, - features: Option[Features] = Some(Features(VariableLengthOnion.optional, PaymentSecretF.optional))): PaymentRequest = { + features: Option[PaymentRequestFeatures] = Some(PaymentRequestFeatures(Features.VariableLengthOnion.optional, Features.PaymentSecret.optional))): PaymentRequest = { val prefix = prefixes(chainHash) val tags = { @@ -331,25 +329,21 @@ object PaymentRequest { /** * Features supported or required for receiving this payment. */ - case class Features(bitmask: BitVector) extends TaggedField { - lazy val supported: Boolean = areSupported(bitmask) - lazy val allowMultiPart: Boolean = hasFeature(bitmask, BasicMultiPartPayment) - lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF) - lazy val requirePaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, Some(FeatureSupport.Mandatory)) - lazy val allowTrampoline: Boolean = hasFeature(bitmask, TrampolinePayment) + case class PaymentRequestFeatures(bitmask: BitVector) extends TaggedField { + lazy val features: Features = Features(bitmask) + lazy val supported: Boolean = Features.areSupported(features) + lazy val allowMultiPart: Boolean = features.hasFeature(Features.BasicMultiPartPayment) + lazy val allowPaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret) + lazy val requirePaymentSecret: Boolean = features.hasFeature(Features.PaymentSecret, Some(FeatureSupport.Mandatory)) + lazy val allowTrampoline: Boolean = features.hasFeature(Features.TrampolinePayment) - override def toString: String = s"Features(${bitmask.toBin})" + def toByteVector: ByteVector = features.toByteVector - // When converting from BitVector to ByteVector, scodec pads right instead of left so we have to do this ourselves. - // We also want to enforce a minimal encoding of the feature bytes. - def toByteVector: ByteVector = { - val pad = if (bitmask.length % 8 == 0) 0 else 8 - bitmask.length % 8 - bitmask.padLeft(bitmask.length + pad).bytes.dropWhile(_ == 0) - } + override def toString: String = s"Features(${bitmask.toBin})" } - object Features { - def apply(features: Int*): Features = Features(long2bits(features.foldLeft(0L) { + object PaymentRequestFeatures { + def apply(features: Int*): PaymentRequestFeatures = PaymentRequestFeatures(long2bits(features.foldLeft(0L) { case (current, feature) => current + (1L << feature) })) } @@ -395,7 +389,7 @@ object PaymentRequest { .typecase(2, dataCodec(bits).as[UnknownTag2]) .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo]) .typecase(4, dataCodec(bits).as[UnknownTag4]) - .typecase(5, dataCodec(bits).as[Features]) + .typecase(5, dataCodec(bits).as[PaymentRequestFeatures]) .typecase(6, dataCodec(bits).as[Expiry]) .typecase(7, dataCodec(bits).as[UnknownTag7]) .typecase(8, dataCodec(bits).as[UnknownTag8]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala index a6e46d43b0..6a537c4ec2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala @@ -62,10 +62,10 @@ class MultiPartHandler(nodeParams: NodeParams, db: IncomingPaymentsDb, commandBu // Once we're confident most of the network has upgraded, we should switch to mandatory payment secrets. val features = { val f1 = Seq(Features.PaymentSecret.optional, Features.VariableLengthOnion.optional) - val allowMultiPart = Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) + val allowMultiPart = nodeParams.features.hasFeature(Features.BasicMultiPartPayment) val f2 = if (allowMultiPart) Seq(Features.BasicMultiPartPayment.optional) else Nil val f3 = if (nodeParams.enableTrampolinePayment) Seq(Features.TrampolinePayment.optional) else Nil - Some(PaymentRequest.Features(f1 ++ f2 ++ f3: _*)) + Some(PaymentRequest.PaymentRequestFeatures(f1 ++ f2 ++ f3: _*)) } val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = features) log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala index ff3da47327..78ff4938d2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelayer.scala @@ -208,7 +208,7 @@ class NodeRelayer(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, c object NodeRelayer { - def props(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(classOf[NodeRelayer], nodeParams, relayer, router, commandBuffer, register) + def props(nodeParams: NodeParams, relayer: ActorRef, router: ActorRef, commandBuffer: ActorRef, register: ActorRef) = Props(new NodeRelayer(nodeParams, relayer, router, commandBuffer, register)) /** * We start by aggregating an incoming HTLC set. Once we received the whole set, we will compute a route to the next @@ -260,13 +260,13 @@ object NodeRelayer { */ private def translateError(failures: Seq[PaymentFailure], outgoingNodeId: PublicKey): Option[FailureMessage] = { def tooManyRouteNotFound(failures: Seq[PaymentFailure]): Boolean = { - val routeNotFoundCount = failures.count(_ == LocalFailure(RouteNotFound)) + val routeNotFoundCount = failures.collect { case f@LocalFailure(_, RouteNotFound) => f }.length routeNotFoundCount > failures.length / 2 } failures match { case Nil => None - case LocalFailure(PaymentError.BalanceTooLow) :: Nil => Some(TemporaryNodeFailure) // we don't have enough outgoing liquidity at the moment + case LocalFailure(_, PaymentError.BalanceTooLow) :: Nil => Some(TemporaryNodeFailure) // we don't have enough outgoing liquidity at the moment case _ if tooManyRouteNotFound(failures) => Some(TrampolineFeeInsufficient) // if we couldn't find routes, it's likely that the fee/cltv was insufficient case _ => // Otherwise, we try to find a downstream error that we could decrypt. 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 e6123a6868..545b49dbc8 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 @@ -29,7 +29,7 @@ import fr.acinq.eclair.payment.{IncomingPacket, PaymentFailed, PaymentSent} import fr.acinq.eclair.transactions.DirectedHtlc.outgoing import fr.acinq.eclair.transactions.OutgoingHtlc import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc} -import fr.acinq.eclair.{LongToBtcAmount, NodeParams} +import fr.acinq.eclair.{Features, LongToBtcAmount, NodeParams} import scodec.bits.ByteVector import scala.compat.Platform @@ -106,6 +106,8 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, commandBuffer: ActorRef, in Metrics.PendingNotRelayed.update(notRelayed1.size) context become main(brokenHtlcs.copy(notRelayed = notRelayed1)) + case _: ChannelStateChanged => // ignore other channel state changes + case ff: Relayer.ForwardFulfill => log.info("htlc fulfilled downstream: ({},{})", ff.htlc.channelId, ff.htlc.id) handleDownstreamFulfill(brokenHtlcs, ff.to, ff.htlc, ff.paymentPreimage) @@ -292,7 +294,7 @@ object PostRestartHtlcCleaner { * Outgoing HTLC sets that are still pending may either succeed or fail: we need to watch them to properly forward the * result upstream to preserve channels. */ - private def checkBrokenHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: ByteVector)(implicit log: LoggingAdapter): BrokenHtlcs = { + private def checkBrokenHtlcs(channels: Seq[HasCommitments], paymentsDb: IncomingPaymentsDb, privateKey: PrivateKey, features: Features)(implicit log: LoggingAdapter): BrokenHtlcs = { // We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed). // They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when // we subsequently sign it. That's why we need to look in *their* commitment with direction=OUT. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 1d972eb338..6e2434a2c6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannel, OutgoingChannels} import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentConfig import fr.acinq.eclair.payment.send.PaymentLifecycle.SendPayment -import fr.acinq.eclair.router.Router.{ChannelHop, GetNetworkStats, GetNetworkStatsResponse, RouteParams, TickComputeNetworkStats} +import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{CltvExpiry, FSMDiagnosticActorLogging, Logs, LongToBtcAmount, MilliSatoshi, NodeParams, ShortChannelId, ToMilliSatoshiConversion} @@ -41,7 +41,6 @@ import kamon.context.Context import scodec.bits.ByteVector import scala.annotation.tailrec -import scala.compat.Platform import scala.util.Random /** @@ -96,8 +95,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val (remaining, payments) = splitPayment(nodeParams, d.request.totalAmount, channels, d.networkStats, d.request, randomize = false) if (remaining > 0.msat) { log.warning(s"cannot send ${d.request.totalAmount} with our current balance") - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(PaymentError.BalanceTooLow))) - goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, LocalFailure(PaymentError.BalanceTooLow) :: Nil, Set.empty) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, PaymentError.BalanceTooLow))) + goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, LocalFailure(Nil, PaymentError.BalanceTooLow) :: Nil, Set.empty) } else { val pending = setFees(d.request.routeParams, payments, payments.size) Kamon.runWithContextEntry(parentPaymentIdKey, cfg.parentId) { @@ -155,8 +154,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, val (remaining, payments) = splitPayment(nodeParams, d.toSend, filteredChannels, d.networkStats, d.request, randomize = true) // we randomize channel selection when we retry if (remaining > 0.msat) { log.warning(s"cannot send ${d.toSend} with our current balance") - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(PaymentError.BalanceTooLow))) - goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, d.failures :+ LocalFailure(PaymentError.BalanceTooLow), d.pending.keySet) + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, PaymentError.BalanceTooLow))) + goto(PAYMENT_ABORTED) using PaymentAborted(d.sender, d.request, d.failures :+ LocalFailure(Nil, PaymentError.BalanceTooLow), d.pending.keySet) } else { val pending = setFees(d.request.routeParams, payments, payments.size + d.pending.size) pending.foreach { case (childId, payment) => spawnChildPaymentFsm(childId) ! payment } @@ -270,7 +269,7 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, if (isFromFinalRecipient) { Some(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id)) } else if (d.remainingAttempts == 0) { - val failure = LocalFailure(PaymentError.RetryExhausted) + val failure = LocalFailure(Nil, PaymentError.RetryExhausted) Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(failure)) Some(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id)) } else { @@ -390,7 +389,7 @@ object MultiPartPaymentLifecycle { /** If the payment failed immediately with a RouteNotFound, the channel we selected should be ignored in retries. */ private def shouldBlacklistChannel(pf: PaymentFailed): Boolean = pf.failures match { - case LocalFailure(RouteNotFound) :: Nil => true + case LocalFailure(_, RouteNotFound) :: Nil => true case _ => false } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala index 6ed1583821..d660dfde73 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.payment.send -import scodec.bits.BitVector +import fr.acinq.eclair.Features sealed trait PaymentError extends Throwable @@ -25,7 +25,7 @@ object PaymentError { // @formatter:off sealed trait InvalidInvoice extends PaymentError /** The invoice contains a feature we don't support. */ - case class UnsupportedFeatures(features: BitVector) extends InvalidInvoice + case class UnsupportedFeatures(features: Features) extends InvalidInvoice /** The invoice is missing a payment secret. */ case object PaymentSecretMissing extends InvalidInvoice // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index 3e93b4e94c..b30555ed94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -21,6 +21,7 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.Features.BasicMultiPartPayment import fr.acinq.eclair.channel.{Channel, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentRequest.ExtraHop @@ -28,10 +29,10 @@ import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.PaymentError._ import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} -import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop, RouteParams} +import fr.acinq.eclair.router.Router.{Hop, NodeHop, Route, RouteParams} import fr.acinq.eclair.wire.Onion.FinalLegacyPayload import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, NodeParams, randomBytes32} /** * Created by PM on 29/08/2016. @@ -50,13 +51,13 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight) r.paymentRequest match { case Some(invoice) if !invoice.features.supported => - sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(UnsupportedFeatures(invoice.features.bitmask)) :: Nil) - case Some(invoice) if invoice.features.allowMultiPart && Features.hasFeature(nodeParams.features, Features.BasicMultiPartPayment) => + sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, UnsupportedFeatures(invoice.features.features)) :: Nil) + case Some(invoice) if invoice.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) => invoice.paymentSecret match { case Some(paymentSecret) => spawnMultiPartPaymentFsm(paymentCfg) forward SendMultiPartPayment(paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs) case None => - sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(PaymentSecretMissing) :: Nil) + sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, PaymentSecretMissing) :: Nil) } case _ => val paymentSecret = r.paymentRequest.flatMap(_.paymentSecret) @@ -69,9 +70,9 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR sender ! paymentId r.trampolineAttempts match { case Nil => - sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineFeesMissing) :: Nil) + sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineFeesMissing) :: Nil) case _ if !r.paymentRequest.features.allowTrampoline && r.paymentRequest.amount.isEmpty => - sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineLegacyAmountLessInvoice) :: Nil) + sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineLegacyAmountLessInvoice) :: Nil) case (trampolineFees, trampolineExpiryDelta) :: remainingAttempts => log.info(s"sending trampoline payment with trampoline fees=$trampolineFees and expiry delta=$trampolineExpiryDelta") sendTrampolinePayment(paymentId, r, trampolineFees, trampolineExpiryDelta) @@ -121,7 +122,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR case None => payFsm forward SendPaymentToRoute(r.route, FinalLegacyPayload(r.recipientAmount, finalExpiry), r.paymentRequest.routingInfo) } case _ => - sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(TrampolineMultiNodeNotSupported) :: Nil) + sender ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(Nil, TrampolineMultiNodeNotSupported) :: Nil) } } @@ -160,7 +161,7 @@ class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, relayer: ActorR object PaymentInitiator { - def props(nodeParams: NodeParams, router: ActorRef, relayer: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], nodeParams, router, relayer, register) + def props(nodeParams: NodeParams, router: ActorRef, relayer: ActorRef, register: ActorRef) = Props(new PaymentInitiator(nodeParams, router, relayer, register)) case class PendingPayment(sender: ActorRef, remainingAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], r: SendTrampolinePaymentRequest) @@ -310,7 +311,7 @@ object PaymentInitiator { storeInDb: Boolean, // e.g. for trampoline we don't want to store in the DB when we're relaying payments publishEvent: Boolean, additionalHops: Seq[NodeHop]) { - def fullRoute(hops: Seq[ChannelHop]): Seq[Hop] = hops ++ additionalHops + def fullRoute(route: Route): Seq[Hop] = route.hops ++ additionalHops def createPaymentSent(preimage: ByteVector32, parts: Seq[PaymentSent.PartialPayment]) = PaymentSent(parentId, paymentHash, preimage, recipientAmount, recipientNodeId, parts) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala index bcb3cf1e8c..58b4d57bde 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentLifecycle.scala @@ -40,7 +40,6 @@ import fr.acinq.eclair.wire._ import kamon.Kamon import kamon.trace.Span -import scala.compat.Platform import scala.util.{Failure, Success} /** @@ -82,7 +81,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A if (cfg.storeInDb) { paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) } - goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, failures = Nil) + goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, send, Nil, Set.empty, Set.empty) case Event(c: SendPayment, WaitingForRequest) => span.tag(Tags.TargetNodeId, c.targetNodeId.toString()) @@ -94,28 +93,28 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A val ignoredNodes = c.routePrefix.map(_.nodeId).toSet if (c.routePrefix.lastOption.exists(_.nextNodeId == c.targetNodeId)) { // If the sender already provided a route to the target, no need to involve the router. - self ! RouteResponse(Nil, ignoredNodes, Set.empty, allowEmpty = true) + self ! RouteResponse(Seq(Route(c.finalPayload.amount, Nil, allowEmpty = true))) } else { router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, routeParams = c.routeParams, ignoreNodes = ignoredNodes) } if (cfg.storeInDb) { paymentsDb.addOutgoingPayment(OutgoingPayment(id, cfg.parentId, cfg.externalId, paymentHash, PaymentType.Standard, c.finalPayload.amount, cfg.recipientAmount, cfg.recipientNodeId, System.currentTimeMillis, cfg.paymentRequest, OutgoingPaymentStatus.Pending)) } - goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, failures = Nil) + goto(WAITING_FOR_ROUTE) using WaitingForRoute(sender, c, Nil, ignoredNodes, Set.empty) } when(WAITING_FOR_ROUTE) { - case Event(RouteResponse(routeHops, ignoreNodes, ignoreChannels, _), WaitingForRoute(s, c, failures)) => - val hops = c.routePrefix ++ routeHops + case Event(RouteResponse(routes), WaitingForRoute(s, c, failures, ignoreNodes, ignoreChannels)) => + val hops = c.routePrefix ++ routes.head.hops log.info(s"route found: attempt=${failures.size + 1}/${c.maxAttempts} route=${hops.map(_.nextNodeId).mkString("->")} channels=${hops.map(_.lastUpdate.shortChannelId).mkString("->")}") val firstHop = hops.head val (cmd, sharedSecrets) = OutgoingPacket.buildCommand(cfg.upstream, paymentHash, hops, c.finalPayload) register ! Register.ForwardShortId(firstHop.lastUpdate.shortChannelId, cmd) - goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops) + goto(WAITING_FOR_PAYMENT_COMPLETE) using WaitingForComplete(s, c, cmd, failures, sharedSecrets, ignoreNodes, ignoreChannels, Route(c.finalPayload.amount, hops)) - case Event(Status.Failure(t), WaitingForRoute(s, _, failures)) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(t))).increment() - onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(t))) + case Event(Status.Failure(t), WaitingForRoute(s, _, failures, _, _)) => + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(Nil, t))).increment() + onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(Nil, t))) myStop() } @@ -135,7 +134,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A } stay - case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => + case Event(fail: UpdateFailHtlc, data@WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, route)) => (Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { case success@Success(e) => Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(RemoteFailure(Nil, e))).increment() @@ -147,37 +146,33 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") - onFailure(s, PaymentFailed(id, paymentHash, failures :+ RemoteFailure(cfg.fullRoute(hops), e))) + onFailure(s, PaymentFailed(id, paymentHash, failures :+ RemoteFailure(cfg.fullRoute(route), e))) myStop() case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") - RemoteFailure(cfg.fullRoute(hops), e) + RemoteFailure(cfg.fullRoute(route), e) case Failure(t) => log.warning(s"cannot parse returned error: ${t.getMessage}") - UnreadableRemoteFailure(cfg.fullRoute(hops)) + UnreadableRemoteFailure(cfg.fullRoute(route)) } log.warning(s"too many failed attempts, failing the payment") onFailure(s, PaymentFailed(id, paymentHash, failures :+ failure)) myStop() case Failure(t) => - log.warning(s"cannot parse returned error: ${t.getMessage}") - // in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node - val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) - log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") - router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) - goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(cfg.fullRoute(hops))) + log.warning(s"cannot parse returned error: ${t.getMessage}, route=${route.hops.map(_.nextNodeId)}") + val failure = UnreadableRemoteFailure(cfg.fullRoute(route)) + retry(failure, data) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") - // let's try to route around this node - router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) - goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e)) + val failure = RemoteFailure(cfg.fullRoute(route), e) + retry(failure, data) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") - if (Announcements.checkSig(failureMessage.update, nodeId)) { - getChannelUpdateForNode(nodeId, hops) match { + val ignoreNodes1 = if (Announcements.checkSig(failureMessage.update, nodeId)) { + route.getChannelUpdateForNode(nodeId) match { case Some(u) if u.shortChannelId != failureMessage.update.shortChannelId => // it is possible that nodes in the route prefer using a different channel (to the same N+1 node) than the one we requested, that's fine log.info(s"received an update for a different channel than the one we asked: requested=${u.shortChannelId} actual=${failureMessage.update.shortChannelId} update=${failureMessage.update}") @@ -185,12 +180,12 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A // node returned the exact same update we used, this can happen e.g. if the channel is imbalanced // in that case, let's temporarily exclude the channel from future routes, giving it time to recover log.info(s"received exact same update from nodeId=$nodeId, excluding the channel from futures routes") - val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId + val nextNodeId = route.hops.find(_.nodeId == nodeId).get.nextNodeId router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId)) case Some(u) if PaymentFailure.hasAlreadyFailedOnce(nodeId, failures) => // this node had already given us a new channel update and is still unhappy, it is probably messing with us, let's exclude it log.warning(s"it is the second time nodeId=$nodeId answers with a new update, excluding it: old=$u new=${failureMessage.update}") - val nextNodeId = hops.find(_.nodeId == nodeId).get.nextNodeId + val nextNodeId = route.hops.find(_.nodeId == nodeId).get.nextNodeId router ! ExcludeChannel(ChannelDesc(u.shortChannelId, nodeId, nextNodeId)) case Some(u) => log.info(s"got a new update for shortChannelId=${u.shortChannelId}: old=$u new=${failureMessage.update}") @@ -210,18 +205,18 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A }) // let's try again, router will have updated its state router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, assistedRoutes1, ignoreNodes, ignoreChannels, c.routeParams) + ignoreNodes } else { // this node is fishy, it gave us a bad sig!! let's filter it out log.warning(s"got bad signature from node=$nodeId update=${failureMessage.update}") router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) + ignoreNodes + nodeId } - goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e)) + goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(route), e), ignoreNodes1, ignoreChannels) case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") - // let's try again without the channel outgoing from nodeId - val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) - router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels ++ faultyChannel.toSet, c.routeParams) - goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(cfg.fullRoute(hops), e)) + val failure = RemoteFailure(cfg.fullRoute(route), e) + retry(failure, data) } case Event(fail: UpdateFailMalformedHtlc, _) => @@ -233,19 +228,18 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A self ! Status.Failure(new RuntimeException("first hop returned an UpdateFailMalformedHtlc message")) stay - case Event(Status.Failure(t), WaitingForComplete(s, c, _, failures, _, ignoreNodes, ignoreChannels, hops)) => - Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(t))).increment() + case Event(Status.Failure(t), data@WaitingForComplete(s, c, _, failures, _, _, _, hops)) => + Metrics.PaymentError.withTag(Tags.Failure, Tags.FailureType(LocalFailure(cfg.fullRoute(hops), t))).increment() val isFatal = failures.size + 1 >= c.maxAttempts || // retries exhausted c.routePrefix.nonEmpty || // first hop was selected by the sender and failed, it doesn't make sense to retry t.isInstanceOf[HtlcsTimedoutDownstream] // htlc timed out so retrying won't help, we need to re-compute cltvs if (isFatal) { - onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(t))) + onFailure(s, PaymentFailed(id, paymentHash, failures :+ LocalFailure(cfg.fullRoute(hops), t))) myStop() } else { log.info(s"received an error message from local, trying to use a different channel (failure=${t.getMessage})") - val faultyChannel = ChannelDesc(hops.head.lastUpdate.shortChannelId, hops.head.nodeId, hops.head.nextNodeId) - router ! RouteRequest(c.getRouteRequestStart(nodeParams), c.targetNodeId, c.finalPayload.amount, c.assistedRoutes, ignoreNodes, ignoreChannels + faultyChannel, c.routeParams) - goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ LocalFailure(t)) + val failure = LocalFailure(cfg.fullRoute(hops), t) + retry(failure, data) } } @@ -260,7 +254,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A // this means that previous state was WAITING_FOR_COMPLETE d.failures.lastOption.foreach(failure => stateSpan.foreach(span => KamonExt.failSpan(span, failure))) case d: WaitingForComplete => - stateSpanBuilder.tag("route", s"${cfg.fullRoute(d.hops).map(_.nextNodeId).mkString("->")}") + stateSpanBuilder.tag("route", s"${cfg.fullRoute(d.route).map(_.nextNodeId).mkString("->")}") case _ => () } stateSpan.foreach(_.finish()) @@ -271,13 +265,19 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A case Event(_: TransportHandler.ReadAck, _) => stay // ignored, router replies with this when we forward a channel_update } - def myStop(): State = { + private def retry(failure: PaymentFailure, data: WaitingForComplete): FSM.State[PaymentLifecycle.State, PaymentLifecycle.Data] = { + val (ignoreNodes1, ignoreChannels1) = PaymentFailure.updateIgnored(failure, data.ignoreNodes, data.ignoreChannels) + router ! RouteRequest(data.c.getRouteRequestStart(nodeParams), data.c.targetNodeId, data.c.finalPayload.amount, data.c.assistedRoutes, ignoreNodes1, ignoreChannels1, data.c.routeParams) + goto(WAITING_FOR_ROUTE) using WaitingForRoute(data.sender, data.c, data.failures :+ failure, ignoreNodes1, ignoreChannels1) + } + + private def myStop(): State = { stateSpan.foreach(_.finish()) span.finish() stop(FSM.Normal) } - def onSuccess(sender: ActorRef, result: PaymentSent): Unit = { + private def onSuccess(sender: ActorRef, result: PaymentSent): Unit = { if (cfg.storeInDb) paymentsDb.updateOutgoingPayment(result) sender ! result if (cfg.publishEvent) context.system.eventStream.publish(result) @@ -287,7 +287,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A .record(System.currentTimeMillis - start, TimeUnit.MILLISECONDS) } - def onFailure(sender: ActorRef, result: PaymentFailed): Unit = { + private def onFailure(sender: ActorRef, result: PaymentFailed): Unit = { span.fail("payment failed") if (cfg.storeInDb) paymentsDb.updateOutgoingPayment(result) sender ! result @@ -307,7 +307,7 @@ class PaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, router: A object PaymentLifecycle { - def props(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], nodeParams, cfg, router, register) + def props(nodeParams: NodeParams, cfg: SendPaymentConfig, router: ActorRef, register: ActorRef) = Props(new PaymentLifecycle(nodeParams, cfg, router, register)) /** * Send a payment to a pre-defined route without running the path-finding algorithm. @@ -350,8 +350,8 @@ object PaymentLifecycle { // @formatter:off sealed trait Data case object WaitingForRequest extends Data - case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure]) extends Data - case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(ByteVector32, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc], hops: Seq[ChannelHop]) extends Data + case class WaitingForRoute(sender: ActorRef, c: SendPayment, failures: Seq[PaymentFailure], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc]) extends Data + case class WaitingForComplete(sender: ActorRef, c: SendPayment, cmd: CMD_ADD_HTLC, failures: Seq[PaymentFailure], sharedSecrets: Seq[(ByteVector32, PublicKey)], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc], route: Route) extends Data sealed trait State case object WAITING_FOR_REQUEST extends State @@ -359,12 +359,4 @@ object PaymentLifecycle { case object WAITING_FOR_PAYMENT_COMPLETE extends State // @formatter:on - /** - * This method retrieves the channel update that we used when we built a route. - * It just iterates over the hops, but there are at most 20 of them. - * - * @return the channel update if found - */ - def getChannelUpdateForNode(nodeId: PublicKey, hops: Seq[ChannelHop]): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index f74f6c3134..dc28fdac05 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, ShortChannelId, serializationResult} +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, serializationResult} import scodec.bits.{BitVector, ByteVector} import shapeless.HNil @@ -31,16 +31,16 @@ import scala.concurrent.duration._ */ object Announcements { - def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: ByteVector, unknownFields: ByteVector): ByteVector = + def channelAnnouncementWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, nodeId2: PublicKey, bitcoinKey1: PublicKey, bitcoinKey2: PublicKey, features: Features, unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelAnnouncementWitnessCodec.encode(features :: chainHash :: shortChannelId :: nodeId1 :: nodeId2 :: bitcoinKey1 :: bitcoinKey2 :: unknownFields :: HNil)))) - def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: ByteVector, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector = + def nodeAnnouncementWitnessEncode(timestamp: Long, nodeId: PublicKey, rgbColor: Color, alias: String, features: Features, addresses: List[NodeAddress], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.nodeAnnouncementWitnessCodec.encode(features :: timestamp :: nodeId :: rgbColor :: alias :: addresses :: unknownFields :: HNil)))) def channelUpdateWitnessEncode(chainHash: ByteVector32, shortChannelId: ShortChannelId, timestamp: Long, messageFlags: Byte, channelFlags: Byte, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: Option[MilliSatoshi], unknownFields: ByteVector): ByteVector = sha256(sha256(serializationResult(LightningMessageCodecs.channelUpdateWitnessCodec.encode(chainHash :: shortChannelId :: timestamp :: messageFlags :: channelFlags :: cltvExpiryDelta :: htlcMinimumMsat :: feeBaseMsat :: feeProportionalMillionths :: htlcMaximumMsat :: unknownFields :: HNil)))) - def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: ByteVector): (ByteVector64, ByteVector64) = { + def signChannelAnnouncement(chainHash: ByteVector32, shortChannelId: ShortChannelId, localNodeSecret: PrivateKey, remoteNodeId: PublicKey, localFundingPrivKey: PrivateKey, remoteFundingKey: PublicKey, features: Features): (ByteVector64, ByteVector64) = { val witness = if (isNode1(localNodeSecret.publicKey, remoteNodeId)) { channelAnnouncementWitnessEncode(chainHash, shortChannelId, localNodeSecret.publicKey, remoteNodeId, localFundingPrivKey.publicKey, remoteFundingKey, features, unknownFields = ByteVector.empty) } else { @@ -68,12 +68,12 @@ object Announcements { nodeId2 = nodeId2, bitcoinKey1 = bitcoinKey1, bitcoinKey2 = bitcoinKey2, - features = ByteVector.empty, + features = Features.empty, chainHash = chainHash ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: ByteVector, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features, timestamp: Long = System.currentTimeMillis.milliseconds.toSeconds): NodeAnnouncement = { require(alias.length <= 32) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 94373aa44c..71adfaaba8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -17,12 +17,13 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Btc, Satoshi} +import fr.acinq.bitcoin.{Btc, MilliBtc, Satoshi} import fr.acinq.eclair._ import fr.acinq.eclair.router.Graph.GraphStructure.{DirectedGraph, GraphEdge} import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.ChannelUpdate +import scala.annotation.tailrec import scala.collection.immutable.SortedMap import scala.collection.mutable @@ -53,9 +54,10 @@ object Graph { /** * This comparator must be consistent with the "equals" behavior, thus for two weighted nodes with - * the same weight we distinguish them by their public key. See https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html + * the same weight we distinguish them by their public key. + * See https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html */ - object QueueComparator extends Ordering[WeightedNode] { + object NodeComparator extends Ordering[WeightedNode] { override def compare(x: WeightedNode, y: WeightedNode): Int = { val weightCmp = x.weight.compareTo(y.weight) if (weightCmp == 0) x.key.toString().compareTo(y.key.toString()) @@ -68,15 +70,18 @@ object Graph { } /** - * Yen's algorithm to find the k-shortest (loop-less) paths in a graph, uses dijkstra as search algo. Is guaranteed to terminate finding - * at most @pathsToFind paths sorted by cost (the cheapest is in position 0). + * Yen's algorithm to find the k-shortest (loop-less) paths in a graph, uses dijkstra as search algo. Is guaranteed to + * terminate finding at most @pathsToFind paths sorted by cost (the cheapest is in position 0). * - * @param graph graph representing the whole network - * @param sourceNode sender node (payer) - * @param targetNode target node (final recipient) + * @param graph the graph on which will be performed the search + * @param sourceNode the starting node of the path we're looking for (payer) + * @param targetNode the destination node of the path (recipient) * @param amount amount to send to the last node + * @param ignoredEdges channels that should be avoided + * @param ignoredVertices nodes that should be avoided + * @param extraEdges additional edges that can be used (e.g. private channels from invoices) * @param pathsToFind number of distinct paths to be returned - * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path + * @param wr ratios used to 'weight' edges when searching for the shortest path * @param currentBlockHeight the height of the chain tip (latest block) * @param boundaries a predicate function that can be used to impose limits on the outcome of the search */ @@ -91,69 +96,51 @@ object Graph { wr: Option[WeightRatios], currentBlockHeight: Long, boundaries: RichWeight => Boolean): Seq[WeightedPath] = { + // find the shortest path (k = 0) + val targetWeight = RichWeight(amount, 0, CltvExpiryDelta(0), 0) + val shortestPath = dijkstraShortestPath(graph, sourceNode, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr) + if (shortestPath.isEmpty) { + return Seq.empty // if we can't even find a single path, avoid returning a Seq(Seq.empty) + } var allSpurPathsFound = false - - // stores the shortest paths val shortestPaths = new mutable.ArrayDeque[WeightedPath] - // stores the candidates for k(K +1) shortest paths, sorted by path cost + shortestPaths += WeightedPath(shortestPath, pathWeight(sourceNode, shortestPath, amount, currentBlockHeight, wr)) + // stores the candidates for the k-th shortest path, sorted by path cost val candidates = new mutable.PriorityQueue[WeightedPath] - // find the shortest path, k = 0 - val initialWeight = RichWeight(cost = amount, 0, CltvExpiryDelta(0), 0) - val shortestPath = dijkstraShortestPath(graph, sourceNode, targetNode, ignoredEdges, ignoredVertices, extraEdges, initialWeight, boundaries, currentBlockHeight, wr) - shortestPaths += WeightedPath(shortestPath, pathWeight(shortestPath, amount, isPartial = false, currentBlockHeight, wr)) - - // avoid returning a list with an empty path - if (shortestPath.isEmpty) return Seq.empty - // main loop for (k <- 1 until pathsToFind) { - if (!allSpurPathsFound) { - - // for every edge in the path - for (i <- shortestPaths(k - 1).path.indices) { - - val prevShortestPath = shortestPaths(k - 1).path - - // select the spur node as the i-th element of the k-th previous shortest path (k -1) - val spurEdge = prevShortestPath(i) - - // select the sub-path from the source to the spur node of the k-th previous shortest path - val rootPathEdges = if (i == 0) prevShortestPath.head :: Nil else prevShortestPath.take(i) - val rootPathWeight = pathWeight(rootPathEdges, amount, isPartial = true, currentBlockHeight, wr) - - // links to be removed that are part of the previous shortest path and which share the same root path - val edgesToIgnore = shortestPaths.flatMap { weightedPath => - if ((i == 0 && (weightedPath.path.head :: Nil) == rootPathEdges) || weightedPath.path.take(i) == rootPathEdges) { - weightedPath.path(i).desc :: Nil - } else { - Nil - } - } - - // remove any link that can lead back to the previous vertex to avoid going back from where we arrived (previous iteration) - val returningEdges = rootPathEdges.lastOption.map(last => graph.getEdgesBetween(last.desc.b, last.desc.a)).toSeq.flatten.map(_.desc) - - // find the "spur" path, a sub-path going from the spur edge to the target avoiding previously found sub-paths - val spurPath = dijkstraShortestPath(graph, spurEdge.desc.a, targetNode, ignoredEdges ++ edgesToIgnore.toSet ++ returningEdges.toSet, ignoredVertices, extraEdges, rootPathWeight, boundaries, currentBlockHeight, wr) - - // if there wasn't a path the spur will be empty + val prevShortestPath = shortestPaths(k - 1).path + // for every edge in the path, we will try to find a different path after that edge + for (i <- prevShortestPath.indices) { + // select the spur node as the i-th element of the previous shortest path + val spurNode = prevShortestPath(i).desc.a + // select the sub-path from the source to the spur node + val rootPathEdges = prevShortestPath.take(i) + // we ignore all the paths that we have already fully explored in previous iterations + // if for example the spur node is B, and we already found shortest paths starting with A-B-C and A-B-D, + // we want to ignore the B-C and B-D edges + // +-- C -- [...] + // | + // A -- B --+-- D -- [...] + // | + // +-- E -- [...] + val alreadyExploredEdges = shortestPaths.collect { case p if p.path.take(i) == rootPathEdges => p.path(i).desc }.toSet + // we also want to ignore any link that can lead back to the previous node (we only want to go forward) + val returningEdges = rootPathEdges.lastOption.map(last => graph.getEdgesBetween(last.desc.b, last.desc.a).map(_.desc).toSet).getOrElse(Set.empty) + // find the "spur" path, a sub-path going from the spur node to the target avoiding previously found sub-paths + val spurPath = dijkstraShortestPath(graph, sourceNode, spurNode, targetNode, ignoredEdges ++ alreadyExploredEdges ++ returningEdges, ignoredVertices, extraEdges, targetWeight, boundaries, currentBlockHeight, wr) if (spurPath.nonEmpty) { - - // candidate k-shortest path is made of the rootPath and the new spurPath - val totalPath = rootPathEdges.head.desc.a == spurPath.head.desc.a match { - case true => rootPathEdges.tail ++ spurPath // if the heads are the same node, drop it from the rootPath - case false => rootPathEdges ++ spurPath - } - - val candidatePath = WeightedPath(totalPath, pathWeight(totalPath, amount, isPartial = false, currentBlockHeight, wr)) - - if (boundaries(candidatePath.weight) && !shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath)) { + // candidate k-shortest path is made of the root path and the new spur path, but the cost of the spur + // path is likely higher than previous shortest paths, so we need to validate that the root path can + // relay the increased amount. + val completePath = rootPathEdges ++ spurPath + val candidatePath = WeightedPath(completePath, pathWeight(sourceNode, completePath, amount, currentBlockHeight, wr)) + if (boundaries(candidatePath.weight) && !shortestPaths.contains(candidatePath) && !candidates.exists(_ == candidatePath) && validatePath(completePath, amount)) { candidates.enqueue(candidatePath) } - } } } @@ -171,102 +158,90 @@ object Graph { } /** - * Finds the shortest path in the graph, uses a modified version of Dijsktra's algorithm that computes - * the shortest path from the target to the source (this is because we want to calculate the weight of the - * edges correctly). The graph @param g is optimized for querying the incoming edges given a vertex. + * Finds the shortest path in the graph, uses a modified version of Dijkstra's algorithm that computes the shortest + * path from the target to the source (this is because we want to calculate the weight of the edges correctly). The + * graph @param g is optimized for querying the incoming edges given a vertex. * * @param g the graph on which will be performed the search + * @param sender node sending the payment (may be different from sourceNode when calculating partial paths) * @param sourceNode the starting node of the path we're looking for * @param targetNode the destination node of the path - * @param ignoredEdges a list of edges we do not want to consider - * @param extraEdges a list of extra edges we want to consider but are not currently in the graph - * @param wr an object containing the ratios used to 'weight' edges when searching for the shortest path - * @param currentBlockHeight the height of the chain tip (latest block) + * @param ignoredEdges channels that should be avoided + * @param ignoredVertices nodes that should be avoided + * @param extraEdges additional edges that can be used (e.g. private channels from invoices) + * @param initialWeight weight that will be applied to the target node * @param boundaries a predicate function that can be used to impose limits on the outcome of the search - * @return + * @param currentBlockHeight the height of the chain tip (latest block) + * @param wr ratios used to 'weight' edges when searching for the shortest path */ - def dijkstraShortestPath(g: DirectedGraph, - sourceNode: PublicKey, - targetNode: PublicKey, - ignoredEdges: Set[ChannelDesc], - ignoredVertices: Set[PublicKey], - extraEdges: Set[GraphEdge], - initialWeight: RichWeight, - boundaries: RichWeight => Boolean, - currentBlockHeight: Long, - wr: Option[WeightRatios]): Seq[GraphEdge] = { - - // the graph does not contain source/destination nodes - if (!g.containsVertex(sourceNode)) return Seq.empty - if (!g.containsVertex(targetNode) && (extraEdges.nonEmpty && !extraEdges.exists(_.desc.b == targetNode))) return Seq.empty - - val maxMapSize = 100 // conservative estimation to avoid over allocating memory - - // this is not the actual optimal size for the maps, because we only put in there all the vertices in the worst case scenario. - val weight = new java.util.HashMap[PublicKey, RichWeight](maxMapSize) - val prev = new java.util.HashMap[PublicKey, GraphEdge](maxMapSize) - val vertexQueue = new org.jheaps.tree.SimpleFibonacciHeap[WeightedNode, Short](QueueComparator) + private def dijkstraShortestPath(g: DirectedGraph, + sender: PublicKey, + sourceNode: PublicKey, + targetNode: PublicKey, + ignoredEdges: Set[ChannelDesc], + ignoredVertices: Set[PublicKey], + extraEdges: Set[GraphEdge], + initialWeight: RichWeight, + boundaries: RichWeight => Boolean, + currentBlockHeight: Long, + wr: Option[WeightRatios]): Seq[GraphEdge] = { + // the graph does not contain source/destination nodes + val sourceNotInGraph = !g.containsVertex(sourceNode) && !extraEdges.exists(_.desc.a == sourceNode) + val targetNotInGraph = !g.containsVertex(targetNode) && !extraEdges.exists(_.desc.b == targetNode) + if (sourceNotInGraph || targetNotInGraph) { + return Seq.empty + } + + // conservative estimation to avoid over-allocating memory: this is not the actual optimal size for the maps, + // because in the worst case scenario we will insert all the vertices. + val initialSize = 100 + val bestWeights = new java.util.HashMap[PublicKey, RichWeight](initialSize) + val bestEdges = new java.util.HashMap[PublicKey, GraphEdge](initialSize) + val toExplore = new org.jheaps.tree.SimpleFibonacciHeap[WeightedNode, Short](NodeComparator) // initialize the queue and cost array with the initial weight - weight.put(targetNode, initialWeight) - vertexQueue.insert(WeightedNode(targetNode, initialWeight)) + bestWeights.put(targetNode, initialWeight) + toExplore.insert(WeightedNode(targetNode, initialWeight)) var targetFound = false - - while (!vertexQueue.isEmpty && !targetFound) { - - // node with the smallest distance from the source - val current = vertexQueue.deleteMin().getKey // O(log(n)) - + while (!toExplore.isEmpty && !targetFound) { + // node with the smallest distance from the target + val current = toExplore.deleteMin().getKey // O(log(n)) if (current.key != sourceNode) { - + val currentWeight = bestWeights.get(current.key) // NB: there is always an entry for the current in the 'bestWeights' map // build the neighbors with optional extra edges - val currentNeighbors = extraEdges.isEmpty match { - case true => g.getIncomingEdgesOf(current.key) - case false => - val extraNeighbors = extraEdges.filter(_.desc.b == current.key) - // the resulting set must have only one element per shortChannelId - g.getIncomingEdgesOf(current.key).filterNot(e => extraNeighbors.exists(_.desc.shortChannelId == e.desc.shortChannelId)) ++ extraNeighbors + val neighborEdges = { + val extraNeighbors = extraEdges.filter(_.desc.b == current.key) + // the resulting set must have only one element per shortChannelId; we prioritize extra edges + g.getIncomingEdgesOf(current.key).filterNot(e => extraNeighbors.exists(_.desc.shortChannelId == e.desc.shortChannelId)) ++ extraNeighbors } - - // note: there is always an entry for the current in the 'weight' map - val currentWeight = weight.get(current.key) - - // for each neighbor - currentNeighbors.foreach { edge => - + neighborEdges.foreach { edge => val neighbor = edge.desc.a - - // note: 'newMinimumKnownWeight' contains the smallest known cumulative cost (amount + fees) necessary to reach 'current' so far - val newMinimumKnownWeight = edgeWeight(edge, currentWeight, initialWeight.length == 0 && neighbor == sourceNode, currentBlockHeight, wr) - - // test for ignored edges - if (edge.update.htlcMaximumMsat.forall(newMinimumKnownWeight.cost <= _) && - newMinimumKnownWeight.cost >= edge.update.htlcMinimumMsat && - boundaries(newMinimumKnownWeight) && // check if this neighbor edge would break off the 'boundaries' - !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor) - ) { - // we call containsKey first because "getOrDefault" is not available in JDK7 - val neighborCost = weight.containsKey(neighbor) match { - case false => RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), Double.MaxValue) - case true => weight.get(neighbor) + // NB: this contains the amount (including fees) that will need to be sent to `neighbor`, but the amount that + // will be relayed through that edge is the one in `currentWeight`. + val neighborWeight = addEdgeWeight(sender, edge, currentWeight, currentBlockHeight, wr) + val canRelayAmount = currentWeight.cost <= edge.capacity && + edge.balance_opt.forall(currentWeight.cost <= _) && + edge.update.htlcMaximumMsat.forall(currentWeight.cost <= _) && + currentWeight.cost >= edge.update.htlcMinimumMsat + if (canRelayAmount && boundaries(neighborWeight) && !ignoredEdges.contains(edge.desc) && !ignoredVertices.contains(neighbor)) { + // we don't use "getOrDefault" because it is not available in JDK7 + val previousNeighborWeight = bestWeights.get(neighbor) match { + case null => RichWeight(MilliSatoshi(Long.MaxValue), Int.MaxValue, CltvExpiryDelta(Int.MaxValue), Double.MaxValue) + case w => w } - - // if this neighbor has a shorter distance than previously known - if (newMinimumKnownWeight.weight < neighborCost.weight) { - - // update the visiting tree - prev.put(neighbor, edge) - - // update the queue - vertexQueue.insert(WeightedNode(neighbor, newMinimumKnownWeight)) // O(1) - + // if this path between neighbor and the target has a shorter distance than previously known, we select it + if (neighborWeight.weight < previousNeighborWeight.weight) { + // update the best edge for this vertex + bestEdges.put(neighbor, edge) + // add this updated node to the list for further exploration + toExplore.insert(WeightedNode(neighbor, neighborWeight)) // O(1) // update the minimum known distance array - weight.put(neighbor, newMinimumKnownWeight) + bestWeights.put(neighbor, neighborWeight) } } } - } else { // we popped the target node from the queue, no need to search any further + } else { targetFound = true } } @@ -274,26 +249,30 @@ object Graph { targetFound match { case false => Seq.empty[GraphEdge] case true => - // we traverse the list of "previous" backward building the final list of edges that make the shortest path val edgePath = new mutable.ArrayBuffer[GraphEdge](RouteCalculation.ROUTE_MAX_LENGTH) - var current = prev.get(sourceNode) - + var current = bestEdges.get(sourceNode) while (current != null) { - edgePath += current - current = prev.get(current.desc.b) + current = bestEdges.get(current.desc.b) } - edgePath.toSeq } } - // Computes the compound weight for the given @param edge, the weight is cumulative and must account for the previous edge's weight. - private def edgeWeight(edge: GraphEdge, prev: RichWeight, isNeighborTarget: Boolean, currentBlockHeight: Long, weightRatios: Option[WeightRatios]): RichWeight = weightRatios match { + /** + * Add the given edge to the path and compute the new weight. + * + * @param sender node sending the payment + * @param edge the edge we want to cross + * @param prev weight of the rest of the path + * @param currentBlockHeight the height of the chain tip (latest block). + * @param weightRatios ratios used to 'weight' edges when searching for the shortest path + */ + private def addEdgeWeight(sender: PublicKey, edge: GraphEdge, prev: RichWeight, currentBlockHeight: Long, weightRatios: Option[WeightRatios]): RichWeight = weightRatios match { case None => - val edgeCost = if (isNeighborTarget) prev.cost else edgeFeeCost(edge, prev.cost) - RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + edge.update.cltvExpiryDelta, weight = edgeCost.toLong) - + val totalCost = if (edge.desc.a == sender) prev.cost else addEdgeFees(edge, prev.cost) + val totalCltv = if (edge.desc.a == sender) prev.cltv else prev.cltv + edge.update.cltvExpiryDelta + RichWeight(totalCost, prev.length + 1, totalCltv, totalCost.toLong) case Some(wr) => import RoutingHeuristics._ @@ -302,48 +281,67 @@ object Graph { val ageFactor = normalize(channelBlockHeight, min = currentBlockHeight - BLOCK_TIME_TWO_MONTHS, max = currentBlockHeight) // Every edge is weighted by channel capacity, larger channels add less weight - val edgeMaxCapacity = edge.update.htlcMaximumMsat.getOrElse(CAPACITY_CHANNEL_LOW) + val edgeMaxCapacity = edge.capacity.toMilliSatoshi val capFactor = 1 - normalize(edgeMaxCapacity.toLong, CAPACITY_CHANNEL_LOW.toLong, CAPACITY_CHANNEL_HIGH.toLong) // Every edge is weighted by its cltv-delta value, normalized - val channelCltvDelta = edge.update.cltvExpiryDelta.toInt - val cltvFactor = normalize(channelCltvDelta, CLTV_LOW, CLTV_HIGH) - - // NB 'edgeCost' includes the amount to be sent plus the fees that must be paid to traverse this @param edge - val edgeCost = if (isNeighborTarget) prev.cost else edgeFeeCost(edge, prev.cost) + val cltvFactor = normalize(edge.update.cltvExpiryDelta.toInt, CLTV_LOW, CLTV_HIGH) + val totalCost = if (edge.desc.a == sender) prev.cost else addEdgeFees(edge, prev.cost) + val totalCltv = if (edge.desc.a == sender) prev.cltv else prev.cltv + edge.update.cltvExpiryDelta // NB we're guaranteed to have weightRatios and factors > 0 val factor = (cltvFactor * wr.cltvDeltaFactor) + (ageFactor * wr.ageFactor) + (capFactor * wr.capacityFactor) - val edgeWeight = if (isNeighborTarget) prev.weight else prev.weight + edgeCost.toLong * factor + val totalWeight = if (edge.desc.a == sender) prev.weight else prev.weight + totalCost.toLong * factor - RichWeight(cost = edgeCost, length = prev.length + 1, cltv = prev.cltv + channelCltvDelta, weight = edgeWeight) + RichWeight(totalCost, prev.length + 1, totalCltv, totalWeight) } /** - * This forces channel_update(s) with fees=0 to have a minimum of 1msat for the baseFee. Note that - * the update is not being modified and the result of the route computation will still have the update - * with fees=0 which is what will be used to build the onion. + * Calculate the minimum amount that the start node needs to receive to be able to forward @amountWithFees to the end + * node. To avoid infinite loops caused by zero-fee edges, we use a lower bound fee of 1 msat. * - * @param edge the edge for which we want to compute the weight - * @param amountWithFees the value that this edge will have to carry along + * @param edge the edge we want to cross + * @param amountToForward the value that this edge will have to carry along * @return the new amount updated with the necessary fees for this edge */ - private def edgeFeeCost(edge: GraphEdge, amountWithFees: MilliSatoshi): MilliSatoshi = { - if (edgeHasZeroFee(edge)) amountWithFees + nodeFee(baseFee = 1 msat, proportionalFee = 0, amountWithFees) - else amountWithFees + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountWithFees) + private def addEdgeFees(edge: GraphEdge, amountToForward: MilliSatoshi): MilliSatoshi = { + if (edgeHasZeroFee(edge)) amountToForward + nodeFee(baseFee = 1 msat, proportionalFee = 0, amountToForward) + else amountToForward + nodeFee(edge.update.feeBaseMsat, edge.update.feeProportionalMillionths, amountToForward) } private def edgeHasZeroFee(edge: GraphEdge): Boolean = { edge.update.feeBaseMsat.toLong == 0 && edge.update.feeProportionalMillionths == 0 } - // Calculates the total cost of a path (amount + fees), direct channels with the source will have a cost of 0 (pay no fees) - def pathWeight(path: Seq[GraphEdge], amountMsat: MilliSatoshi, isPartial: Boolean, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { - path.drop(if (isPartial) 0 else 1).foldRight(RichWeight(amountMsat, 0, CltvExpiryDelta(0), 0)) { (edge, prev) => - edgeWeight(edge, prev, isNeighborTarget = false, currentBlockHeight, wr) - } + /** Validate that all edges along the path can relay the amount with fees. */ + def validatePath(path: Seq[GraphEdge], amount: MilliSatoshi): Boolean = validateReversePath(path.reverse, amount) + + @tailrec + private def validateReversePath(path: Seq[GraphEdge], amount: MilliSatoshi): Boolean = path.headOption match { + case None => true + case Some(edge) => + val canRelayAmount = amount <= edge.capacity && + edge.balance_opt.forall(amount <= _) && + edge.update.htlcMaximumMsat.forall(amount <= _) && + edge.update.htlcMinimumMsat <= amount + if (canRelayAmount) validateReversePath(path.tail, addEdgeFees(edge, amount)) else false } + /** + * Calculates the total weighted cost of a path. + * Note that the first hop from the sender is ignored: we don't pay a routing fee to ourselves. + * + * @param sender node sending the payment + * @param path candidate path. + * @param amount amount to send to the last node. + * @param currentBlockHeight the height of the chain tip (latest block). + * @param wr ratios used to 'weight' edges when searching for the shortest path + */ + def pathWeight(sender: PublicKey, path: Seq[GraphEdge], amount: MilliSatoshi, currentBlockHeight: Long, wr: Option[WeightRatios]): RichWeight = { + path.foldRight(RichWeight(amount, 0, CltvExpiryDelta(0), 0)) { (edge, prev) => + addEdgeWeight(sender, edge, prev, currentBlockHeight, wr) + } + } object RoutingHeuristics { @@ -351,7 +349,7 @@ object Graph { val BLOCK_TIME_TWO_MONTHS = 8640 // Low/High bound for channel capacity - val CAPACITY_CHANNEL_LOW = Btc(0.001).toMilliSatoshi + val CAPACITY_CHANNEL_LOW = MilliBtc(1).toMilliSatoshi val CAPACITY_CHANNEL_HIGH = Btc(1).toMilliSatoshi // Low/High bound for CLTV channel value @@ -362,26 +360,27 @@ object Graph { * Normalize the given value between (0, 1). If the @param value is outside the min/max window we flatten it to something very close to the * extremes but always bigger than zero so it's guaranteed to never return zero */ - def normalize(value: Double, min: Double, max: Double) = { + def normalize(value: Double, min: Double, max: Double): Double = { if (value <= min) 0.00001D else if (value > max) 0.99999D else (value - min) / (max - min) } + } - /** - * A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors - */ object GraphStructure { /** * Representation of an edge of the graph * - * @param desc channel description - * @param update channel info + * @param desc channel description + * @param update channel info + * @param capacity channel capacity + * @param balance_opt (optional) available balance that can be sent through this edge */ case class GraphEdge(desc: ChannelDesc, update: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi]) + /** A graph data structure that uses an adjacency list, stores the incoming edges of the neighbors */ case class DirectedGraph(private val vertices: Map[PublicKey, List[GraphEdge]]) { def addEdge(d: ChannelDesc, u: ChannelUpdate, capacity: Satoshi, balance_opt: Option[MilliSatoshi] = None): DirectedGraph = addEdge(GraphEdge(d, u, capacity, balance_opt)) @@ -524,11 +523,10 @@ object Graph { // @formatter:on /** - * This is the recommended way of creating the network graph. - * We don't include private channels: they would bloat the graph without providing any value (if they are private - * they likely don't want to be involved in routing other people's payments). - * The only private channels we know are ours: we should check them to see if our destination can be reached in a - * single hop via a private channel before using the public network graph. + * This is the recommended way of initializing the network graph (from a public network DB). + * We only use public channels at first; private channels will be added one by one as they come online, and removed + * as they go offline. + * Private channels may be used to route payments, but most of the time, they will be the first or last hop. * * @param channels map of all known public channels in the network. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala index c247bf8345..9ba1a4bffd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/RouteCalculation.scala @@ -29,7 +29,6 @@ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.ChannelUpdate import fr.acinq.eclair.{ShortChannelId, _} -import scala.compat.Platform import scala.concurrent.duration._ import scala.util.{Random, Try} @@ -49,7 +48,7 @@ object RouteCalculation { // select the largest edge (using balance when available, otherwise capacity). val selectedEdges = edges.map(es => es.maxBy(e => e.balance_opt.getOrElse(e.capacity.toMilliSatoshi))) val hops = selectedEdges.map(d => ChannelHop(d.desc.a, d.desc.b, d.update)) - ctx.sender ! RouteResponse(hops, Set.empty, Set.empty) + ctx.sender ! RouteResponse(Route(fr.amount, hops) :: Nil) case _ => // some nodes in the supplied route aren't connected in our graph ctx.sender ! Status.Failure(new IllegalArgumentException("Not all the nodes in the supplied route are connected with public channels")) } @@ -69,16 +68,16 @@ object RouteCalculation { val defaultRouteParams: RouteParams = getDefaultRouteParams(routerConf) val params = r.routeParams.getOrElse(defaultRouteParams) val routesToFind = if (params.randomize) DEFAULT_ROUTES_COUNT else 1 - + log.info(s"finding a route ${r.source}->${r.target} with assistedChannels={} ignoreNodes={} ignoreChannels={} excludedChannels={}", assistedChannels.keys.mkString(","), r.ignoreNodes.map(_.value).mkString(","), r.ignoreChannels.mkString(","), d.excludedChannels.mkString(",")) log.info(s"finding a route with randomize={} params={}", routesToFind > 1, params) findRoute(d.graph, r.source, r.target, r.amount, numRoutes = routesToFind, extraEdges = extraEdges, ignoredEdges = ignoredEdges, ignoredVertices = r.ignoreNodes, routeParams = params, currentBlockHeight) - .map(route => ctx.sender ! RouteResponse(route, r.ignoreNodes, r.ignoreChannels)) + .map(route => ctx.sender ! RouteResponse(route :: Nil)) .recover { case t => ctx.sender ! Status.Failure(t) } d } - def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { + private def toFakeUpdate(extraHop: ExtraHop, htlcMaximum: MilliSatoshi): ChannelUpdate = { // the `direction` bit in flags will not be accurate but it doesn't matter because it is not used // what matters is that the `disable` bit is 0 so that this update doesn't get filtered out ChannelUpdate(signature = ByteVector64.Zeroes, chainHash = ByteVector32.Zeroes, extraHop.shortChannelId, System.currentTimeMillis.milliseconds.toSeconds, messageFlags = 1, channelFlags = 0, extraHop.cltvExpiryDelta, htlcMinimumMsat = 0 msat, extraHop.feeBase, extraHop.feeProportionalMillionths, Some(htlcMaximum)) @@ -100,11 +99,9 @@ object RouteCalculation { } /** Bolt 11 routing hints don't include the channel's capacity, so we round up the maximum htlc amount. */ - def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat + private def htlcMaxToCapacity(htlcMaximum: MilliSatoshi): Satoshi = htlcMaximum.truncateToSatoshi + 1.sat - /** - * This method is used after a payment failed, and we want to exclude some nodes that we know are failing - */ + /** This method is used after a payment failed, and we want to exclude some nodes that we know are failing */ def getIgnoredChannelDesc(channels: Map[ShortChannelId, PublicChannel], ignoreNodes: Set[PublicKey]): Iterable[ChannelDesc] = { val desc = if (ignoreNodes.isEmpty) { Iterable.empty[ChannelDesc] @@ -117,18 +114,16 @@ object RouteCalculation { desc } - /** - * https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications - */ + /** https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#clarifications */ val ROUTE_MAX_LENGTH = 20 - // Max allowed CLTV for a route + /** Max allowed CLTV for a route (one week) */ val DEFAULT_ROUTE_MAX_CLTV = CltvExpiryDelta(1008) - // The default number of routes we'll search for when findRoute is called with randomize = true + /** The default number of routes we'll search for when findRoute is called with randomize = true */ val DEFAULT_ROUTES_COUNT = 3 - def getDefaultRouteParams(routerConf: RouterConf) = RouteParams( + def getDefaultRouteParams(routerConf: RouterConf): RouteParams = RouteParams( randomize = routerConf.randomizeRouteSelection, maxFeeBase = routerConf.searchMaxFeeBase.toMilliSatoshi, maxFeePct = routerConf.searchMaxFeePct, @@ -167,7 +162,7 @@ object RouteCalculation { ignoredEdges: Set[ChannelDesc] = Set.empty, ignoredVertices: Set[PublicKey] = Set.empty, routeParams: RouteParams, - currentBlockHeight: Long): Try[Seq[ChannelHop]] = Try { + currentBlockHeight: Long): Try[Route] = Try { if (localNodeId == targetNodeId) throw CannotRouteToSelf @@ -207,7 +202,8 @@ object RouteCalculation { val randomizedRoutes = if (routeParams.randomize) Random.shuffle(routes) else routes val route = randomizedRoutes.head.path.map(graphEdgeToHop) Metrics.RouteLength.withTag(Tags.Amount, Tags.amountBucket(amount)).record(route.length) - route + Route(amount, route) } } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index a90951c10f..db8bbb6d96 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -375,8 +375,16 @@ object Router { case class FinalizeRoute(amount: MilliSatoshi, hops: Seq[PublicKey], assistedRoutes: Seq[Seq[ExtraHop]] = Nil) - case class RouteResponse(hops: Seq[ChannelHop], ignoreNodes: Set[PublicKey], ignoreChannels: Set[ChannelDesc], allowEmpty: Boolean = false) { + case class Route(amount: MilliSatoshi, hops: Seq[ChannelHop], allowEmpty: Boolean = false) { require(allowEmpty || hops.nonEmpty, "route cannot be empty") + val length = hops.length + + /** This method retrieves the channel update that we used when we built the route. */ + def getChannelUpdateForNode(nodeId: PublicKey): Option[ChannelUpdate] = hops.find(_.nodeId == nodeId).map(_.lastUpdate) + } + + case class RouteResponse(routes: Seq[Route]) { + require(routes.nonEmpty, "routes cannot be empty") } // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 488d787d6f..601758cc7f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -284,7 +284,7 @@ object Validation { val origins1 = d.rebroadcast.updates(u) ++ origins // NB: we update the channels because the balances may have changed even if the channel_update is the same. val pc1 = pc.applyChannelUpdate(update) - val graph1 = d.graph.removeEdge(desc).addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) d.copy(rebroadcast = d.rebroadcast.copy(updates = d.rebroadcast.updates + (u -> origins1)), channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) } else if (StaleChannels.isStale(u)) { log.debug("ignoring {} (stale)", u) @@ -293,7 +293,14 @@ object Validation { } else if (pc.getChannelUpdateSameSideAs(u).exists(_.timestamp >= u.timestamp)) { log.debug("ignoring {} (duplicate)", u) sendDecision(origins, GossipDecision.Duplicate(u)) - d + update match { + case Left(_) => + // NB: we update the graph because the balances may have changed even if the channel_update is the same. + val pc1 = pc.applyChannelUpdate(update) + val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + d.copy(channels = d.channels + (u.shortChannelId -> pc1), graph = graph1) + case Right(_) => d + } } else if (!Announcements.checkSig(u, pc.getNodeIdSameSideAs(u))) { log.warning("bad signature for announcement shortChannelId={} {}", u.shortChannelId, u) sendDecision(origins, GossipDecision.InvalidSignature(u)) @@ -307,7 +314,7 @@ object Validation { // update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = if (Announcements.isEnabled(u.channelFlags)) { - d.graph.removeEdge(desc).addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) } else { d.graph.removeEdge(desc) } @@ -356,7 +363,7 @@ object Validation { // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = if (Announcements.isEnabled(u.channelFlags)) { - d.graph.removeEdge(desc).addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) + d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) } else { d.graph.removeEdge(desc) } @@ -468,7 +475,7 @@ object Validation { val pc1 = pc.updateBalances(e.commitments) val desc = ChannelDesc(e.shortChannelId, e.commitments.localParams.nodeId, e.commitments.remoteParams.nodeId) val update_opt = if (e.commitments.localParams.nodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt - val graph1 = update_opt.map(u => d.graph.removeEdge(desc).addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) + val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) (d.channels + (e.shortChannelId -> pc1), graph1) case None => (d.channels, d.graph) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 0dccf69cbd..378ee6d5da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.wire.CommonCodecs._ import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.{KamonExt, wire} +import fr.acinq.eclair.{Features, KamonExt, wire} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -28,15 +28,20 @@ import scodec.{Attempt, Codec} */ object LightningMessageCodecs { + val featuresCodec: Codec[Features] = varsizebinarydata.xmap[Features]( + { bytes => Features(bytes) }, + { features => features.toByteVector } + ) + /** For historical reasons, features are divided into two feature bitmasks. We only send from the second one, but we allow receiving in both. */ - val combinedFeaturesCodec: Codec[ByteVector] = ( + val combinedFeaturesCodec: Codec[Features] = ( ("globalFeatures" | varsizebinarydata) :: - ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[ByteVector]( + ("localFeatures" | varsizebinarydata)).as[(ByteVector, ByteVector)].xmap[Features]( { case (gf, lf) => val length = gf.length.max(lf.length) - gf.padLeft(length) | lf.padLeft(length) + Features(gf.padLeft(length) | lf.padLeft(length)) }, - { features => (ByteVector.empty, features) }) + { features => (ByteVector.empty, features.toByteVector) }) val initCodec: Codec[Init] = (("features" | combinedFeaturesCodec) :: ("tlvStream" | InitTlvCodecs.initTlvCodec)).as[Init] @@ -165,7 +170,7 @@ object LightningMessageCodecs { ("bitcoinSignature" | bytes64)).as[AnnouncementSignatures] val channelAnnouncementWitnessCodec = - ("features" | varsizebinarydata) :: + ("features" | featuresCodec) :: ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: ("nodeId1" | publicKey) :: @@ -182,7 +187,7 @@ object LightningMessageCodecs { channelAnnouncementWitnessCodec).as[ChannelAnnouncement] val nodeAnnouncementWitnessCodec = - ("features" | varsizebinarydata) :: + ("features" | featuresCodec) :: ("timestamp" | uint32) :: ("nodeId" | publicKey) :: ("rgbColor" | rgb) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 90055d8537..4acd7f6299 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -23,7 +23,7 @@ import com.google.common.base.Charsets import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi} import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector import scala.util.Try @@ -46,7 +46,7 @@ sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32 sealed trait UpdateMessage extends HtlcMessage // <- not in the spec // @formatter:on -case class Init(features: ByteVector, tlvs: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { +case class Init(features: Features, tlvs: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { val networks = tlvs.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) } @@ -162,7 +162,7 @@ case class ChannelAnnouncement(nodeSignature1: ByteVector64, nodeSignature2: ByteVector64, bitcoinSignature1: ByteVector64, bitcoinSignature2: ByteVector64, - features: ByteVector, + features: Features, chainHash: ByteVector32, shortChannelId: ShortChannelId, nodeId1: PublicKey, @@ -206,7 +206,7 @@ case class Tor3(tor3: String, port: Int) extends OnionAddress { override def soc case class NodeAnnouncement(signature: ByteVector64, - features: ByteVector, + features: Features, timestamp: Long, nodeId: PublicKey, rgbColor: Color, 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 d36afacef6..f69e0d45a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair +import com.typesafe.config.ConfigFactory +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -27,35 +29,36 @@ import scodec.bits._ class FeaturesSpec extends AnyFunSuite { test("'initial_routing_sync' feature") { - assert(hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Optional))) - assert(!hasFeature(hex"08", InitialRoutingSync, Some(FeatureSupport.Mandatory))) + assert(Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Optional))) + assert(!Features(hex"08").hasFeature(InitialRoutingSync, Some(FeatureSupport.Mandatory))) } test("'data_loss_protect' feature") { - assert(hasFeature(hex"01", OptionDataLossProtect, Some(FeatureSupport.Mandatory))) - assert(hasFeature(hex"02", OptionDataLossProtect, Some(FeatureSupport.Optional))) + assert(Features(hex"01").hasFeature(OptionDataLossProtect, Some(FeatureSupport.Mandatory))) + assert(Features(hex"02").hasFeature(OptionDataLossProtect, Some(FeatureSupport.Optional))) } test("'initial_routing_sync', 'data_loss_protect' and 'variable_length_onion' features") { - val features = hex"010a" + val features = Features(Set(ActivatedFeature(InitialRoutingSync, Optional), ActivatedFeature(OptionDataLossProtect, Optional), ActivatedFeature(VariableLengthOnion, Mandatory))) + assert(features.toByteVector == hex"010a") assert(areSupported(features)) - assert(hasFeature(features, OptionDataLossProtect)) - assert(hasFeature(features, InitialRoutingSync, None)) - assert(hasFeature(features, VariableLengthOnion)) + assert(features.hasFeature(OptionDataLossProtect)) + assert(features.hasFeature(InitialRoutingSync, None)) + assert(features.hasFeature(VariableLengthOnion)) } test("'variable_length_onion' feature") { - assert(hasFeature(hex"0100", VariableLengthOnion)) - assert(hasFeature(hex"0100", VariableLengthOnion, Some(FeatureSupport.Mandatory))) - assert(hasFeature(hex"0200", VariableLengthOnion, None)) - assert(hasFeature(hex"0200", VariableLengthOnion, Some(FeatureSupport.Optional))) + assert(Features(hex"0100").hasFeature(VariableLengthOnion)) + assert(Features(hex"0100").hasFeature(VariableLengthOnion, Some(FeatureSupport.Mandatory))) + assert(Features(hex"0200").hasFeature(VariableLengthOnion, None)) + assert(Features(hex"0200").hasFeature(VariableLengthOnion, Some(FeatureSupport.Optional))) } test("'option_static_remotekey' feature") { - assert(hasFeature(hex"1000", StaticRemoteKey)) - assert(hasFeature(hex"1000", StaticRemoteKey, Some(FeatureSupport.Mandatory))) - assert(hasFeature(hex"2000", StaticRemoteKey, None)) - assert(hasFeature(hex"2000", StaticRemoteKey, Some(FeatureSupport.Optional))) + assert(Features(hex"1000").hasFeature(StaticRemoteKey)) + assert(Features(hex"1000").hasFeature(StaticRemoteKey, Some(Mandatory))) + assert(Features(hex"2000").hasFeature(StaticRemoteKey)) + assert(Features(hex"2000").hasFeature(StaticRemoteKey, Some(Optional))) } @@ -84,31 +87,31 @@ class FeaturesSpec extends AnyFunSuite { for ((testCase, valid) <- testCases) { if (valid) { - assert(validateFeatureGraph(testCase) === None) - assert(validateFeatureGraph(testCase.bytes) === None) + assert(validateFeatureGraph(Features(testCase)) === None) + assert(validateFeatureGraph(Features(testCase.bytes)) === None) } else { - assert(validateFeatureGraph(testCase).nonEmpty) - assert(validateFeatureGraph(testCase.bytes).nonEmpty) + assert(validateFeatureGraph(Features(testCase)).nonEmpty) + assert(validateFeatureGraph(Features(testCase.bytes)).nonEmpty) } } } test("features compatibility") { - assert(areSupported(ByteVector.fromLong(1L << InitialRoutingSync.optional))) - assert(areSupported(ByteVector.fromLong(1L << OptionDataLossProtect.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << OptionDataLossProtect.optional))) - assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueries.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueries.optional))) - assert(areSupported(ByteVector.fromLong(1L << VariableLengthOnion.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << VariableLengthOnion.optional))) - assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueriesExtended.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << ChannelRangeQueriesExtended.optional))) - assert(areSupported(ByteVector.fromLong(1L << PaymentSecret.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << PaymentSecret.optional))) - assert(areSupported(ByteVector.fromLong(1L << BasicMultiPartPayment.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << BasicMultiPartPayment.optional))) - assert(areSupported(ByteVector.fromLong(1L << Wumbo.mandatory))) - assert(areSupported(ByteVector.fromLong(1L << Wumbo.optional))) + assert(areSupported(Features(Set(ActivatedFeature(InitialRoutingSync, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(OptionDataLossProtect, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(OptionDataLossProtect, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueries, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueriesExtended, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(ChannelRangeQueriesExtended, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(VariableLengthOnion, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(VariableLengthOnion, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(PaymentSecret, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(PaymentSecret, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(BasicMultiPartPayment, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(BasicMultiPartPayment, Optional))))) + assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Mandatory))))) + assert(areSupported(Features(Set(ActivatedFeature(Wumbo, Optional))))) val testCases = Map( bin" 00000000000000001011" -> true, @@ -132,8 +135,128 @@ class FeaturesSpec extends AnyFunSuite { bin"01000000000000000000000000000000" -> false ) for ((testCase, expected) <- testCases) { - assert(areSupported(testCase) === expected, testCase) + assert(areSupported(Features(testCase)) === expected, testCase.toBin) } } + test("features to bytes") { + val testCases = Map( + hex"" -> Features.empty, + hex"0100" -> Features(Set(ActivatedFeature(VariableLengthOnion, Mandatory))), + hex"028a8a" -> Features(Set(ActivatedFeature(OptionDataLossProtect, Optional), ActivatedFeature(InitialRoutingSync, Optional), ActivatedFeature(ChannelRangeQueries, Optional), ActivatedFeature(VariableLengthOnion, Optional), ActivatedFeature(ChannelRangeQueriesExtended, Optional), ActivatedFeature(PaymentSecret, Optional), ActivatedFeature(BasicMultiPartPayment, Optional))), + hex"09004200" -> Features(Set(ActivatedFeature(VariableLengthOnion, Optional), ActivatedFeature(PaymentSecret, Mandatory)), Set(UnknownFeature(24), UnknownFeature(27))), + hex"52000000" -> Features(Set.empty, Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30))) + ) + + for ((bin, features) <- testCases) { + assert(features.toByteVector === bin) + assert(Features(bin) === features) + val notMinimallyEncoded = Features(hex"00" ++ bin) + assert(notMinimallyEncoded === features) + assert(notMinimallyEncoded.toByteVector === bin) // features are minimally-encoded when converting to bytes + } + } + + test("parse features from configuration") { + { + val conf = ConfigFactory.parseString( + """ + |features { + | option_data_loss_protect = optional + | initial_routing_sync = optional + | gossip_queries = optional + | gossip_queries_ex = optional + | var_onion_optin = optional + | payment_secret = optional + | basic_mpp = optional + |} + """.stripMargin) + + val features = fromConfiguration(conf) + assert(features.toByteVector === hex"028a8a") + assert(Features(hex"028a8a") === features) + assert(areSupported(features)) + assert(validateFeatureGraph(features) === None) + assert(features.hasFeature(OptionDataLossProtect, Some(Optional))) + assert(features.hasFeature(InitialRoutingSync, Some(Optional))) + assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) + assert(features.hasFeature(ChannelRangeQueriesExtended, Some(Optional))) + assert(features.hasFeature(VariableLengthOnion, Some(Optional))) + assert(features.hasFeature(PaymentSecret, Some(Optional))) + assert(features.hasFeature(BasicMultiPartPayment, Some(Optional))) + } + + { + val conf = ConfigFactory.parseString( + """ + | features { + | initial_routing_sync = optional + | option_data_loss_protect = optional + | gossip_queries = optional + | gossip_queries_ex = mandatory + | var_onion_optin = optional + | } + | + """.stripMargin + ) + + val features = fromConfiguration(conf) + assert(features.toByteVector === hex"068a") + assert(Features(hex"068a") === features) + assert(areSupported(features)) + assert(validateFeatureGraph(features) === None) + assert(features.hasFeature(OptionDataLossProtect, Some(Optional))) + assert(features.hasFeature(InitialRoutingSync, Some(Optional))) + assert(!features.hasFeature(InitialRoutingSync, Some(Mandatory))) + assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) + assert(features.hasFeature(ChannelRangeQueriesExtended, Some(Mandatory))) + assert(features.hasFeature(VariableLengthOnion, Some(Optional))) + assert(!features.hasFeature(PaymentSecret)) + } + + { + val confWithUnknownFeatures = ConfigFactory.parseString( + """ + |features { + | option_non_existent = mandatory # this is ignored + | gossip_queries = optional + | payment_secret = mandatory + |} + """.stripMargin) + + val features = fromConfiguration(confWithUnknownFeatures) + assert(features.toByteVector === hex"4080") + assert(Features(hex"4080") === features) + assert(areSupported(features)) + assert(features.hasFeature(ChannelRangeQueries, Some(Optional))) + assert(features.hasFeature(PaymentSecret, Some(Mandatory))) + } + + { + val confWithUnknownSupport = ConfigFactory.parseString( + """ + |features { + | option_data_loss_protect = what + | gossip_queries = optional + | payment_secret = mandatory + |} + """.stripMargin) + + assertThrows[RuntimeException](fromConfiguration(confWithUnknownSupport)) + } + } + + test("'knownFeatures' contains all our known features (reflection test)") { + import scala.reflect.runtime.universe._ + import scala.reflect.runtime.{ universe => runtime } + val mirror = runtime.runtimeMirror(ClassLoader.getSystemClassLoader) + val subclasses = typeOf[Feature].typeSymbol.asClass.knownDirectSubclasses + val knownFeatures = subclasses.map({ desc => + val mod = mirror.staticModule(desc.asClass.fullName) + mirror.reflectModule(mod).instance.asInstanceOf[Feature] + }) + + assert((knownFeatures -- Features.knownFeatures).isEmpty) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 00eaf88952..624cd3e664 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -20,7 +20,11 @@ import java.util.concurrent.atomic.AtomicLong import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Block +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.FeatureSupport.Mandatory +import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.crypto.LocalKeyManager +import scodec.bits.ByteVector import org.scalatest.funsuite.AnyFunSuite import scala.jdk.CollectionConverters._ @@ -63,7 +67,7 @@ class StartupSpec extends AnyFunSuite { assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) } - test("NodeParams should fail with deprecated global-features or local-features") { + test("NodeParams should fail with deprecated global-features, local-features or hex features") { for (deprecated <- Seq("global-features", "local-features")) { val illegalGlobalFeaturesConf = ConfigFactory.parseString(deprecated + " = \"0200\"") val conf = illegalGlobalFeaturesConf.withFallback(defaultConf) @@ -71,17 +75,59 @@ class StartupSpec extends AnyFunSuite { val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf)) assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains(deprecated)) } + + val illegalByteVectorFeatures = ConfigFactory.parseString("features = \"0200\"") + val conf = illegalByteVectorFeatures.withFallback(defaultConf) + val nodeParamsAttempt = Try(makeNodeParamsWithDefaults(conf)) + assert(nodeParamsAttempt.failed.get.getMessage == "requirement failed: configuration key 'features' have moved from bytevector to human readable (ex: 'feature-name' = optional/mandatory)") } test("NodeParams should fail if features are inconsistent") { - val legalFeaturesConf = ConfigFactory.parseString("features = \"028a8a\"") - val illegalButAllowedFeaturesConf = ConfigFactory.parseString("features = \"028000\"") // basic_mpp without var_onion_optin - val illegalFeaturesConf = ConfigFactory.parseString("features = \"020000\"") // basic_mpp without payment_secret + val legalFeaturesConf = ConfigFactory.parseMap(Map( + s"features.${OptionDataLossProtect.rfcName}" -> "optional", + s"features.${InitialRoutingSync.rfcName}" -> "optional", + s"features.${ChannelRangeQueries.rfcName}" -> "optional", + s"features.${ChannelRangeQueriesExtended.rfcName}" -> "optional", + s"features.${VariableLengthOnion.rfcName}" -> "optional", + s"features.${PaymentSecret.rfcName}" -> "optional", + s"features.${BasicMultiPartPayment.rfcName}" -> "optional" + ).asJava) + + // basic_mpp without var_onion_optin + val illegalButAllowedFeaturesConf = ConfigFactory.parseMap(Map( + s"features.${PaymentSecret.rfcName}" -> "optional", + s"features.${BasicMultiPartPayment.rfcName}" -> "optional" + ).asJava) + + // basic_mpp without payment_secret + val illegalFeaturesConf = ConfigFactory.parseMap(Map( + s"features.${BasicMultiPartPayment.rfcName}" -> "optional" + ).asJava) + assert(Try(makeNodeParamsWithDefaults(legalFeaturesConf.withFallback(defaultConf))).isSuccess) assert(Try(makeNodeParamsWithDefaults(illegalButAllowedFeaturesConf.withFallback(defaultConf))).isSuccess) assert(Try(makeNodeParamsWithDefaults(illegalFeaturesConf.withFallback(defaultConf))).isFailure) } + test("parse human readable override features") { + val perNodeConf = ConfigFactory.parseString( + """ + | override-features = [ // optional per-node features + | { + | nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + | features { + | basic_mpp = mandatory + | } + | } + | ] + """.stripMargin + ) + + val nodeParams = makeNodeParamsWithDefaults(perNodeConf.withFallback(defaultConf)) + val perNodeFeatures = nodeParams.overrideFeatures(PublicKey(ByteVector.fromValidHex("02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))) + assert(perNodeFeatures.hasFeature(BasicMultiPartPayment, Some(Mandatory))) + } + test("NodeParams should fail if htlc-minimum-msat is set to 0") { val noHtlcMinimumConf = ConfigFactory.parseString("htlc-minimum-msat = 0") assert(Try(makeNodeParamsWithDefaults(noHtlcMinimumConf.withFallback(defaultConf))).isFailure) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 9e1300d8aa..563a4932fd 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -21,6 +21,8 @@ import java.util.concurrent.atomic.AtomicLong import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Script} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.{ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, VariableLengthOnion} import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.crypto.LocalKeyManager @@ -70,7 +72,12 @@ object TestConstants { alias = "alice", color = Color(1, 2, 3), publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, - features = ByteVector.fromValidHex("0a8a"), + features = Features(Set( + ActivatedFeature(InitialRoutingSync, Optional), + ActivatedFeature(OptionDataLossProtect, Optional), + ActivatedFeature(ChannelRangeQueries, Optional), + ActivatedFeature(ChannelRangeQueriesExtended, Optional), + ActivatedFeature(VariableLengthOnion, Optional))), overrideFeatures = Map.empty, syncWhitelist = Set.empty, dustLimit = 1100 sat, @@ -154,7 +161,7 @@ object TestConstants { alias = "bob", color = Color(4, 5, 6), publicAddresses = NodeAddress.fromParts("localhost", 9732).get :: Nil, - features = ByteVector.fromValidHex("0200"), // variable_length_onion, no announcement + features = Features(Set(ActivatedFeature(VariableLengthOnion, Optional))), overrideFeatures = Map.empty, syncWhitelist = Set.empty, dustLimit = 1000 sat, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index a57f85b8da..33630a92db 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -490,8 +490,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: Long = 0, dustLimit: Satoshi = 0 sat, isFunder: Boolean = true, announceChannel: Boolean = true): Commitments = { - val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, ByteVector.empty) - val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty) + val localParams = LocalParams(randomKey.publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(randomKey.publicKey, dustLimit, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, Features.empty) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey) Commitments( ChannelVersion.STANDARD, @@ -512,8 +512,8 @@ object CommitmentsSpec { } def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, localNodeId: PublicKey, remoteNodeId: PublicKey, announceChannel: Boolean): Commitments = { - val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, ByteVector.empty) - val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, ByteVector.empty) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, isFunder = true, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, 0 sat, 1 msat, CltvExpiryDelta(144), 50, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, Features.empty) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32, 0, (toLocal + toRemote).truncateToSatoshi, randomKey.publicKey, remoteParams.fundingPubKey) Commitments( ChannelVersion.STANDARD, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala index 1a55dc2fae..9d317b5784 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzyPipe.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel import akka.actor.{Actor, ActorLogging, ActorRef, Stash} +import fr.acinq.eclair.Features import fr.acinq.eclair.channel.Commitments.msg2String import fr.acinq.eclair.wire.{Init, LightningMessage} import scodec.bits.ByteVector @@ -71,7 +72,7 @@ class FuzzyPipe(fuzzy: Boolean) extends Actor with Stash with ActorLogging { log.debug(f" X-${msg2String(msg)}%-6s--- B") case 'reconnect => log.debug("RECONNECTED") - val dummyInit = Init(ByteVector.empty) + val dummyInit = Init(Features.empty) a ! INPUT_RECONNECTED(self, dummyInit, dummyInit) b ! INPUT_RECONNECTED(self, dummyInit, dummyInit) context become connected(a, b, Random.nextInt(40)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 71f97588e4..6c0067045e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -21,6 +21,8 @@ import java.util.UUID import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Crypto, ScriptFlags, Transaction} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob, TestFeeEstimator} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeTargets @@ -82,8 +84,8 @@ trait StateTestsHelperMethods extends TestKitBase with FixtureTestSuite with Par val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty val pushMsat = if (tags.contains("no_push_msat")) 0.msat else TestConstants.pushMsat val (aliceParams, bobParams) = if(tags.contains("static_remotekey")) { - (Alice.channelParams.copy(features = hex"2000", localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))), - Bob.channelParams.copy(features = hex"2000", localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet)))) + (Alice.channelParams.copy(features = Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet))), + Bob.channelParams.copy(features = Features(Set(ActivatedFeature(StaticRemoteKey, Optional))), localPaymentBasepoint = Some(Helpers.getWalletPaymentBasepoint(wallet)))) } else { (Alice.channelParams, Bob.channelParams) } 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 50c2c186c6..7895565886 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 @@ -18,13 +18,15 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Block, Btc, ByteVector32, Satoshi} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.{MakeFundingTxResponse, TestWallet} import fr.acinq.eclair.channel.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{WAIT_FOR_FUNDING_INTERNAL, _} import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.{ByteVector, HexStringSyntax} @@ -48,12 +50,12 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS import com.softwaremill.quicklens._ val aliceNodeParams = TestConstants.Alice.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") + .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional)))) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) val bobNodeParams = TestConstants.Bob.nodeParams .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) - .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") + .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional)))) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) val setup = init(aliceNodeParams, bobNodeParams, wallet = noopWallet) 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 1a2c3ecfa5..0cdcd6ebe7 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 @@ -18,11 +18,13 @@ package fr.acinq.eclair.channel.states.a import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Block, Btc, ByteVector32} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.wire.{AcceptChannel, ChannelTlv, Error, Init, OpenChannel, TlvStream} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.{ByteVector, HexStringSyntax} @@ -40,7 +42,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui override def withFixture(test: OneArgTest): Outcome = { import com.softwaremill.quicklens._ val bobNodeParams = Bob.nodeParams - .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") + .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional)))) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) val setup = init(nodeParamsB = bobNodeParams) 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 1537ad5180..93bfa7efec 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 @@ -23,6 +23,7 @@ import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} +import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ @@ -1106,8 +1107,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features.hasFeature(StaticRemoteKey)) def aliceToRemoteScript() = { val toRemoteAmount = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.spec.toRemote diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index e44ec60047..27e1de0f08 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -20,12 +20,14 @@ import java.sql.Connection import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto, Satoshi} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.VariableLengthOnion import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.db.sqlite.SqliteUtils._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.PublicChannel import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.HexStringSyntax @@ -82,10 +84,10 @@ class SqliteNetworkDbSpec extends AnyFunSuite { val sqlite = TestConstants.sqliteInMemory() val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"") - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200") - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, hex"0200") - val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, hex"00") + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(Set(ActivatedFeature(VariableLengthOnion, Optional)))) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(Set(ActivatedFeature(VariableLengthOnion, Optional)))) + val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, Features.empty) assert(db.listNodes().toSet === Set.empty) db.addNode(node_1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala index 318b100f9f..dea6043e10 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePaymentsDbSpec.scala @@ -29,7 +29,6 @@ import fr.acinq.eclair.wire.{ChannelUpdate, UnknownNextPeer} import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, TestConstants, randomBytes32, randomBytes64, randomKey} import org.scalatest.funsuite.AnyFunSuite -import scala.compat.Platform import scala.concurrent.duration._ class SqlitePaymentsDbSpec extends AnyFunSuite { @@ -245,7 +244,7 @@ class SqlitePaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, completed_at, failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")) { statement => statement.setString(1, ps1.id.toString) statement.setString(2, ps1.parentId.toString) - statement.setString(3, ps1.externalId.get.toString) + statement.setString(3, ps1.externalId.get) statement.setBytes(4, ps1.paymentHash.toArray) statement.setLong(5, ps1.amount.toLong) statement.setBytes(6, ps1.recipientNodeId.value.toArray) @@ -258,7 +257,7 @@ class SqlitePaymentsDbSpec extends AnyFunSuite { using(connection.prepareStatement("INSERT INTO sent_payments (id, parent_id, external_id, payment_hash, amount_msat, target_node_id, created_at, payment_request) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => statement.setString(1, ps2.id.toString) statement.setString(2, ps2.parentId.toString) - statement.setString(3, ps2.externalId.get.toString) + statement.setString(3, ps2.externalId.get) statement.setBytes(4, ps2.paymentHash.toArray) statement.setLong(5, ps2.amount.toLong) statement.setBytes(6, ps2.recipientNodeId.value.toArray) @@ -394,8 +393,8 @@ class SqlitePaymentsDbSpec extends AnyFunSuite { db.updateOutgoingPayment(PaymentFailed(s3.id, s3.paymentHash, Nil, 310)) val ss3 = s3.copy(status = OutgoingPaymentStatus.Failed(Nil, 310)) assert(db.getOutgoingPayment(s3.id) === Some(ss3)) - db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(new RuntimeException("woops")), RemoteFailure(Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320)) - val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", Nil), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)))), 320)) + db.updateOutgoingPayment(PaymentFailed(s4.id, s4.paymentHash, Seq(LocalFailure(Seq(hop_ab), new RuntimeException("woops")), RemoteFailure(Seq(hop_ab, hop_bc), Sphinx.DecryptedFailurePacket(carol, UnknownNextPeer))), 320)) + val ss4 = s4.copy(status = OutgoingPaymentStatus.Failed(Seq(FailureSummary(FailureType.LOCAL, "woops", List(HopSummary(alice, bob, Some(ShortChannelId(42))))), FailureSummary(FailureType.REMOTE, "processing node does not know the next peer in the route", List(HopSummary(alice, bob, Some(ShortChannelId(42))), HopSummary(bob, carol, None)))), 320)) assert(db.getOutgoingPayment(s4.id) === Some(ss4)) // can't update again once it's in a final state 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 1485af72d9..ae250cf098 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.integration -import java.io.{File, PrintWriter} +import java.io.File import java.util.{Properties, UUID} import akka.actor.{ActorRef, ActorSystem} @@ -25,6 +25,7 @@ import com.google.common.net.HostAndPort import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} +import fr.acinq.eclair.Features._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient @@ -36,7 +37,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.db._ -import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.io.{Peer, PeerConnection} import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment._ @@ -102,7 +103,27 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS "eclair.router.broadcast-interval" -> "2 second", "eclair.auto-reconnect" -> false, "eclair.to-remote-delay-blocks" -> 144, - "eclair.multi-part-payment-expiry" -> "20 seconds").asJava) + "eclair.multi-part-payment-expiry" -> "20 seconds").asJava).withFallback(ConfigFactory.load()) + + val commonFeatures = ConfigFactory.parseMap(Map( + s"eclair.features.${OptionDataLossProtect.rfcName}" -> "optional", + s"eclair.features.${InitialRoutingSync.rfcName}" -> "optional", + s"eclair.features.${ChannelRangeQueries.rfcName}" -> "optional", + s"eclair.features.${ChannelRangeQueriesExtended.rfcName}" -> "optional", + s"eclair.features.${VariableLengthOnion.rfcName}" -> "optional", + s"eclair.features.${PaymentSecret.rfcName}" -> "optional", + s"eclair.features.${BasicMultiPartPayment.rfcName}" -> "optional" + ).asJava) + + val withWumbo = commonFeatures.withFallback(ConfigFactory.parseMap(Map( + s"eclair.features.${Wumbo.rfcName}" -> "optional", + "eclair.max-funding-satoshis" -> 500000000 + ).asJava)) + + val withStaticRemoteKey = commonFeatures.withFallback(ConfigFactory.parseMap(Map( + s"eclair.features.${StaticRemoteKey.rfcName}" -> "optional" + ).asJava)) + implicit val formats = DefaultFormats @@ -135,11 +156,7 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS def instantiateEclairNode(name: String, config: Config): Unit = { val datadir = new File(INTEGRATION_TMP_DIR, s"datadir-eclair-$name") datadir.mkdirs() - new PrintWriter(new File(datadir, "eclair.conf")) { - write(config.root().render()) - close() - } - implicit val system = ActorSystem(s"system-$name") + implicit val system = ActorSystem(s"system-$name", config) val setup = new Setup(datadir) val kit = Await.result(setup.bootstrap, 10 seconds) nodes = nodes + (name -> kit) @@ -152,17 +169,17 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS } test("starting 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.features" -> "028a8a", "eclair.channel-flags" -> 0).asJava).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.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).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.features" -> "0aaa8a", "eclair.max-funding-satoshis" -> 500000000, "eclair.trampoline-payments-enable" -> true, "eclair.max-payment-attempts" -> 15).asJava).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.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + 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, "eclair.max-payment-attempts" -> 15).asJava).withFallback(withStaticRemoteKey).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(commonConfig)) - instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.features" -> "0a8a8a", "eclair.max-funding-satoshis" -> 500000000).asJava).withFallback(commonConfig)) + instantiateEclairNode("F1", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F1", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085).asJava).withFallback(withWumbo).withFallback(commonConfig)) instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086).asJava).withFallback(commonConfig)) - instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.features" -> "028a8a", "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) + instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.expiry-delta-blocks" -> 137, "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig)) instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.expiry-delta-blocks" -> 138, "eclair.server.port" -> 29738, "eclair.api.port" -> 28088).asJava).withFallback(commonConfig)) instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.expiry-delta-blocks" -> 139, "eclair.server.port" -> 29739, "eclair.api.port" -> 28089).asJava).withFallback(commonConfig)) - instantiateEclairNode("F6", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F6", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090, "eclair.features" -> "2a8a").asJava).withFallback(commonConfig)) // supports optional option_static_remotekey + instantiateEclairNode("F6", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F6", "eclair.expiry-delta-blocks" -> 140, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig)) // supports optional option_static_remotekey instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 141, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091, "eclair.fee-base-msat" -> 1010, "eclair.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig)) // by default C has a normal payment handler, but this can be overriden in tests @@ -177,7 +194,7 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS nodeId = node2.nodeParams.nodeId, address_opt = Some(HostAndPort.fromParts(address.socketAddress.getHostString, address.socketAddress.getPort)) )) - sender.expectMsgAnyOf(10 seconds, "connected", "already connected") + sender.expectMsgAnyOf(10 seconds, PeerConnection.ConnectionResult.Connected, PeerConnection.ConnectionResult.AlreadyConnected) sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, fundingSatoshis = fundingSatoshis, @@ -324,7 +341,7 @@ class IntegrationSpec extends TestKitBaseClass with BitcoindService with AnyFunS nodeId = funder.nodeParams.nodeId, address_opt = Some(HostAndPort.fromParts(funder.nodeParams.publicAddresses.head.socketAddress.getHostString, funder.nodeParams.publicAddresses.head.socketAddress.getPort)) )) - sender.expectMsgAnyOf(10 seconds, "connected", "already connected", "reconnection in progress") + sender.expectMsgAnyOf(10 seconds, PeerConnection.ConnectionResult.Connected, PeerConnection.ConnectionResult.AlreadyConnected) sender.send(fundee.register, Forward(channelId, CMD_GETSTATE)) val fundeeState = sender.expectMsgType[State](max = 30 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala index a179943f74..ab70f68eb6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala @@ -22,6 +22,8 @@ import akka.actor.PoisonPill import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Block import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.{ChannelRangeQueries, VariableLengthOnion} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods @@ -107,26 +109,31 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi test("disconnect if authentication timeout") { f => import f._ val probe = TestProbe() + val origin = TestProbe() probe.watch(peerConnection) - probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) + probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref))) probe.expectTerminated(peerConnection, nodeParams.authTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here + origin.expectMsg(PeerConnection.ConnectionResult.AuthenticationFailed("authentication timed out")) } test("disconnect if init timeout") { f => import f._ val probe = TestProbe() + val origin = TestProbe() probe.watch(peerConnection) - probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) + probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref))) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref)) probe.expectTerminated(peerConnection, nodeParams.initTimeout / transport.testKitSettings.TestTimeFactor + 1.second) // we don't want dilated time here + origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("initialization timed out")) } test("disconnect if incompatible local features") { f => import f._ val probe = TestProbe() + val origin = TestProbe() probe.watch(transport.ref) - probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) + probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref))) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref)) transport.expectMsgType[TransportHandler.Listener] @@ -134,13 +141,15 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"0000 00050100000000".bits).require.value) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) + origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features")) } test("disconnect if incompatible global features") { f => import f._ val probe = TestProbe() + val origin = TestProbe() probe.watch(transport.ref) - probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) + probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref))) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref)) transport.expectMsgType[TransportHandler.Listener] @@ -148,6 +157,7 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.send(peerConnection, LightningMessageCodecs.initCodec.decode(hex"00050100000000 0000".bits).require.value) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) + origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible features")) } test("masks off MPP and PaymentSecret features") { f => @@ -164,22 +174,23 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi ) for ((configuredFeatures, sentFeatures) <- testCases) { - val nodeParams = TestConstants.Alice.nodeParams.copy(features = configuredFeatures.bytes) + val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(configuredFeatures)) val peerConnection = TestFSMRef(new PeerConnection(nodeParams, switchboard.ref, router.ref)) probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref)) transport.expectMsgType[TransportHandler.Listener] val init = transport.expectMsgType[wire.Init] - assert(init.features === sentFeatures.bytes) + assert(init.features.toByteVector === sentFeatures.bytes) } } test("disconnect if incompatible networks") { f => import f._ val probe = TestProbe() + val origin = TestProbe() probe.watch(transport.ref) - probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = None, transport_opt = Some(transport.ref))) + probe.send(peerConnection, PeerConnection.PendingAuth(connection.ref, Some(remoteNodeId), address, origin_opt = Some(origin.ref), transport_opt = Some(transport.ref))) transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId)) probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref)) transport.expectMsgType[TransportHandler.Listener] @@ -187,23 +198,24 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi transport.send(peerConnection, wire.Init(Bob.nodeParams.features, TlvStream(InitTlv.Networks(Block.LivenetGenesisBlock.hash :: Block.SegnetGenesisBlock.hash :: Nil)))) transport.expectMsgType[TransportHandler.ReadAck] probe.expectTerminated(transport.ref) + origin.expectMsg(PeerConnection.ConnectionResult.InitializationFailed("incompatible networks")) } test("sync if no whitelist is defined") { f => import f._ - val remoteInit = wire.Init(bin"10000000".bytes) // bob supports channel range queries + val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional)))) connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = true) } test("sync if whitelist contains peer", Tag("sync-whitelist-bob")) { f => import f._ - val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries and variable length onion + val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional), ActivatedFeature(VariableLengthOnion, Optional)))) connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = true) } test("don't sync if whitelist doesn't contain peer", Tag("sync-whitelist-random")) { f => import f._ - val remoteInit = wire.Init(bin"0000001010000000".bytes) // bob supports channel range queries + val remoteInit = wire.Init(Features(Set(ActivatedFeature(ChannelRangeQueries, Optional)))) // bob supports channel range queries connect(remoteNodeId, switchboard, router, connection, transport, peerConnection, peer, remoteInit, expectSync = false) } 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 a68bc2032b..1a13702cf7 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 @@ -25,6 +25,8 @@ import akka.testkit.{TestFSMRef, TestProbe} import com.google.common.net.HostAndPort import fr.acinq.bitcoin.{Btc, Script} import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.{Wumbo, StaticRemoteKey} import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} @@ -54,13 +56,13 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams - .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(hex"2200") - .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") + .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(Set(ActivatedFeature(StaticRemoteKey, Optional)))) + .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Set(ActivatedFeature(Wumbo, Optional)))) .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcement")) { - val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) aliceParams.db.network.addNode(bobAnnouncement) } @@ -93,7 +95,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe val probe = TestProbe() probe.send(peer, Peer.Init(Set.empty)) probe.send(peer, Peer.Connect(remoteNodeId, address_opt = None)) - probe.expectMsg(s"no address found") + probe.expectMsg(PeerConnection.ConnectionResult.NoAddressFound) } test("successfully connect to peer at user request") { f => @@ -133,7 +135,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe val mockAddress = NodeAddress.fromParts(mockServer.getInetAddress.getHostAddress, mockServer.getLocalPort).get // we put the server address in the node db - val ann = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) + val ann = NodeAnnouncement(randomBytes64, Features.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) nodeParams.db.network.addNode(ann) val probe = TestProbe() @@ -157,7 +159,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe connect(remoteNodeId, peer, peerConnection, channels = Set(ChannelCodecsSpec.normal)) probe.send(peer, Peer.Connect(remoteNodeId, None)) - probe.expectMsg("already connected") + probe.expectMsg(PeerConnection.ConnectionResult.AlreadyConnected) } test("handle disconnect in state CONNECTED") { f => @@ -259,7 +261,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe val probe = TestProbe() val fundingAmountBig = Btc(1).toSatoshi system.eventStream.subscribe(probe.ref, classOf[ChannelCreated]) - connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(hex"80000")) // Bob supports wumbo + connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(Features(Set(ActivatedFeature(Wumbo, Optional))))) // Bob supports wumbo assert(peer.stateData.channels.isEmpty) probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None)) @@ -287,7 +289,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTe import f._ val probe = TestProbe() - connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey + connect(remoteNodeId, peer, peerConnection, remoteInit = wire.Init(Features(Set(ActivatedFeature(StaticRemoteKey, Optional))))) // Bob supports option_static_remotekey probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None)) awaitCond(peer.stateData.channels.nonEmpty) peer.stateData.channels.foreach { case (_, channelRef) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala index f83aff0d8c..2e77cef202 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/ReconnectionTaskSpec.scala @@ -52,7 +52,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) if (test.tags.contains("with_node_announcements")) { - val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) aliceParams.db.network.addNode(bobAnnouncement) } @@ -163,7 +163,7 @@ class ReconnectionTaskSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike // we create a dummy tcp server and update bob's announcement to point to it val mockServer = new ServerSocket(0, 1, InetAddress.getLocalHost) // port will be assigned automatically val mockAddress = NodeAddress.fromParts(mockServer.getInetAddress.getHostAddress, mockServer.getLocalPort).get - val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) + val bobAnnouncement = NodeAnnouncement(randomBytes64, Features.empty, 1, remoteNodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", mockAddress :: Nil) nodeParams.db.network.addNode(bobAnnouncement) val peer = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index 9aa2887523..db74d9a2b9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -7,7 +7,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.TestWallet import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{NodeParams, TestKitBaseClass} +import fr.acinq.eclair.{Features, NodeParams, TestKitBaseClass} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -36,7 +36,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { val (probe, peer) = (TestProbe(), TestProbe()) val remoteNodeId = PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f") val remoteNodeAddress = NodeAddress.fromParts("127.0.0.1", 9735).get - nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, ByteVector.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil)) + nodeParams.db.network.addNode(NodeAnnouncement(ByteVector64.Zeroes, Features.empty, 0, remoteNodeId, Color(0, 0, 0), "alias", remoteNodeAddress :: Nil)) val switchboard = TestActorRef(new TestSwitchboard(nodeParams, remoteNodeId, peer)) probe.send(switchboard, Peer.Connect(remoteNodeId, None)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 223fb00d5e..e92c0ddd8c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -16,10 +16,11 @@ package fr.acinq.eclair.payment -import akka.actor.ActorSystem import akka.actor.Status.Failure import akka.testkit.{TestActorRef, TestKit, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair.FeatureSupport.Optional +import fr.acinq.eclair.Features.{BasicMultiPartPayment, ChannelRangeQueries, ChannelRangeQueriesExtended, InitialRoutingSync, OptionDataLossProtect, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} import fr.acinq.eclair.db.IncomingPaymentStatus @@ -30,10 +31,9 @@ import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM.HtlcPart import fr.acinq.eclair.payment.receive.{MultiPartPaymentFSM, PaymentHandler} import fr.acinq.eclair.payment.relay.CommandBuffer import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomKey} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, NodeParams, ShortChannelId, TestConstants, TestKitBaseClass, randomKey} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike -import scodec.bits.HexStringSyntax import scala.concurrent.duration._ @@ -43,9 +43,19 @@ import scala.concurrent.duration._ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { + val featuresWithMpp = Features(Set( + ActivatedFeature(OptionDataLossProtect, Optional), + ActivatedFeature(InitialRoutingSync, Optional), + ActivatedFeature(ChannelRangeQueries, Optional), + ActivatedFeature(ChannelRangeQueriesExtended, Optional), + ActivatedFeature(VariableLengthOnion, Optional), + ActivatedFeature(PaymentSecret, Optional), + ActivatedFeature(BasicMultiPartPayment, Optional) + )) + case class FixtureParam(nodeParams: NodeParams, defaultExpiry: CltvExpiry, commandBuffer: TestProbe, eventListener: TestProbe, sender: TestProbe) { lazy val normalHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, commandBuffer.ref)) - lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = hex"028a8a"), commandBuffer.ref)) + lazy val mppHandler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams.copy(features = featuresWithMpp), commandBuffer.ref)) } override def withFixture(test: OneArgTest): Outcome = { @@ -169,7 +179,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } { - val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = hex"028a8a"), TestProbe().ref)) + val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = false, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) @@ -185,7 +195,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } { - val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = hex"028a8a"), TestProbe().ref)) + val handler = TestActorRef[PaymentHandler](PaymentHandler.props(Alice.nodeParams.copy(enableTrampolinePayment = true, features = featuresWithMpp), TestProbe().ref)) sender.send(handler, ReceivePayment(Some(42 msat), "1 coffee")) val pr = sender.expectMsgType[PaymentRequest] assert(pr.features.allowMultiPart) @@ -328,7 +338,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } test("PaymentHandler should handle multi-part payment timeout") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = hex"028a8a") + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 200 millis, features = featuresWithMpp) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) // Partial payment missing additional parts. @@ -366,7 +376,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } test("PaymentHandler should handle multi-part payment success") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = hex"028a8a") + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 500 millis, features = featuresWithMpp) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 fast coffee")) @@ -412,7 +422,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike } test("PaymentHandler should handle multi-part payment timeout then success") { f => - val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = hex"028a8a") + val nodeParams = Alice.nodeParams.copy(multiPartPaymentExpiry = 250 millis, features = featuresWithMpp) val handler = TestActorRef[PaymentHandler](PaymentHandler.props(nodeParams, f.commandBuffer.ref)) f.sender.send(handler, ReceivePayment(Some(1000 msat), "1 coffee, no sugar")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index 61d34e0d69..5881fe6c34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.payment import java.util.UUID -import akka.actor.{ActorRef, ActorSystem} -import akka.testkit.{TestFSMRef, TestKit, TestProbe} +import akka.actor.ActorRef +import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.TestConstants.TestFeeEstimator import fr.acinq.eclair._ @@ -331,7 +331,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS val faultyLocalPayments = pending.filter { case (_, p) => getFirstHopShortChannelId(p) == faultyLocalChannelId } val faultyRemotePayment = pending.filter { case (_, p) => getFirstHopShortChannelId(p) != faultyLocalChannelId }.head faultyLocalPayments.keys.foreach(id => { - childPayFsm.send(payFsm, PaymentFailed(id, paymentHash, LocalFailure(RouteNotFound) :: Nil)) + childPayFsm.send(payFsm, PaymentFailed(id, paymentHash, LocalFailure(Nil, RouteNotFound) :: Nil)) }) childPayFsm.send(payFsm, PaymentFailed(faultyRemotePayment._1, paymentHash, UnreadableRemoteFailure(Nil) :: Nil)) @@ -345,7 +345,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS // New payments should be sent that match the failed amount. waitUntilAmountSent(f, faultyRemotePayment._2.finalPayload.amount + faultyLocalPayments.values.map(_.finalPayload.amount).sum) val stateData = payFsm.stateData.asInstanceOf[PaymentProgress] - assert(stateData.failures.toSet === Set(LocalFailure(RouteNotFound), UnreadableRemoteFailure(Nil))) + assert(stateData.failures.toSet === Set(LocalFailure(Nil, RouteNotFound), UnreadableRemoteFailure(Nil))) assert(stateData.pending.values.forall(p => getFirstHopShortChannelId(p) != faultyLocalChannelId)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala index c1eaa5ebb3..544771617e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/NodeRelayerSpec.scala @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.{Block, Crypto} import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, Upstream} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features} +import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.receive.MultiPartPaymentFSM import fr.acinq.eclair.payment.relay.{CommandBuffer, NodeRelayer, Origin, Relayer} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment @@ -219,7 +219,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id outgoingPayFSM.expectMsgType[SendMultiPartPayment] - outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(PaymentError.BalanceTooLow) :: Nil)) + outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, LocalFailure(Nil, PaymentError.BalanceTooLow) :: Nil)) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TemporaryNodeFailure), commit = true)))) commandBuffer.expectNoMsg(100 millis) eventListener.expectNoMsg(100 millis) @@ -234,7 +234,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { outgoingPayFSM.expectMsgType[SendMultiPartPayment] // If we're having a hard time finding routes, raising the fee/cltv will likely help. - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(Nil, RouteNotFound) :: RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, PermanentNodeFailure)) :: LocalFailure(Nil, RouteNotFound) :: Nil outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures)) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(TrampolineFeeInsufficient), commit = true)))) commandBuffer.expectNoMsg(100 millis) @@ -249,7 +249,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val outgoingPaymentId = outgoingPayFSM.expectMsgType[SendPaymentConfig].id outgoingPayFSM.expectMsgType[SendMultiPartPayment] - val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: LocalFailure(RouteNotFound) :: Nil + val failures = RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(outgoingNodeId, FinalIncorrectHtlcAmount(42 msat))) :: UnreadableRemoteFailure(Nil) :: LocalFailure(Nil, RouteNotFound) :: Nil outgoingPayFSM.send(nodeRelayer, PaymentFailed(outgoingPaymentId, paymentHash, failures)) incomingMultiPart.foreach(p => commandBuffer.expectMsg(CommandBuffer.CommandSend(p.add.channelId, CMD_FAIL_HTLC(p.add.id, Right(FinalIncorrectHtlcAmount(42 msat)), commit = true)))) commandBuffer.expectNoMsg(100 millis) @@ -335,7 +335,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // Receive an upstream multi-part payment. val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val features = Features(VariableLengthOnion.optional, PaymentSecret.mandatory, BasicMultiPartPayment.optional) + val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.mandatory, BasicMultiPartPayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount * 3), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(features)) incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload( incoming.innerPayload.amountToForward, outgoingAmount * 3, outgoingExpiry, outgoingNodeId, pr @@ -370,7 +370,7 @@ class NodeRelayerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { // Receive an upstream multi-part payment. val hints = List(List(ExtraHop(outgoingNodeId, ShortChannelId(42), feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(Features())) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey, "Some invoice", extraHops = hints, features = Some(PaymentRequestFeatures())) incomingMultiPart.foreach(incoming => relayer.send(nodeRelayer, incoming.copy(innerPayload = Onion.createNodeRelayToNonTrampolinePayload( incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr )))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index 60eef6bb0e..0c0add846d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -18,15 +18,16 @@ package fr.acinq.eclair.payment import java.util.UUID -import akka.actor.{ActorRef, ActorSystem} -import akka.testkit.{TestActorRef, TestKit, TestProbe} +import akka.actor.ActorRef +import akka.testkit.{TestActorRef, TestProbe} import fr.acinq.bitcoin.Block +import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features._ import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.channel.{Channel, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentPacketSpec._ -import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, Features} +import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment import fr.acinq.eclair.payment.send.PaymentInitiator._ import fr.acinq.eclair.payment.send.PaymentLifecycle.{SendPayment, SendPaymentToRoute} @@ -35,7 +36,7 @@ import fr.acinq.eclair.router.Router.{NodeHop, RouteParams} import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload} import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv} import fr.acinq.eclair.wire.{Onion, OnionCodecs, OnionTlv, TrampolineFeeInsufficient, _} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiryDelta, Features, LongToBtcAmount, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.HexStringSyntax @@ -50,8 +51,20 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike case class FixtureParam(nodeParams: NodeParams, initiator: TestActorRef[PaymentInitiator], payFsm: TestProbe, multiPartPayFsm: TestProbe, sender: TestProbe, eventListener: TestProbe) + val defaultTestFeatures = Features(Set( + ActivatedFeature(InitialRoutingSync, Optional), + ActivatedFeature(OptionDataLossProtect, Optional), + ActivatedFeature(ChannelRangeQueries, Optional), + ActivatedFeature(ChannelRangeQueriesExtended, Optional), + ActivatedFeature(VariableLengthOnion, Optional))) + + val featuresWithMpp = Features( + defaultTestFeatures.activated + + ActivatedFeature(PaymentSecret, Optional) + + ActivatedFeature(BasicMultiPartPayment, Optional)) + override def withFixture(test: OneArgTest): Outcome = { - val features = if (test.tags.contains("mpp_disabled")) hex"0a8a" else hex"028a8a" + val features = if (test.tags.contains("mpp_disabled")) defaultTestFeatures else featuresWithMpp val nodeParams = TestConstants.Alice.nodeParams.copy(features = features) val (sender, payFsm, multiPartPayFsm) = (TestProbe(), TestProbe(), TestProbe()) val eventListener = TestProbe() @@ -88,7 +101,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("reject payment with unknown mandatory feature") { f => import f._ val unknownFeature = 42 - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(unknownFeature))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(PaymentRequestFeatures(unknownFeature))) val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] @@ -123,7 +136,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward single-part payment when multi-part deactivated", Tag("mpp_disabled")) { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some MPP invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some MPP invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) val req = SendPaymentRequest(finalAmount, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] @@ -133,7 +146,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) val req = SendPaymentRequest(finalAmount + 100.msat, paymentHash, c, 1, CltvExpiryDelta(42), Some(pr)) sender.send(initiator, req) val id = sender.expectMsgType[UUID] @@ -143,7 +156,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward multi-part payment with pre-defined route") { f => import f._ - val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) + val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional))) val req = SendPaymentToRouteRequest(finalAmount / 2, finalAmount, None, None, pr, Channel.MIN_CLTV_EXPIRY_DELTA, Seq(a, b, c), None, 0 msat, CltvExpiryDelta(0), Nil) sender.send(initiator, req) val payment = sender.expectMsgType[SendPaymentToRouteResponse] @@ -157,7 +170,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("forward trampoline payment") { f => import f._ - val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) + val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) val ignoredRoutingHints = List(List(ExtraHop(b, channelUpdate_bc.shortChannelId, feeBase = 10 msat, feeProportionalMillionths = 1, cltvExpiryDelta = CltvExpiryDelta(12)))) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features), extraHops = ignoredRoutingHints) val trampolineFees = 21000 msat @@ -229,7 +242,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ // This is disabled because it would let the trampoline node steal the whole payment (if malicious). val routingHints = List(List(PaymentRequest.ExtraHop(b, channelUpdate_bc.shortChannelId, 10 msat, 100, CltvExpiryDelta(144)))) - val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional) + val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional) val pr = PaymentRequest(Block.RegtestGenesisBlock.hash, None, paymentHash, priv_a.privateKey, "#abittooreckless", None, None, routingHints, features = Some(features)) val trampolineFees = 21000 msat val req = SendTrampolinePaymentRequest(finalAmount, pr, b, Seq((trampolineFees, CltvExpiryDelta(12))), CltvExpiryDelta(9)) @@ -237,7 +250,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val id = sender.expectMsgType[UUID] val fail = sender.expectMsgType[PaymentFailed] assert(fail.id === id) - assert(fail.failures === LocalFailure(PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) + assert(fail.failures === LocalFailure(Nil, PaymentError.TrampolineLegacyAmountLessInvoice) :: Nil) multiPartPayFsm.expectNoMsg(50 millis) payFsm.expectNoMsg(50 millis) @@ -245,7 +258,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment") { f => import f._ - val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) + val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features)) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9)) @@ -275,7 +288,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("retry trampoline payment and fail") { f => import f._ - val features = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) + val features = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional) val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, priv_c.privateKey, "Some phoenix invoice", features = Some(features)) val trampolineAttempts = (21000 msat, CltvExpiryDelta(12)) :: (25000 msat, CltvExpiryDelta(24)) :: Nil val req = SendTrampolinePaymentRequest(finalAmount, pr, b, trampolineAttempts, CltvExpiryDelta(9)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index e9602cb326..dac828049a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -38,7 +38,7 @@ import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentConfig, SendPay import fr.acinq.eclair.payment.send.PaymentLifecycle import fr.acinq.eclair.payment.send.PaymentLifecycle._ import fr.acinq.eclair.router.Announcements.{makeChannelUpdate, makeNodeAnnouncement} -import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, ExcludeChannel, FinalizeRoute, RouteParams, RouteRequest, RouteResponse} +import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.Onion.FinalLegacyPayload @@ -152,7 +152,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) - routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, update_cd)), Set.empty, Set.empty)) + routerForwarder.send(paymentFSM, RouteResponse(Route(defaultAmountMsat, Seq(ChannelHop(c, d, update_cd))) :: Nil)) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) } @@ -178,7 +178,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.Pending)) - routerForwarder.send(paymentFSM, RouteResponse(Seq(ChannelHop(c, d, update_cd)), Set(a, b), Set.empty)) + routerForwarder.send(paymentFSM, RouteResponse(Route(defaultAmountMsat, Seq(ChannelHop(c, d, update_cd))) :: Nil)) val Transition(_, WAITING_FOR_ROUTE, WAITING_FOR_PAYMENT_COMPLETE) = monitor.expectMsgClass(classOf[Transition[_]]) sender.send(paymentFSM, UpdateFailHtlc(randomBytes32, 0, randomBytes(Sphinx.FailurePacket.PacketLength))) @@ -198,7 +198,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) routerForwarder.forward(routerFixture.router, routeRequest) - assert(sender.expectMsgType[PaymentFailed].failures === LocalFailure(RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures === LocalFailure(Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -212,7 +212,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val Transition(_, WAITING_FOR_REQUEST, WAITING_FOR_ROUTE) = monitor.expectMsgClass(classOf[Transition[_]]) routerForwarder.forward(routerFixture.router, routeRequest) - val Seq(LocalFailure(RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures + val Seq(LocalFailure(Nil, RouteNotFound)) = sender.expectMsgType[PaymentFailed].failures awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -225,10 +225,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, _, ignoreNodes1, _, route) = paymentFSM.stateData + assert(ignoreNodes1.isEmpty) register.expectMsg(ForwardShortId(channelId_ab, cmd1)) sender.send(paymentFSM, Relayer.ForwardRemoteFail(UpdateFailHtlc(ByteVector32.Zeroes, 0, randomBytes32), defaultOrigin, UpdateAddHtlc(ByteVector32.Zeroes, 0, defaultAmountMsat, defaultPaymentHash, defaultExpiry, TestConstants.emptyOnionPacket))) // unparsable message @@ -237,15 +238,16 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, ignoreNodes = Set(c), ignoreChannels = Set.empty)) // let's simulate a response by the router with another route - sender.send(paymentFSM, RouteResponse(hops, Set(c), Set.empty)) + sender.send(paymentFSM, RouteResponse(route :: Nil)) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd2, _, _, _, _, _) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd2, _, _, ignoreNodes2, _, _) = paymentFSM.stateData + assert(ignoreNodes2 === Set(c)) // and reply a 2nd time with an unparsable failure register.expectMsg(ForwardShortId(channelId_ab, cmd2)) sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash)) // unparsable message // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === UnreadableRemoteFailure(hops) :: UnreadableRemoteFailure(hops) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures === UnreadableRemoteFailure(route.hops) :: UnreadableRemoteFailure(route.hops) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) // after last attempt the payment is failed } @@ -257,7 +259,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -279,7 +281,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -300,11 +302,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val request = SendPayment(d, FinalLegacyPayload(defaultAmountMsat, defaultExpiry), 2) sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route) = paymentFSM.stateData register.expectMsg(ForwardShortId(channelId_ab, cmd1)) val failure = TemporaryChannelFailure(update_bc) @@ -319,7 +321,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) // we allow 2 tries, so we send a 2nd request to the router - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil) } test("payment failed (Update)") { routerFixture => @@ -330,11 +332,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData register.expectMsg(ForwardShortId(channelId_ab, cmd1)) // we change the cltv expiry @@ -351,7 +353,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // router answers with a new route, taking into account the new update awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd2, _, sharedSecrets2, _, _, hops2) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd2, _, sharedSecrets2, _, _, route2) = paymentFSM.stateData register.expectMsg(ForwardShortId(channelId_ab, cmd2)) // we change the cltv expiry one more time @@ -370,7 +372,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(routerFixture.router) // this time the router can't find a route: game over - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(hops2, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(route2.hops, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -388,7 +390,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = assistedRoutes, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(routerFixture.router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) @@ -426,11 +428,11 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.send(paymentFSM, request) awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE && nodeParams.db.payments.getOutgoingPayment(id).exists(_.status === OutgoingPaymentStatus.Pending)) - val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + val WaitingForRoute(_, _, Nil, _, _) = paymentFSM.stateData routerForwarder.expectMsg(RouteRequest(nodeParams.nodeId, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) - val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, hops) = paymentFSM.stateData + val WaitingForComplete(_, _, cmd1, Nil, sharedSecrets1, _, _, route1) = paymentFSM.stateData register.expectMsg(ForwardShortId(channelId_ab, cmd1)) sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) @@ -441,7 +443,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil) + assert(sender.expectMsgType[PaymentFailed].failures === RemoteFailure(route1.hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(Nil, RouteNotFound) :: Nil) awaitCond(nodeParams.db.payments.getOutgoingPayment(id).exists(_.status.isInstanceOf[OutgoingPaymentStatus.Failed])) } @@ -489,7 +491,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { // \--(5)--> g val (priv_g, priv_funding_g) = (randomKey, randomKey) val (g, funding_g) = (priv_g.publicKey, priv_funding_g.publicKey) - val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil, hex"0200") + val ann_g = makeNodeAnnouncement(priv_g, "node-G", Color(-30, 10, -50), Nil, TestConstants.Bob.nodeParams.features) val channelId_bg = ShortChannelId(420000, 5, 0) val chan_bg = channelAnnouncement(channelId_bg, priv_b, priv_g, priv_funding_b, priv_funding_g) val channelUpdate_bg = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, g, channelId_bg, CltvExpiryDelta(9), htlcMinimumMsat = 0 msat, feeBaseMsat = 0 msat, feeProportionalMillionths = 0, htlcMaximumMsat = 500000000 msat) @@ -530,9 +532,55 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } test("filter errors properly") { _ => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = Seq( + LocalFailure(Nil, RouteNotFound), + RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(ChannelHop(a, b, update_ab) :: Nil, AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)), + LocalFailure(Nil, RouteNotFound) + ) val filtered = PaymentFailure.transformForUser(failures) - assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) + val expected = Seq( + LocalFailure(Nil, RouteNotFound), + RemoteFailure(ChannelHop(a, b, update_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)), + LocalFailure(ChannelHop(a, b, update_ab) :: Nil, ChannelUnavailable(ByteVector32.Zeroes)) + ) + assert(filtered === expected) + } + + test("ignore failed nodes/channels") { _ => + val route_abcd = ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: Nil + val testCases = Seq( + // local failures -> ignore first channel if there is one + (LocalFailure(Nil, RouteNotFound), Set.empty, Set.empty), + (LocalFailure(NodeHop(a, b, CltvExpiryDelta(144), 0 msat) :: NodeHop(b, c, CltvExpiryDelta(144), 0 msat) :: Nil, RouteNotFound), Set.empty, Set.empty), + (LocalFailure(route_abcd, new RuntimeException("fatal")), Set.empty, Set(ChannelDesc(channelId_ab, a, b))), + // remote failure from final recipient -> all intermediate nodes behaved correctly + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(d, IncorrectOrUnknownPaymentDetails(100 msat, 42))), Set.empty, Set.empty), + // remote failures from intermediate nodes -> depending on the failure, ignore either the failing node or its outgoing channel + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentNodeFailure)), Set(b), Set.empty), + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), Set(c), Set.empty), + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, PermanentChannelFailure)), Set.empty, Set(ChannelDesc(channelId_bc, b, c))), + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(c, UnknownNextPeer)), Set.empty, Set(ChannelDesc(channelId_cd, c, d))), + (RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, update_bc))), Set.empty, Set.empty), + // unreadable remote failures -> blacklist all nodes except our direct peer and the final recipient + (UnreadableRemoteFailure(ChannelHop(a, b, update_ab) :: Nil), Set.empty, Set.empty), + (UnreadableRemoteFailure(ChannelHop(a, b, update_ab) :: ChannelHop(b, c, update_bc) :: ChannelHop(c, d, update_cd) :: ChannelHop(d, e, null) :: Nil), Set(c, d), Set.empty) + ) + + for ((failure, expectedNodes, expectedChannels) <- testCases) { + val (ignoreNodes, ignoreChannels) = PaymentFailure.updateIgnored(failure, Set.empty, Set.empty) + assert(ignoreNodes === expectedNodes, failure) + assert(ignoreChannels === expectedChannels, failure) + } + + val failures = Seq( + RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(c, TemporaryNodeFailure)), + RemoteFailure(route_abcd, Sphinx.DecryptedFailurePacket(b, UnknownNextPeer)), + LocalFailure(route_abcd, new RuntimeException("fatal")) + ) + val (ignoreNodes, ignoreChannels) = PaymentFailure.updateIgnored(failures, Set.empty, Set.empty) + assert(ignoreNodes === Set(c)) + assert(ignoreChannels === Set(ChannelDesc(channelId_ab, a, b), ChannelDesc(channelId_bc, b, c))) } test("disable database and events") { routerFixture => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 361438e507..825925bd05 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -21,17 +21,18 @@ import java.util.UUID import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet} +import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features._ import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.IncomingPacket.{ChannelRelayPacket, FinalPacket, NodeRelayPacket, decrypt} import fr.acinq.eclair.payment.OutgoingPacket._ -import fr.acinq.eclair.payment.PaymentRequest.Features +import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.wire.Onion.{FinalLegacyPayload, FinalTlvPayload, RelayLegacyPayload} import fr.acinq.eclair.wire.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey} +import fr.acinq.eclair.{ActivatedFeature, CltvExpiry, CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, UInt64, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -69,11 +70,11 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(onion.packet.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val features = if (legacy) ByteVector.empty else variableLengthOnionFeature + val features = if (legacy) Features.empty else variableLengthOnionFeature testPeelOnion(onion.packet, features) } - def testPeelOnion(packet_b: OnionRoutingPacket, features: ByteVector): Unit = { + def testPeelOnion(packet_b: OnionRoutingPacket, features: Features): Unit = { val add_b = UpdateAddHtlc(randomBytes32, 0, amount_ab, paymentHash, expiry_ab, packet_b) val Right(relay_b@ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, features) assert(add_b2 === add_b) @@ -129,7 +130,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - testPeelOnion(add.onion, ByteVector.empty) + testPeelOnion(add.onion, Features.empty) } test("build a command with no hops") { @@ -141,7 +142,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // let's peel the onion val add_b = UpdateAddHtlc(randomBytes32, 0, finalAmount, paymentHash, finalExpiry, add.onion) - val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty) + val Right(FinalPacket(add_b2, payload_b)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 === add_b) assert(payload_b.amount === finalAmount) assert(payload_b.totalAmount === finalAmount) @@ -164,7 +165,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(firstExpiry === expiry_ab) val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet) - val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(add_b2, payload_b, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) assert(add_b2 === add_b) assert(payload_b === RelayLegacyPayload(channelUpdate_bc.shortChannelId, amount_bc, expiry_bc)) @@ -215,7 +216,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { // a -> b -> c d -> e val routingHints = List(List(PaymentRequest.ExtraHop(randomKey.publicKey, ShortChannelId(42), 10 msat, 100, CltvExpiryDelta(144)))) - val invoiceFeatures = Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional) + val invoiceFeatures = PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional) val invoice = PaymentRequest(Block.RegtestGenesisBlock.hash, Some(finalAmount), paymentHash, priv_a.privateKey, "#reckless", None, None, routingHints, features = Some(invoiceFeatures)) val (amount_ac, expiry_ac, trampolineOnion) = buildTrampolineToLegacyPacket(invoice, trampolineHops, FinalLegacyPayload(finalAmount, finalExpiry)) assert(amount_ac === amount_bc) @@ -226,7 +227,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { assert(firstExpiry === expiry_ab) val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c) val Right(NodeRelayPacket(_, outer_c, inner_c, packet_d)) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature) @@ -279,7 +280,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount * 2, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet.copy(payload = trampolineOnion.packet.payload.reverse))) val add_b = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(add_b, priv_b.privateKey, Features.empty) val add_c = UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c) val Left(failure) = decrypt(add_c, priv_c.privateKey, variableLengthOnionFeature) assert(failure.isInstanceOf[InvalidOnionHmac]) @@ -295,28 +296,28 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt when variable length onion is disabled") { val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalTlvPayload(TlvStream(AmountToForward(finalAmount), OutgoingCltv(finalExpiry)))) val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet) - val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty) // tlv payload requires setting the variable-length onion feature bit + val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) // tlv payload requires setting the variable-length onion feature bit assert(failure === InvalidRealm) } test("fail to decrypt at the final node when amount has been modified by next-to-last node") { val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry)) val add = UpdateAddHtlc(randomBytes32, 1, firstAmount - 100.msat, paymentHash, firstExpiry, onion.packet) - val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty) + val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure === FinalIncorrectHtlcAmount(firstAmount - 100.msat)) } test("fail to decrypt at the final node when expiry has been modified by next-to-last node") { val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, hops.take(1), FinalLegacyPayload(finalAmount, finalExpiry)) val add = UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry - CltvExpiryDelta(12), onion.packet) - val Left(failure) = decrypt(add, priv_b.privateKey, ByteVector.empty) + val Left(failure) = decrypt(add, priv_b.privateKey, Features.empty) assert(failure === FinalIncorrectCltvExpiry(firstExpiry - CltvExpiryDelta(12))) } test("fail to decrypt at the final trampoline node when amount has been modified by next-to-last trampoline") { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet)) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature) // c forwards the trampoline payment to d. val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d)) @@ -331,7 +332,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final trampoline node when expiry has been modified by next-to-last trampoline") { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createMultiPartPayload(finalAmount, finalAmount, finalExpiry, paymentSecret)) val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet)) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature) // c forwards the trampoline payment to d. val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d)) @@ -346,7 +347,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at the final trampoline node when payment secret is missing") { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet)) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty) val Right(NodeRelayPacket(_, _, _, packet_d)) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature) // c forwards the trampoline payment to d. val (amount_d, expiry_d, onion_d) = buildPacket(Sphinx.PaymentPacket)(paymentHash, ChannelHop(c, d, channelUpdate_cd) :: Nil, Onion.createTrampolinePayload(amount_cd, amount_cd, expiry_cd, randomBytes32, packet_d)) @@ -360,7 +361,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at intermediate trampoline node when amount is invalid") { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet)) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC amount matches the onion outer amount. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc - 100.msat, paymentHash, expiry_bc, packet_c), priv_c.privateKey, variableLengthOnionFeature) assert(failure === FinalIncorrectHtlcAmount(amount_bc - 100.msat)) @@ -369,7 +370,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { test("fail to decrypt at intermediate trampoline node when expiry is invalid") { val (amount_ac, expiry_ac, trampolineOnion) = buildPacket(Sphinx.TrampolinePacket)(paymentHash, trampolineHops, Onion.createSinglePartPayload(finalAmount, finalExpiry)) // no payment secret val (firstAmount, firstExpiry, onion) = buildPacket(Sphinx.PaymentPacket)(paymentHash, trampolineChannelHops, Onion.createTrampolinePayload(amount_ac, amount_ac, expiry_ac, randomBytes32, trampolineOnion.packet)) - val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, ByteVector.empty) + val Right(ChannelRelayPacket(_, _, packet_c)) = decrypt(UpdateAddHtlc(randomBytes32, 1, firstAmount, paymentHash, firstExpiry, onion.packet), priv_b.privateKey, Features.empty) // A trampoline relay is very similar to a final node: it can validate that the HTLC expiry matches the onion outer expiry. val Left(failure) = decrypt(UpdateAddHtlc(randomBytes32, 2, amount_bc, paymentHash, expiry_bc - CltvExpiryDelta(12), packet_c), priv_c.privateKey, variableLengthOnionFeature) assert(failure === FinalIncorrectCltvExpiry(expiry_bc - CltvExpiryDelta(12))) @@ -379,7 +380,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { object PaymentPacketSpec { - val variableLengthOnionFeature = ByteVector.fromLong(1L << VariableLengthOnion.optional) + val variableLengthOnionFeature = Features(Set(ActivatedFeature(VariableLengthOnion, Optional))) /** Build onion from arbitrary tlv stream (potentially invalid). */ def buildTlvOnion[T <: Onion.PacketType](packetType: Sphinx.OnionRoutingPacket[T])(nodes: Seq[PublicKey], payloads: Seq[TlvStream[OnionTlv]], associatedData: ByteVector32): OnionRoutingPacket = { 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 ca48c45cf1..9ce4f2327e 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 @@ -380,23 +380,23 @@ class PaymentRequestSpec extends AnyFunSuite { case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule" val featureBits = Map( - Features(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - Features(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00010100001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), - Features(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), - Features(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - Features(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - Features(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), - Features(bin" 0000110000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 00000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 00010100001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = true, areSupported = true), + PaymentRequestFeatures(bin" 00011000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 00101000001000000000") -> Result(allowMultiPart = true, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 01000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 0000010000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 0000011000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true), + PaymentRequestFeatures(bin" 0000110000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), // those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit) - Features(bin" 0000100000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), - Features(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), - Features(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), - Features(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), - Features(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), - Features(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false) + PaymentRequestFeatures(bin" 0000100000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false), + PaymentRequestFeatures(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false) ) for ((features, res) <- featureBits) { @@ -419,14 +419,14 @@ class PaymentRequestSpec extends AnyFunSuite { ) for ((bitmask, featureBytes) <- testCases) { - assert(Features(bitmask).toByteVector === featureBytes) + assert(PaymentRequestFeatures(bitmask).toByteVector === featureBytes) } } test("payment secret") { val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice") assert(pr.paymentSecret.isDefined) - assert(pr.features === Features(PaymentSecret.optional, VariableLengthOnion.optional)) + assert(pr.features === PaymentRequestFeatures(PaymentSecret.optional, VariableLengthOnion.optional)) assert(!pr.features.requirePaymentSecret) val pr1 = PaymentRequest.read(PaymentRequest.write(pr)) @@ -443,7 +443,7 @@ class PaymentRequestSpec extends AnyFunSuite { // A multi-part invoice must use a payment secret. assertThrows[IllegalArgumentException]( - PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", features = Some(Features(BasicMultiPartPayment.optional, VariableLengthOnion.optional))) + PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "MPP without secrets", features = Some(PaymentRequestFeatures(BasicMultiPartPayment.optional, VariableLengthOnion.optional))) ) } @@ -451,11 +451,11 @@ class PaymentRequestSpec extends AnyFunSuite { val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice") assert(!pr.features.allowTrampoline) - val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, TrampolinePayment.optional))) + val pr1 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, TrampolinePayment.optional))) assert(!pr1.features.allowMultiPart) assert(pr1.features.allowTrampoline) - val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(Features(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional))) + val pr2 = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(123 msat), ByteVector32.One, priv, "Some invoice", features = Some(PaymentRequestFeatures(VariableLengthOnion.optional, PaymentSecret.optional, BasicMultiPartPayment.optional, TrampolinePayment.optional))) assert(pr2.features.allowMultiPart) assert(pr2.features.allowTrampoline) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index 9ebe967fc4..7367dd05b6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -27,11 +27,9 @@ import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, ShortChannelId, randomKey} -import org.json4s -import org.json4s.JsonAST.{JString, JValue} +import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, ShortChannelId, randomKey} +import org.json4s.JsonAST.JString import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.ByteVector import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} @@ -104,8 +102,8 @@ object AnnouncementsBatchValidationSpec { def makeChannelAnnouncement(c: SimulatedChannel)(implicit extendedBitcoinClient: ExtendedBitcoinClient, ec: ExecutionContext): ChannelAnnouncement = { val (blockHeight, txIndex) = Await.result(extendedBitcoinClient.getTransactionShortId(c.fundingTx.txid), 10 seconds) val shortChannelId = ShortChannelId(blockHeight, txIndex, c.fundingOutputIndex) - val (channelAnnNodeSig1, channelAnnBitcoinSig1) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key, c.node2Key.publicKey, c.node1FundingKey, c.node2FundingKey.publicKey, ByteVector.empty) - val (channelAnnNodeSig2, channelAnnBitcoinSig2) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node2Key, c.node1Key.publicKey, c.node2FundingKey, c.node1FundingKey.publicKey, ByteVector.empty) + val (channelAnnNodeSig1, channelAnnBitcoinSig1) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key, c.node2Key.publicKey, c.node1FundingKey, c.node2FundingKey.publicKey, Features.empty) + val (channelAnnNodeSig2, channelAnnBitcoinSig2) = Announcements.signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node2Key, c.node1Key.publicKey, c.node2FundingKey, c.node1FundingKey.publicKey, Features.empty) val channelAnnouncement = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, c.node1Key.publicKey, c.node2Key.publicKey, c.node1FundingKey.publicKey, c.node2FundingKey.publicKey, channelAnnNodeSig1, channelAnnNodeSig2, channelAnnBitcoinSig1, channelAnnBitcoinSig2) channelAnnouncement } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 70afe5da65..b588b16436 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -40,8 +40,8 @@ class AnnouncementsSpec extends AnyFunSuite { test("create valid signed channel announcement") { val (node_a, node_b, bitcoin_a, bitcoin_b) = (randomKey, randomKey, randomKey, randomKey) - val (node_a_sig, bitcoin_a_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a, node_b.publicKey, bitcoin_a, bitcoin_b.publicKey, ByteVector.empty) - val (node_b_sig, bitcoin_b_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_b, node_a.publicKey, bitcoin_b, bitcoin_a.publicKey, ByteVector.empty) + val (node_a_sig, bitcoin_a_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a, node_b.publicKey, bitcoin_a, bitcoin_b.publicKey, Features.empty) + val (node_b_sig, bitcoin_b_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_b, node_a.publicKey, bitcoin_b, bitcoin_a.publicKey, Features.empty) val ann = makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42L), node_a.publicKey, node_b.publicKey, bitcoin_a.publicKey, bitcoin_b.publicKey, node_a_sig, node_b_sig, bitcoin_a_sig, bitcoin_b_sig) assert(checkSigs(ann)) assert(checkSigs(ann.copy(nodeId1 = randomKey.publicKey)) === false) @@ -49,7 +49,7 @@ class AnnouncementsSpec extends AnyFunSuite { test("create valid signed node announcement") { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses, Alice.nodeParams.features) - assert(Features.hasFeature(ann.features, Features.VariableLengthOnion, Some(FeatureSupport.Optional))) + assert(ann.features.hasFeature(Features.VariableLengthOnion, Some(FeatureSupport.Optional))) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala index fd0059536a..6ec661bcec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/BaseRouterSpec.scala @@ -61,13 +61,13 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val (funding_a, funding_b, funding_c, funding_d, funding_e, funding_f, funding_g, funding_h) = (priv_funding_a.publicKey, priv_funding_b.publicKey, priv_funding_c.publicKey, priv_funding_d.publicKey, priv_funding_e.publicKey, priv_funding_f.publicKey, priv_funding_g.publicKey, priv_funding_h.publicKey) // in the tests we are 'a', we don't define a node_a, it will be generated automatically when the router validates the first channel - val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, hex"") - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200") - val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, hex"00") - val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, hex"00") - val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, hex"00") - val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, hex"00") - val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, hex"00") + val node_b = makeNodeAnnouncement(priv_b, "node-B", Color(50, 99, -80), Nil, Features.empty) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) + val node_d = makeNodeAnnouncement(priv_d, "node-D", Color(-120, -20, 60), Nil, Features.empty) + val node_e = makeNodeAnnouncement(priv_e, "node-E", Color(-50, 0, 10), Nil, Features.empty) + val node_f = makeNodeAnnouncement(priv_f, "node-F", Color(30, 10, -50), Nil, Features.empty) + val node_g = makeNodeAnnouncement(priv_g, "node-G", Color(30, 10, -50), Nil, Features.empty) + val node_h = makeNodeAnnouncement(priv_h, "node-H", Color(30, 10, -50), Nil, Features.empty) val channelId_ab = ShortChannelId(420000, 1, 0) val channelId_bc = ShortChannelId(420000, 2, 0) @@ -77,8 +77,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi val channelId_gh = ShortChannelId(420000, 6, 0) def channelAnnouncement(shortChannelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { - val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, ByteVector.empty) - val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, ByteVector.empty) + val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, Features.empty) + val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, Features.empty) makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, shortChannelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } @@ -152,7 +152,7 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_gh)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_hg)) // then private channels - sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32, channelId_ag, g, None, update_ag, CommitmentsSpec.makeCommitments(300000000 msat, 100000000 msat, a, g, announceChannel = false))) + sender.send(router, LocalChannelUpdate(sender.ref, randomBytes32, channelId_ag, g, None, update_ag, CommitmentsSpec.makeCommitments(30000000 msat, 8000000 msat, a, g, announceChannel = false))) // watcher receives the get tx requests watcher.expectMsg(ValidateRequest(chan_ab)) watcher.expectMsg(ValidateRequest(chan_bc)) @@ -213,8 +213,8 @@ abstract class BaseRouterSpec extends TestKitBaseClass with FixtureAnyFunSuiteLi object BaseRouterSpec { def channelAnnouncement(channelId: ShortChannelId, node1_priv: PrivateKey, node2_priv: PrivateKey, funding1_priv: PrivateKey, funding2_priv: PrivateKey) = { - val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, ByteVector.empty) - val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, ByteVector.empty) + val (node1_sig, funding1_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv, node2_priv.publicKey, funding1_priv, funding2_priv.publicKey, Features.empty) + val (node2_sig, funding2_sig) = signChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node2_priv, node1_priv.publicKey, funding2_priv, funding1_priv.publicKey, Features.empty) makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, channelId, node1_priv.publicKey, node2_priv.publicKey, funding1_priv.publicKey, funding2_priv.publicKey, node1_sig, node2_sig, funding1_sig, funding2_sig) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index 0c37c422ed..ce55f5fa3f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -209,6 +209,28 @@ class GraphSpec extends AnyFunSuite { assert(!withoutE.containsEdge(descFromNodes(6, b, e))) } + test("update edge balance") { + val edgeAB = makeEdge(1L, a, b, 0 msat, 0, capacity = 1500 sat, balance_opt = Some(300000 msat)) + val edgeBC = makeEdge(2L, b, c, 0 msat, 0, capacity = 500 sat, balance_opt = None) + val edgeAD = makeEdge(3L, a, d, 0 msat, 0, capacity = 1000 sat, balance_opt = Some(50000 msat)) + val edgeDC = makeEdge(4L, d, c, 0 msat, 0, capacity = 800 sat, balance_opt = Some(50000 msat)) + val graph = DirectedGraph(Seq(edgeAB, edgeAD, edgeBC, edgeDC)) + + assert(graph.edgesOf(a).toSet === Set(edgeAB, edgeAD)) + assert(graph.getIncomingEdgesOf(a) === Nil) + assert(graph.edgesOf(c) === Nil) + assert(graph.getIncomingEdgesOf(c).toSet === Set(edgeBC, edgeDC)) + + val edgeAB1 = edgeAB.copy(balance_opt = Some(200000 msat)) + val edgeBC1 = edgeBC.copy(balance_opt = Some(150000 msat)) + val graph1 = graph.addEdge(edgeAB1).addEdge(edgeBC1) + + assert(graph1.edgesOf(a).toSet === Set(edgeAB1, edgeAD)) + assert(graph1.getIncomingEdgesOf(a) === Nil) + assert(graph1.edgesOf(c) === Nil) + assert(graph1.getIncomingEdgesOf(c).toSet === Set(edgeBC1, edgeDC)) + } + def descFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): ChannelDesc = makeEdge(shortChannelId, a, b, 0 msat, 0).desc def edgeFromNodes(shortChannelId: Long, a: PublicKey, b: PublicKey): GraphEdge = makeEdge(shortChannelId, a, b, 0 msat, 0) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala index 0a1ecc1c77..cd4591d79a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouteCalculationSpec.scala @@ -26,7 +26,7 @@ import fr.acinq.eclair.router.RouteCalculation._ import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, ToMilliSatoshiConversion, randomKey} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.ParallelTestExecution import scodec.bits._ @@ -46,19 +46,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { test("calculate simple route") { val g = DirectedGraph(List( - makeEdge(1L, a, b, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), + makeEdge(1L, a, b, 1 msat, 10, cltvDelta = CltvExpiryDelta(1), balance_opt = Some(DEFAULT_AMOUNT_MSAT * 2)), makeEdge(2L, b, c, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(3L, c, d, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)), makeEdge(4L, d, e, 1 msat, 10, cltvDelta = CltvExpiryDelta(1)) )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - - assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("check fee against max pct properly") { - // fee is acceptable is it is either + // fee is acceptable if it is either // - below our maximum fee base // - below our maximum fraction of the paid amount @@ -75,7 +74,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(maxFeeBase = 1 msat), currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("calculate the shortest path (correct fees)") { @@ -95,10 +94,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // cost(EF) = 10007,0008 = cost(FD) + 1 + (cost(FD) * 400 / 1000000) // cost(AE) = 10007 -> A is source, shortest path found // cost(AB) = 10009 + // + // The amounts that need to be sent through each edge are then: + // + // +--- A ---+ + // 10009,0015 msat | | 10007,0008 msat + // B E + // 10005 msat | | 10002 msat + // C F + // 10000 msat | | 10000 msat + // +--> D <--+ val amount = 10000 msat val expectedCost = 10007 msat - val graph = DirectedGraph(List( makeEdge(1L, a, b, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), makeEdge(4L, a, e, feeBase = 1 msat, feeProportionalMillionth = 200, minHtlc = 0 msat), @@ -109,18 +117,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val Success(route) = findRoute(graph, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - - val totalCost = Graph.pathWeight(hops2Edges(route), amount, isPartial = false, 0, None).cost - - assert(hops2Ids(route) === 4 :: 5 :: 6 :: Nil) - assert(totalCost === expectedCost) - - // now channel 5 could route the amount (10000) but not the amount + fees (10007) - val graph1 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10005 msat))) - - val Success(route1) = findRoute(graph1, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - - assert(hops2Ids(route1) === 1 :: 2 :: 3 :: Nil) + val weightedPath = Graph.pathWeight(a, route2Edges(route), amount, 0, None) + assert(route2Ids(route) === 4 :: 5 :: 6 :: Nil) + assert(weightedPath.length === 3) + assert(weightedPath.cost === expectedCost) + + // update channel 5 so that it can route the final amount (10000) but not the amount + fees (10002) + val graph1 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, maxHtlc = Some(10001 msat))) + val graph2 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, capacity = 10 sat)) + val graph3 = graph.addEdge(makeEdge(5L, e, f, feeBase = 1 msat, feeProportionalMillionth = 400, minHtlc = 0 msat, balance_opt = Some(10001 msat))) + for (g <- Seq(graph1, graph2, graph3)) { + val Success(route1) = findRoute(g, a, d, amount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + assert(route2Ids(route1) === 1 :: 2 :: 3 :: Nil) + } } test("calculate route considering the direct channel pays no fees") { @@ -133,7 +142,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(2 :: 5 :: Nil)) + assert(route.map(route2Ids) === Success(2 :: 5 :: Nil)) } test("calculate simple route (add and remove edges") { @@ -145,11 +154,11 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) val graphWithRemovedEdge = g.removeEdge(ChannelDesc(ShortChannelId(3L), c, d)) val route2 = findRoute(graphWithRemovedEdge, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route2.map(hops2Ids) === Failure(RouteNotFound)) + assert(route2.map(route2Ids) === Failure(RouteNotFound)) } test("calculate the shortest path (hardcoded nodes)") { @@ -161,15 +170,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val graph = DirectedGraph(List( - makeEdge(1L, f, g, 0 msat, 0), - makeEdge(2L, g, h, 0 msat, 0), - makeEdge(3L, h, i, 0 msat, 0), - makeEdge(4L, f, h, 50 msat, 0) // more expensive + makeEdge(1L, f, g, 1 msat, 0), + makeEdge(2L, g, h, 1 msat, 0), + makeEdge(3L, h, i, 1 msat, 0), + makeEdge(4L, f, h, 50 msat, 0) // more expensive but fee will be ignored since f is the payer )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(4 :: 3 :: Nil)) - + assert(route.map(route2Ids) === Success(4 :: 3 :: Nil)) } test("calculate the shortest path (select direct channel)") { @@ -188,7 +196,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 2, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(4 :: Nil)) + assert(route.map(route2Ids) === Success(4 :: Nil)) } test("find a route using channels with htlMaximumMsat close to the payment amount") { @@ -200,14 +208,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val graph = DirectedGraph(List( - makeEdge(1L, f, g, 1 msat, 0), - // the maximum htlc allowed by this channel is only 50msat greater than what we're sending + makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), + // the maximum htlc allowed by this channel is only 50 msat greater than what we're sending makeEdge(2L, g, h, 1 msat, 0, maxHtlc = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), makeEdge(3L, h, i, 1 msat, 0) )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) == Success(1 :: 2 :: 3 :: Nil)) + assert(route.map(route2Ids) == Success(1 :: 2 :: 3 :: Nil)) } test("find a route using channels with htlMinimumMsat close to the payment amount") { @@ -219,14 +227,14 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val graph = DirectedGraph(List( - makeEdge(1L, f, g, 1 msat, 0), + makeEdge(1L, f, g, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 50.msat)), // this channel requires a minimum amount that is larger than what we are sending makeEdge(2L, g, h, 1 msat, 0, minHtlc = DEFAULT_AMOUNT_MSAT + 50.msat), makeEdge(3L, h, i, 1 msat, 0) )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Failure(RouteNotFound)) + assert(route.map(route2Ids) === Failure(RouteNotFound)) } test("if there are multiple channels between the same node, select the cheapest") { @@ -245,7 +253,26 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(1 :: 6 :: 3 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: 6 :: 3 :: Nil)) + } + + test("if there are multiple channels between the same node, select one that has enough balance") { + val (f, g, h, i) = ( + PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), // F source + PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), // G + PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), // H + PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c") // I target + ) + + val graph = DirectedGraph(List( + makeEdge(1L, f, g, 0 msat, 0), + makeEdge(2L, g, h, 5 msat, 5, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), // expensive g -> h channel with enough balance + makeEdge(6L, g, h, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT - 10.msat)), // cheap g -> h channel without enough balance + makeEdge(3L, h, i, 0 msat, 0) + )) + + val route = findRoute(graph, f, i, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: Nil)) } test("calculate longer but cheaper route") { @@ -258,7 +285,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("no local channels") { @@ -268,7 +295,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Failure(RouteNotFound)) + assert(route.map(route2Ids) === Failure(RouteNotFound)) } test("route not found") { @@ -279,7 +306,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Failure(RouteNotFound)) + assert(route.map(route2Ids) === Failure(RouteNotFound)) } test("route not found (source OR target node not connected)") { @@ -315,6 +342,35 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { assert(findRoute(g1, a, d, lowAmount, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound)) } + test("route not found (balance too low)") { + val g = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) + )) + assert(findRoute(g, a, d, 15000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).isSuccess) + + // not enough balance on the last edge + val g1 = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(10000 msat)) + )) + // not enough balance on intermediate edge (taking fee into account) + val g2 = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), + makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) + )) + // no enough balance on first edge (taking fee into account) + val g3 = DirectedGraph(List( + makeEdge(1L, a, b, 1 msat, 2, minHtlc = 10000 msat, balance_opt = Some(15000 msat)), + makeEdge(2L, b, c, 1 msat, 2, minHtlc = 10000 msat), + makeEdge(3L, c, d, 1 msat, 2, minHtlc = 10000 msat) + )) + Seq(g1, g2, g3).foreach(g => assert(findRoute(g, a, d, 15000 msat, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) === Failure(RouteNotFound))) + } + test("route to self") { val g = DirectedGraph(List( makeEdge(1L, a, b, 0 msat, 0), @@ -323,19 +379,19 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Failure(CannotRouteToSelf)) + assert(route.map(route2Ids) === Failure(CannotRouteToSelf)) } test("route to immediate neighbor") { val g = DirectedGraph(List( - makeEdge(1L, a, b, 0 msat, 0), + makeEdge(1L, a, b, 0 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), makeEdge(2L, b, c, 0 msat, 0), makeEdge(3L, c, d, 0 msat, 0), makeEdge(4L, d, e, 0 msat, 0) )) val route = findRoute(g, a, b, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(1 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: Nil)) } test("directed graph") { @@ -348,10 +404,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) val route2 = findRoute(g, e, a, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route2.map(hops2Ids) === Failure(RouteNotFound)) + assert(route2.map(route2Ids) === Failure(RouteNotFound)) } test("calculate route and return metadata") { @@ -367,20 +423,18 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val ued = ChannelUpdate(DUMMY_SIG, Block.RegtestGenesisBlock.hash, ShortChannelId(4L), 1L, 0, 1, CltvExpiryDelta(1), 49 msat, 2507 msat, 147, None) val edges = Seq( - GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), uab, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), uba, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ubc, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ucb, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ucd, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), udc, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ude, 0 sat, None), - GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ued, 0 sat, None) + GraphEdge(ChannelDesc(ShortChannelId(1L), a, b), uab, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(1L), b, a), uba, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), b, c), ubc, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(2L), c, b), ucb, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), c, d), ucd, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(3L), d, c), udc, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), d, e), ude, DEFAULT_CAPACITY, None), + GraphEdge(ChannelDesc(ShortChannelId(4L), e, d), ued, DEFAULT_CAPACITY, None) ) val g = DirectedGraph(edges) - - val hops = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get - + val hops = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).get.hops assert(hops === ChannelHop(a, b, uab) :: ChannelHop(b, c, ubc) :: ChannelHop(c, d, ucd) :: ChannelHop(d, e, ude) :: Nil) } @@ -415,7 +469,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, ignoredEdges = Set(ChannelDesc(ShortChannelId(3L), c, d)), routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Failure(RouteNotFound)) + assert(route1.map(route2Ids) === Failure(RouteNotFound)) // verify that we left the graph untouched assert(g.containsEdge(ChannelDesc(ShortChannelId(3), c, d))) @@ -424,7 +478,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { // make sure we can find a route if without the blacklist val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route2.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) } test("route to a destination that is not in the graph (with assisted routes)") { @@ -435,12 +489,27 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Failure(RouteNotFound)) + assert(route.map(route2Ids) === Failure(RouteNotFound)) // now we add the missing edge to reach the destination val extraGraphEdges = Set(makeEdge(4L, d, e, 5 msat, 5)) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + } + + test("route from a source that is not in the graph (with assisted routes)") { + val g = DirectedGraph(List( + makeEdge(2L, b, c, 10 msat, 10), + makeEdge(3L, c, d, 10 msat, 10) + )) + + val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + assert(route.map(route2Ids) === Failure(RouteNotFound)) + + // now we add the missing starting edge + val extraGraphEdges = Set(makeEdge(1L, a, b, 5 msat, 5)) + val route1 = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: Nil)) } test("verify that extra hops takes precedence over known channels") { @@ -452,13 +521,13 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route1.get(1).lastUpdate.feeBaseMsat === 10.msat) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route1.get.hops(1).lastUpdate.feeBaseMsat === 10.msat) val extraGraphEdges = Set(makeEdge(2L, b, c, 5 msat, 5)) val route2 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, extraEdges = extraGraphEdges, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route2.map(hops2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) - assert(route2.get(1).lastUpdate.feeBaseMsat === 5.msat) + assert(route2.map(route2Ids) === Success(1 :: 2 :: 3 :: 4 :: Nil)) + assert(route2.get.hops(1).lastUpdate.feeBaseMsat === 5.msat) } test("compute ignored channels") { @@ -514,10 +583,10 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(edges) - assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 18)) - assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 19)) - assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Success(0 until 20)) - assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(hops2Ids) === Failure(RouteNotFound)) + assert(findRoute(g, nodes(0), nodes(18), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 18)) + assert(findRoute(g, nodes(0), nodes(19), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 19)) + assert(findRoute(g, nodes(0), nodes(20), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Success(0 until 20)) + assert(findRoute(g, nodes(0), nodes(21), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000).map(route2Ids) === Failure(RouteNotFound)) } test("ignore cheaper route when it has more than 20 hops") { @@ -533,7 +602,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val g = DirectedGraph(expensiveShortEdge :: edges) val route = findRoute(g, nodes(0), nodes(49), DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil)) + assert(route.map(route2Ids) === Success(0 :: 1 :: 99 :: 48 :: Nil)) } test("ignore cheaper route when it has more than the requested CLTV") { @@ -548,7 +617,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxCltv = CltvExpiryDelta(28)), currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(4 :: 5 :: 6 :: Nil)) + assert(route.map(route2Ids) === Success(4 :: 5 :: 6 :: Nil)) } test("ignore cheaper route when it grows longer than the requested size") { @@ -563,7 +632,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route = findRoute(g, a, f, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(routeMaxLength = 3), currentBlockHeight = 400000) - assert(route.map(hops2Ids) === Success(1 :: 6 :: Nil)) + assert(route.map(route2Ids) === Success(1 :: 6 :: Nil)) } test("ignore loops") { @@ -576,7 +645,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, e, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil)) + assert(route1.map(route2Ids) === Success(1 :: 2 :: 4 :: 5 :: Nil)) } test("ensure the route calculation terminates correctly when selecting 0-fees edges") { @@ -592,59 +661,62 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { )) val route1 = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(route1.map(hops2Ids) === Success(1 :: 3 :: 5 :: Nil)) - } - - // @formatter:off - /** - * +---+ +---+ +---+ - * | A +-----+ | B +----------> | C | - * +-+-+ | +-+-+ +-+-+ - * ^ | ^ | - * | | | | - * | v----> + | | - * +-+-+ <-+-+ +-+-+ - * | D +----------> | E +----------> | F | - * +---+ +---+ +---+ - */ - // @formatter:on + assert(route1.map(route2Ids) === Success(1 :: 3 :: 5 :: Nil)) + } + + // +---+ +---+ +---+ + // | A |-----+ +--->| B |--->| C | + // +---+ | | +---+ +---+ + // ^ | +---+ | | + // | +--->| E |---+ | + // | | +---+ | | + // +---+ | | +---+ | + // | D |-----+ +--->| F |<-----+ + // +---+ +---+ test("find the k-shortest paths in a graph, k=4") { val (a, b, c, d, e, f) = ( - PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //a - PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //b - PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //c - PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //d - PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //e - PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //f + PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), + PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), + PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), + PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), + PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), + PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) - val graph = DirectedGraph(Seq( - makeEdge(1L, d, a, 1 msat, 0), - makeEdge(2L, d, e, 1 msat, 0), - makeEdge(3L, a, e, 1 msat, 0), - makeEdge(4L, e, b, 1 msat, 0), - makeEdge(5L, e, f, 1 msat, 0), - makeEdge(6L, b, c, 1 msat, 0), - makeEdge(7L, c, f, 1 msat, 0) + val g1 = DirectedGraph(Seq( + makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 4.msat)), + makeEdge(2L, d, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), + makeEdge(3L, a, e, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat)), + makeEdge(4L, e, b, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 2.msat)), + makeEdge(5L, e, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)), + makeEdge(6L, b, c, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 1.msat)), + makeEdge(7L, c, f, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT)) )) - val fourShortestPaths = Graph.yenKshortestPaths(graph, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) - + val fourShortestPaths = Graph.yenKshortestPaths(g1, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) assert(fourShortestPaths.size === 4) assert(hops2Ids(fourShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F assert(hops2Ids(fourShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F assert(hops2Ids(fourShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F assert(hops2Ids(fourShortestPaths(3).path.map(graphEdgeToHop)) === 1 :: 3 :: 4 :: 6 :: 7 :: Nil) // D -> A -> E -> B -> C -> F + + // Update balance D -> A to evict the last path (balance too low) + val g2 = g1.addEdge(makeEdge(1L, d, a, 1 msat, 0, balance_opt = Some(DEFAULT_AMOUNT_MSAT + 3.msat))) + val threeShortestPaths = Graph.yenKshortestPaths(g2, d, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 4, None, 0, noopBoundaries) + assert(threeShortestPaths.size === 3) + assert(hops2Ids(threeShortestPaths(0).path.map(graphEdgeToHop)) === 2 :: 5 :: Nil) // D -> E -> F + assert(hops2Ids(threeShortestPaths(1).path.map(graphEdgeToHop)) === 1 :: 3 :: 5 :: Nil) // D -> A -> E -> F + assert(hops2Ids(threeShortestPaths(2).path.map(graphEdgeToHop)) === 2 :: 4 :: 6 :: 7 :: Nil) // D -> E -> B -> C -> F } test("find the k shortest path (wikipedia example)") { val (c, d, e, f, g, h) = ( - PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), //c - PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), //d - PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), //e - PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), //f - PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), //g - PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") //h + PublicKey(hex"02999fa724ec3c244e4da52b4a91ad421dc96c9a810587849cd4b2469313519c73"), + PublicKey(hex"03f1cb1af20fe9ccda3ea128e27d7c39ee27375c8480f11a87c17197e97541ca6a"), + PublicKey(hex"0358e32d245ff5f5a3eb14c78c6f69c67cea7846bdf9aeeb7199e8f6fbb0306484"), + PublicKey(hex"029e059b6780f155f38e83601969919aae631ddf6faed58fe860c72225eb327d7c"), + PublicKey(hex"02f38f4e37142cc05df44683a83e22dea608cf4691492829ff4cf99888c5ec2d3a"), + PublicKey(hex"03fc5b91ce2d857f146fd9b986363374ffe04dc143d8bcd6d7664c8873c463cdfc") ) val graph = DirectedGraph(Seq( @@ -689,9 +761,8 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { makeEdge(6L, f, e, 1 msat, 0) )) - //we ask for 3 shortest paths but only 2 can be found + // we ask for 3 shortest paths but only 2 can be found val foundPaths = Graph.yenKshortestPaths(graph, a, f, DEFAULT_AMOUNT_MSAT, Set.empty, Set.empty, Set.empty, pathsToFind = 3, None, 0, noopBoundaries) - assert(foundPaths.size === 2) assert(hops2Ids(foundPaths(0).path.map(graphEdgeToHop)) === 1 :: 2 :: 3 :: Nil) // A -> B -> C -> F assert(hops2Ids(foundPaths(1).path.map(graphEdgeToHop)) === 1 :: 2 :: 4 :: 5 :: 6 :: Nil) // A -> B -> C -> D -> E -> F @@ -716,48 +787,47 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { (for {_ <- 0 to 10} yield findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 3, routeParams = strictFeeParams, currentBlockHeight = 400000)).map { case Failure(thr) => fail(thr) case Success(someRoute) => - - val routeCost = Graph.pathWeight(hops2Edges(someRoute), DEFAULT_AMOUNT_MSAT, isPartial = false, 0, None).cost - DEFAULT_AMOUNT_MSAT - - // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7msat of fees) - assert(routeCost === 5.msat || routeCost === 6.msat) + val weightedPath = Graph.pathWeight(a, route2Edges(someRoute), DEFAULT_AMOUNT_MSAT, 0, None) + val totalFees = weightedPath.cost - DEFAULT_AMOUNT_MSAT + // over the three routes we could only get the 2 cheapest because the third is too expensive (over 7 msat of fees) + assert(totalFees === 5.msat || totalFees === 6.msat) + assert(weightedPath.length === 3) } } - test("Use weight ratios to when computing the edge weight") { - val largeCapacity = 8000000000L msat + test("use weight ratios when computing the edge weight") { + val defaultCapacity = 15000 sat + val largeCapacity = 8000000 sat // A -> B -> C -> D is 'fee optimized', lower fees route (totFees = 2, totCltv = 4000) // A -> E -> F -> D is 'timeout optimized', lower CLTV route (totFees = 3, totCltv = 18) // A -> E -> C -> D is 'capacity optimized', more recent channel/larger capacity route val g = DirectedGraph(List( - makeEdge(1L, a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(13)), - makeEdge(4L, a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(12)), - makeEdge(2L, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(500)), - makeEdge(3L, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(500)), - makeEdge(5L, e, f, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), - makeEdge(6L, f, d, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = None, CltvExpiryDelta(9)), - makeEdge(7L, e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, maxHtlc = Some(largeCapacity), CltvExpiryDelta(12)) + makeEdge(1L, a, b, feeBase = 0 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(13)), + makeEdge(4L, a, e, feeBase = 0 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(12)), + makeEdge(2L, b, c, feeBase = 1 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(500)), + makeEdge(3L, c, d, feeBase = 1 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(500)), + makeEdge(5L, e, f, feeBase = 2 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), + makeEdge(6L, f, d, feeBase = 2 msat, 0, minHtlc = 0 msat, capacity = defaultCapacity, cltvDelta = CltvExpiryDelta(9)), + makeEdge(7L, e, c, feeBase = 2 msat, 0, minHtlc = 0 msat, capacity = largeCapacity, cltvDelta = CltvExpiryDelta(12)) )) - val Success(routeFeeOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) - assert(hops2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + val Success(routeFeeOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS, currentBlockHeight = 400000) + assert(route2Nodes(routeFeeOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) - val Success(routeCltvOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( + val Success(routeCltvOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( cltvDeltaFactor = 1, ageFactor = 0, capacityFactor = 0 ))), currentBlockHeight = 400000) + assert(route2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) - assert(hops2Nodes(routeCltvOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) - - val Success(routeCapacityOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 0, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( + val Success(routeCapacityOptimized) = findRoute(g, a, d, DEFAULT_AMOUNT_MSAT, numRoutes = 1, routeParams = DEFAULT_ROUTE_PARAMS.copy(ratios = Some(WeightRatios( cltvDeltaFactor = 0, ageFactor = 0, capacityFactor = 1 ))), currentBlockHeight = 400000) - - assert(hops2Nodes(routeCapacityOptimized) === (a, e) :: (e, c) :: (c, d) :: Nil) + assert(route2Nodes(routeCapacityOptimized) === (a, e) :: (e, c) :: (c, d) :: Nil) } test("prefer going through an older channel if fees and CLTV are the same") { @@ -778,7 +848,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0.33 ))), currentBlockHeight = currentBlockHeight) - assert(hops2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) } test("prefer a route with a smaller total CLTV if fees and score are the same") { @@ -797,7 +867,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0.33 ))), currentBlockHeight = 400000) - assert(hops2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) === (a, b) :: (b, c) :: (c, d) :: Nil) } test("avoid a route that breaks off the max CLTV") { @@ -818,7 +888,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { capacityFactor = 0.33 ))), currentBlockHeight = 400000) - assert(hops2Nodes(routeScoreOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) + assert(route2Nodes(routeScoreOptimized) === (a, e) :: (e, f) :: (f, d) :: Nil) } test("cost function is monotonic") { @@ -828,7 +898,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ShortChannelId("565643x1216x0") -> PublicChannel( ann = makeChannel(ShortChannelId("565643x1216x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca")), fundingTxid = ByteVector32.Zeroes, - capacity = 0 sat, + capacity = DEFAULT_CAPACITY, update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(14), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 10, Some(4294967295L msat))), update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565643x1216x0"), 0, 1.toByte, 1.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 0 msat, feeBaseMsat = 1000 msat, 100, Some(15000000000L msat))), meta_opt = None @@ -836,7 +906,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ShortChannelId("542280x2156x0") -> PublicChannel( ann = makeChannel(ShortChannelId("542280x2156x0").toLong, PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), PublicKey(hex"03cb7983dc247f9f81a0fa2dfa3ce1c255365f7279c8dd143e086ca333df10e278")), fundingTxid = ByteVector32.Zeroes, - capacity = 0 sat, + capacity = DEFAULT_CAPACITY, update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1000 msat, feeBaseMsat = 1000 msat, 100, Some(16777000000L msat))), update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("542280x2156x0"), 0, 1.toByte, 1.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 667 msat, 1, Some(16777000000L msat))), meta_opt = None @@ -844,7 +914,7 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ShortChannelId("565779x2711x0") -> PublicChannel( ann = makeChannel(ShortChannelId("565779x2711x0").toLong, PublicKey(hex"036d65409c41ab7380a43448f257809e7496b52bf92057c09c4f300cbd61c50d96"), PublicKey(hex"03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f")), fundingTxid = ByteVector32.Zeroes, - capacity = 0 sat, + capacity = DEFAULT_CAPACITY, update_1_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 0.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), update_2_opt = Some(ChannelUpdate(ByteVector64.Zeroes, ByteVector32.Zeroes, ShortChannelId("565779x2711x0"), 0, 1.toByte, 3.toByte, CltvExpiryDelta(144), htlcMinimumMsat = 1 msat, feeBaseMsat = 1000 msat, 100, Some(230000000L msat))), meta_opt = None @@ -852,7 +922,6 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { ) val g = DirectedGraph.makeGraph(updates) - val params = RouteParams(randomize = false, maxFeeBase = 21000 msat, maxFeePct = 0.03, routeMaxCltv = CltvExpiryDelta(1008), routeMaxLength = 6, ratios = Some( WeightRatios(cltvDeltaFactor = 0.15, ageFactor = 0.35, capacityFactor = 0.5) )) @@ -861,10 +930,30 @@ class RouteCalculationSpec extends AnyFunSuite with ParallelTestExecution { val amount = 351000 msat val Success(route) = findRoute(g, thisNode, targetNode, amount, 1, Set.empty, Set.empty, Set.empty, params, currentBlockHeight = 567634) // simulate mainnet block for heuristic - - assert(route.size == 2) - assert(route.last.nextNodeId == targetNode) + assert(route.length == 2) + assert(route.hops.last.nextNodeId == targetNode) + } + + test("validate path fees") { + val ab = makeEdge(1L, a, b, feeBase = 100 msat, 10000, minHtlc = 150 msat, maxHtlc = Some(300 msat), capacity = 1 sat, balance_opt = Some(260 msat)) + val bc = makeEdge(10L, b, c, feeBase = 5 msat, 10000, minHtlc = 100 msat, maxHtlc = Some(400 msat), capacity = 1 sat) + val cd = makeEdge(20L, c, d, feeBase = 5 msat, 10000, minHtlc = 50 msat, maxHtlc = Some(500 msat), capacity = 1 sat) + + assert(Graph.validatePath(Nil, 200 msat)) // ok + assert(Graph.validatePath(Seq(ab), 260 msat)) // ok + assert(!Graph.validatePath(Seq(ab), 10000 msat)) // above max-htlc + assert(Graph.validatePath(Seq(ab, bc), 250 msat)) // ok + assert(!Graph.validatePath(Seq(ab, bc), 255 msat)) // above balance (AB) + assert(Graph.validatePath(Seq(ab, bc, cd), 200 msat)) // ok + assert(!Graph.validatePath(Seq(ab, bc, cd), 25 msat)) // below min-htlc (CD) + assert(!Graph.validatePath(Seq(ab, bc, cd), 60 msat)) // below min-htlc (BC) + assert(!Graph.validatePath(Seq(ab, bc, cd), 110 msat)) // below min-htlc (AB) + assert(!Graph.validatePath(Seq(ab, bc, cd), 550 msat)) // above max-htlc (CD) + assert(!Graph.validatePath(Seq(ab, bc, cd), 450 msat)) // above max-htlc (BC) + assert(!Graph.validatePath(Seq(ab, bc, cd), 350 msat)) // above max-htlc (AB) + assert(!Graph.validatePath(Seq(ab, bc, cd), 250 msat)) // above balance (AB) } + } object RouteCalculationSpec { @@ -880,7 +969,7 @@ object RouteCalculationSpec { def makeChannel(shortChannelId: Long, nodeIdA: PublicKey, nodeIdB: PublicKey): ChannelAnnouncement = { val (nodeId1, nodeId2) = if (Announcements.isNode1(nodeIdA, nodeIdB)) (nodeIdA, nodeIdB) else (nodeIdB, nodeIdA) - ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, ByteVector.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey) + ChannelAnnouncement(DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, DUMMY_SIG, Features.empty, Block.RegtestGenesisBlock.hash, ShortChannelId(shortChannelId), nodeId1, nodeId2, randomKey.publicKey, randomKey.publicKey) } def makeEdge(shortChannelId: Long, @@ -915,10 +1004,12 @@ object RouteCalculationSpec { htlcMaximumMsat = maxHtlc ) - def hops2Ids(route: Seq[ChannelHop]) = route.map(hop => hop.lastUpdate.shortChannelId.toLong) + def hops2Ids(hops: Seq[ChannelHop]) = hops.map(hop => hop.lastUpdate.shortChannelId.toLong) + + def route2Ids(route: Route) = hops2Ids(route.hops) - def hops2Edges(route: Seq[ChannelHop]) = route.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate, 0 sat, None)) + def route2Edges(route: Route) = route.hops.map(hop => GraphEdge(ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId), hop.lastUpdate, 0 sat, None)) - def hops2Nodes(route: Seq[ChannelHop]) = route.map(hop => (hop.nodeId, hop.nextNodeId)) + def route2Nodes(route: Route) = route.hops.map(hop => (hop.nodeId, hop.nextNodeId)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index 5862c51e2c..a526bbc1a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.router.RouteCalculationSpec.DEFAULT_AMOUNT_MSAT import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.{Color, QueryShortChannelIds} -import fr.acinq.eclair.{CltvExpiryDelta, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomKey} +import fr.acinq.eclair.{CltvExpiryDelta, Features, LongToBtcAmount, MilliSatoshi, ShortChannelId, TestConstants, randomKey} import scodec.bits._ import scala.compat.Platform @@ -56,7 +56,7 @@ class RouterSpec extends BaseRouterSpec { // valid channel announcement, no stashing val chan_ac = channelAnnouncement(ShortChannelId(420000, 5, 0), priv_a, priv_c, priv_funding_a, priv_funding_c) val update_ac = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, c, chan_ac.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, hex"0200", timestamp = System.currentTimeMillis.milliseconds.toSeconds + 1) + val node_c = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features, timestamp = System.currentTimeMillis.milliseconds.toSeconds + 1) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ac)) peerConnection.expectNoMsg(100 millis) // we don't immediately acknowledge the announcement (back pressure) watcher.expectMsg(ValidateRequest(chan_ac)) @@ -86,7 +86,7 @@ class RouterSpec extends BaseRouterSpec { val priv_funding_u = randomKey val chan_uc = channelAnnouncement(ShortChannelId(420000, 100, 0), priv_u, priv_c, priv_funding_u, priv_funding_c) val update_uc = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_u, c, chan_uc.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, hex"00") + val node_u = makeNodeAnnouncement(priv_u, "node-U", Color(-120, -20, 60), Nil, Features.empty) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_uc)) peerConnection.expectNoMsg(200 millis) // we don't immediately acknowledge the announcement (back pressure) watcher.expectMsg(ValidateRequest(chan_uc)) @@ -174,7 +174,7 @@ class RouterSpec extends BaseRouterSpec { // unknown channel val priv_y = randomKey val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, ShortChannelId(4646464), CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, hex"0200") + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) peerConnection.expectMsg(TransportHandler.ReadAck(update_ay)) peerConnection.expectMsg(GossipDecision.NoRelatedChannel(update_ay)) @@ -192,7 +192,7 @@ class RouterSpec extends BaseRouterSpec { val priv_funding_y = randomKey // a-y will have an invalid script val chan_ay = channelAnnouncement(ShortChannelId(42002), priv_a, priv_y, priv_funding_a, priv_funding_y) val update_ay = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, priv_y.publicKey, chan_ay.shortChannelId, CltvExpiryDelta(7), 0 msat, 766000 msat, 10, htlcMaximum) - val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, hex"0200") + val node_y = makeNodeAnnouncement(priv_y, "node-Y", Color(123, 100, -40), Nil, TestConstants.Bob.nodeParams.features) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, chan_ay)) watcher.expectMsg(ValidateRequest(chan_ay)) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, update_ay)) @@ -340,8 +340,13 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams)) val res = sender.expectMsgType[RouteResponse] - assert(res.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId === d) + + sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT)) + val res1 = sender.expectMsgType[RouteResponse] + assert(res1.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) + assert(res1.routes.head.hops.last.nextNodeId === h) } test("route found (with extra routing info)") { fixture => @@ -355,8 +360,8 @@ class RouterSpec extends BaseRouterSpec { val extraHop_yz = ExtraHop(y, ShortChannelId(3), 20 msat, 21, CltvExpiryDelta(22)) sender.send(router, RouteRequest(a, z, DEFAULT_AMOUNT_MSAT, assistedRoutes = Seq(extraHop_cx :: extraHop_xy :: extraHop_yz :: Nil))) val res = sender.expectMsgType[RouteResponse] - assert(res.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil) - assert(res.hops.last.nextNodeId === z) + assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: x :: y :: Nil) + assert(res.routes.head.hops.last.nextNodeId === z) } test("route not found (channel disabled)") { fixture => @@ -365,8 +370,8 @@ class RouterSpec extends BaseRouterSpec { val peerConnection = TestProbe() sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT, routeParams = relaxedRouteParams)) val res = sender.expectMsgType[RouteResponse] - assert(res.hops.map(_.nodeId).toList === a :: b :: c :: Nil) - assert(res.hops.last.nextNodeId === d) + assert(res.routes.head.hops.map(_.nodeId).toList === a :: b :: c :: Nil) + assert(res.routes.head.hops.last.nextNodeId === d) val channelUpdate_cd1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_c, d, channelId_cd, CltvExpiryDelta(3), 0 msat, 153000 msat, 4, htlcMaximum, enable = false) peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, channelUpdate_cd1)) @@ -380,8 +385,8 @@ class RouterSpec extends BaseRouterSpec { val sender = TestProbe() sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT)) val res = sender.expectMsgType[RouteResponse] - assert(res.hops.map(_.nodeId).toList === a :: g :: Nil) - assert(res.hops.last.nextNodeId === h) + assert(res.routes.head.hops.map(_.nodeId).toList === a :: g :: Nil) + assert(res.routes.head.hops.last.nextNodeId === h) val channelUpdate_ag1 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_a, g, channelId_ag, CltvExpiryDelta(7), 0 msat, 10 msat, 10, htlcMaximum, enable = false) sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ag, g, None, channelUpdate_ag1, CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, g, announceChannel = false))) @@ -389,6 +394,27 @@ class RouterSpec extends BaseRouterSpec { sender.expectMsg(Failure(RouteNotFound)) } + test("route not found (balance too low)") { fixture => + import fixture._ + val sender = TestProbe() + + // Via private channels. + sender.send(router, RouteRequest(a, h, DEFAULT_AMOUNT_MSAT)) + sender.expectMsgType[RouteResponse] + sender.send(router, RouteRequest(a, h, 50000000 msat)) + sender.expectMsg(Failure(RouteNotFound)) + + // Via public channels. + sender.send(router, RouteRequest(a, d, DEFAULT_AMOUNT_MSAT)) + sender.expectMsgType[RouteResponse] + val commitments1 = CommitmentsSpec.makeCommitments(10000000 msat, 20000000 msat, a, b, announceChannel = true) + sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1)) + sender.send(router, RouteRequest(a, d, 12000000 msat)) + sender.expectMsg(Failure(RouteNotFound)) + sender.send(router, RouteRequest(a, d, 5000000 msat)) + sender.expectMsgType[RouteResponse] + } + test("temporary channel exclusion") { fixture => import fixture._ val sender = TestProbe() @@ -451,9 +477,9 @@ class RouterSpec extends BaseRouterSpec { val response = sender.expectMsgType[RouteResponse] // the route hasn't changed (nodes are the same) - assert(response.hops.map(_.nodeId).toList == preComputedRoute.dropRight(1).toList) - assert(response.hops.last.nextNodeId == preComputedRoute.last) - assert(response.hops.map(_.lastUpdate).toList == List(update_ab, update_bc, update_cd)) + assert(response.routes.head.hops.map(_.nodeId).toList == preComputedRoute.dropRight(1).toList) + assert(response.routes.head.hops.last.nextNodeId == preComputedRoute.last) + assert(response.routes.head.hops.map(_.lastUpdate).toList == List(update_ab, update_bc, update_cd)) } test("ask for channels that we marked as stale for which we receive a new update") { fixture => @@ -495,37 +521,64 @@ class RouterSpec extends BaseRouterSpec { val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get assert(channel_ab.meta_opt === None) - // When the local channel comes back online, it will send a LocalChannelUpdate to the router. - val balances1 = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) - val commitments1 = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) - sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments1)) - sender.send(router, GetRoutingState) - val channel_ab1 = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab1.meta_opt.map(_.balance1), channel_ab1.meta_opt.map(_.balance2)) === balances1) - // And the graph should be updated too. - sender.send(router, 'data) - val g1 = sender.expectMsgType[Data].graph - val edge_ab1 = g1.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba1 = g1.getEdge(ChannelDesc(channelId_ab, b, a)).get - assert(edge_ab1.capacity == channel_ab.capacity && edge_ba1.capacity == channel_ab.capacity) - assert(balances1.contains(edge_ab1.balance_opt)) - assert(edge_ba1.balance_opt === None) - - // When HTLCs are relayed through the channel, balance changes are sent to the router. - val balances2 = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) - val commitments2 = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) - sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ab, commitments2)) - sender.send(router, GetRoutingState) - val channel_ab2 = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get - assert(Set(channel_ab2.meta_opt.map(_.balance1), channel_ab2.meta_opt.map(_.balance2)) === balances2) - // And the graph should be updated too. - sender.send(router, 'data) - val g2 = sender.expectMsgType[Data].graph - val edge_ab2 = g2.getEdge(ChannelDesc(channelId_ab, a, b)).get - val edge_ba2 = g2.getEdge(ChannelDesc(channelId_ab, b, a)).get - assert(edge_ab2.capacity == channel_ab.capacity && edge_ba2.capacity == channel_ab.capacity) - assert(balances2.contains(edge_ab2.balance_opt)) - assert(edge_ba2.balance_opt === None) + { + // When the local channel comes back online, it will send a LocalChannelUpdate to the router. + val balances = Set[Option[MilliSatoshi]](Some(10000 msat), Some(15000 msat)) + val commitments = CommitmentsSpec.makeCommitments(10000 msat, 15000 msat, a, b, announceChannel = true) + sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, GetRoutingState) + val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + // And the graph should be updated too. + sender.send(router, Symbol("data")) + val g = sender.expectMsgType[Data].graph + val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) + assert(balances.contains(edge_ab.balance_opt)) + assert(edge_ba.balance_opt === None) + } + + { + // First we make sure we aren't in the "pending rebroadcast" state for this channel update. + sender.send(router, TickBroadcast) + sender.send(router, Symbol("data")) + assert(sender.expectMsgType[Data].rebroadcast.updates.isEmpty) + + // Then we update the balance without changing the contents of the channel update; the graph should still be updated. + val balances = Set[Option[MilliSatoshi]](Some(11000 msat), Some(14000 msat)) + val commitments = CommitmentsSpec.makeCommitments(11000 msat, 14000 msat, a, b, announceChannel = true) + sender.send(router, LocalChannelUpdate(sender.ref, null, channelId_ab, b, Some(chan_ab), update_ab, commitments)) + sender.send(router, GetRoutingState) + val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + // And the graph should be updated too. + sender.send(router, Symbol("data")) + val g = sender.expectMsgType[Data].graph + val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) + assert(balances.contains(edge_ab.balance_opt)) + assert(edge_ba.balance_opt === None) + } + + { + // When HTLCs are relayed through the channel, balance changes are sent to the router. + val balances = Set[Option[MilliSatoshi]](Some(12000 msat), Some(13000 msat)) + val commitments = CommitmentsSpec.makeCommitments(12000 msat, 13000 msat, a, b, announceChannel = true) + sender.send(router, AvailableBalanceChanged(sender.ref, null, channelId_ab, commitments)) + sender.send(router, GetRoutingState) + val channel_ab = sender.expectMsgType[RoutingState].channels.find(_.ann == chan_ab).get + assert(Set(channel_ab.meta_opt.map(_.balance1), channel_ab.meta_opt.map(_.balance2)) === balances) + // And the graph should be updated too. + sender.send(router, Symbol("data")) + val g = sender.expectMsgType[Data].graph + val edge_ab = g.getEdge(ChannelDesc(channelId_ab, a, b)).get + val edge_ba = g.getEdge(ChannelDesc(channelId_ab, b, a)).get + assert(edge_ab.capacity == channel_ab.capacity && edge_ba.capacity == channel_ab.capacity) + assert(balances.contains(edge_ab.balance_opt)) + assert(edge_ba.balance_opt === None) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index 4ec6b9b084..b9736f167c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -325,8 +325,8 @@ object RoutingSyncSpec { val channelAnn_12 = channelAnnouncement(shortChannelId, priv1, priv2, priv_funding1, priv_funding2) val channelUpdate_12 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv1, priv2.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) val channelUpdate_21 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv2, priv1.publicKey, shortChannelId, cltvExpiryDelta = CltvExpiryDelta(7), 0 msat, feeBaseMsat = 766000 msat, feeProportionalMillionths = 10, 500000000L msat, timestamp = timestamp) - val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), hex"0200") - val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), hex"00") + val nodeAnnouncement_1 = makeNodeAnnouncement(priv1, "a", Color(0, 0, 0), List(), TestConstants.Bob.nodeParams.features) + val nodeAnnouncement_2 = makeNodeAnnouncement(priv2, "b", Color(0, 0, 0), List(), Features.empty) val publicChannel = PublicChannel(channelAnn_12, ByteVector32.Zeroes, Satoshi(0), Some(channelUpdate_12), Some(channelUpdate_21), None) (publicChannel, nodeAnnouncement_1, nodeAnnouncement_2) } @@ -342,7 +342,7 @@ object RoutingSyncSpec { def makeFakeNodeAnnouncement(pub2priv: mutable.Map[PublicKey, PrivateKey])(nodeId: PublicKey): NodeAnnouncement = { val priv = pub2priv(nodeId) - makeNodeAnnouncement(priv, "", Color(0, 0, 0), List(), hex"00") + makeNodeAnnouncement(priv, "", Color(0, 0, 0), List(), Features.empty) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index d9a5a923ae..585cd50670 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -99,7 +99,7 @@ class ChannelCodecsSpec extends AnyFunSuite { defaultFinalScriptPubKey = randomBytes(10 + Random.nextInt(200)), localPaymentBasepoint = None, isFunder = Random.nextBoolean(), - features = randomBytes(256)) + features = TestConstants.Alice.nodeParams.features) val encoded = localParamsCodec(ChannelVersion.ZEROES).encode(o).require val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require.value assert(decoded.localPaymentBasepoint.isEmpty) @@ -108,7 +108,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" val withGlobalFeaturesDecoded = localParamsCodec(ChannelVersion.STANDARD).decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.features === hex"0a8a") + assert(withGlobalFeaturesDecoded.features.toByteVector === hex"0a8a") val o1 = LocalParams( nodeId = randomKey.publicKey, @@ -122,7 +122,7 @@ class ChannelCodecsSpec extends AnyFunSuite { defaultFinalScriptPubKey = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32).publicKey)), localPaymentBasepoint = Some(PrivateKey(randomBytes32).publicKey), isFunder = Random.nextBoolean(), - features = randomBytes(256)) + features = Features(randomBytes(256))) val encoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).encode(o1).require val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require.value assert(decoded1.localPaymentBasepoint.isDefined) @@ -143,7 +143,7 @@ class ChannelCodecsSpec extends AnyFunSuite { paymentBasepoint = randomKey.publicKey, delayedPaymentBasepoint = randomKey.publicKey, htlcBasepoint = randomKey.publicKey, - features = randomBytes(256)) + features = TestConstants.Alice.nodeParams.features) val encoded = remoteParamsCodec.encode(o).require val decoded = remoteParamsCodec.decodeValue(encoded).require assert(o === decoded) @@ -151,7 +151,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // Backwards-compatibility: decode remoteparams with global features. val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value - assert(withGlobalFeaturesDecoded.features === hex"028a") + assert(withGlobalFeaturesDecoded.features.toByteVector === hex"028a") } test("encode/decode htlc") { @@ -388,6 +388,9 @@ class ChannelCodecsSpec extends AnyFunSuite { .replace(""""toRemote"""", """"toRemoteMsat"""") .replace("fundingKeyPath", "channelKeyPath") .replace(""""version":0,""", "") + .replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"8a"""") + .replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"81"""") + .replace(""""features":{"activated":[],"unknown":[]}""", """"features":""""") val newjson = Serialization.write(newnormal)(JsonSupport.formats) .replace(""","unknownFields":""""", "") @@ -399,6 +402,9 @@ class ChannelCodecsSpec extends AnyFunSuite { .replace(""""toRemote"""", """"toRemoteMsat"""") .replace("fundingKeyPath", "channelKeyPath") .replace(""""version":0,""", "") + .replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"8a"""") + .replace(""""features":{"activated":[{"feature":{},"support":{}},{"feature":{},"support":{}}],"unknown":[]}""", """"features":"81"""") + .replace(""""features":{"activated":[],"unknown":[]}""", """"features":""""") assert(oldjson === refjson) assert(newjson === refjson) @@ -420,7 +426,7 @@ object ChannelCodecsSpec { defaultFinalScriptPubKey = ByteVector.empty, localPaymentBasepoint = None, isFunder = true, - features = hex"deadbeef") + features = Features.empty) val remoteParams = RemoteParams( nodeId = randomKey.publicKey, @@ -435,7 +441,7 @@ object ChannelCodecsSpec { paymentBasepoint = PrivateKey(ByteVector.fill(32)(3)).publicKey, delayedPaymentBasepoint = PrivateKey(ByteVector.fill(32)(4)).publicKey, htlcBasepoint = PrivateKey(ByteVector.fill(32)(6)).publicKey, - features = hex"deadbeef") + features = Features.empty) val paymentPreimages = Seq( ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index e675b6bf4d..78d3fbc4ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -44,7 +44,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey test("encode/decode init message") { - case class TestCase(encoded: ByteVector, features: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None) + case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None) val chainHash1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101") val chainHash2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202") val testCases = Seq( @@ -67,7 +67,7 @@ class LightningMessageCodecsSpec extends AnyFunSuite { for (testCase <- testCases) { if (testCase.valid) { val init = initCodec.decode(testCase.encoded.bits).require.value - assert(init.features === testCase.features) + assert(init.features.toByteVector === testCase.rawFeatures) assert(init.networks === testCase.networks) val encoded = initCodec.encode(init).require assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded)) @@ -173,8 +173,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111) val commit_sig = CommitSig(randomBytes32, randomBytes64, randomBytes64 :: randomBytes64 :: randomBytes64 :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1)) - val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) - val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, Features(bin(7, 9)), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) + val node_announcement = NodeAnnouncement(randomBytes64, Features(bin(1, 2)), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, CltvExpiryDelta(3), 4 msat, 5 msat, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) diff --git a/eclair-node-gui/src/main/resources/eclair-node-gui.sh b/eclair-node-gui/src/main/resources/eclair-node-gui.sh index 9fb49b5f2e..e7c01a47d5 100755 --- a/eclair-node-gui/src/main/resources/eclair-node-gui.sh +++ b/eclair-node-gui/src/main/resources/eclair-node-gui.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + # Copyright (c) 2012, Joshua Suereth # All rights reserved. # @@ -7,8 +9,6 @@ # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#!/usr/bin/env bash - ### ------------------------------- ### ### Helper methods for BASH scripts ### ### ------------------------------- ### @@ -346,7 +346,7 @@ declare -a java_args declare -a app_commands declare -r real_script_path="$(realpath "$0")" declare -r app_home="$(realpath "$(dirname "$real_script_path")")" -declare -r lib_dir="$(realpath "${app_home::-4}/lib")" # {app_home::-4} transforms ../bin in ../ +declare -r lib_dir="$(realpath "${app_home:0:${#app_home}-4}/lib")" # {app_home:0:${#app_home}-4} transforms ../bin in ../ declare -a app_mainclass=("fr.acinq.eclair.JavafxBoot") declare -a app_entrypoint=$(ls $lib_dir |grep eclair-node-gui) # TODO: improve this declare -a app_classpath=("$lib_dir:$lib_dir/$app_entrypoint") diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/JavafxBoot.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/JavafxBoot.scala index 9152e82cad..f3d95c4b7d 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/JavafxBoot.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/JavafxBoot.scala @@ -27,15 +27,16 @@ import scala.concurrent.ExecutionContext.Implicits.global * Created by PM on 25/01/2016. */ object JavafxBoot extends App with Logging { - val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) try { + val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) + val config = NodeParams.loadConfiguration(datadir) val headless = System.getProperty("eclair.headless") != null if (headless) { - implicit val system = ActorSystem("eclair-node-gui") + implicit val system = ActorSystem("eclair-node-gui", config) val setup = new Setup(datadir) setup.bootstrap.map { kit => - Boot.startApiServiceIfEnabled(setup.config, kit) + Boot.startApiServiceIfEnabled(kit) } } else { System.setProperty("javafx.preloader", classOf[FxPreloader].getName) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala index a77ee3f1dd..892d331072 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala @@ -82,7 +82,8 @@ class FxApp extends Application with Logging { mainFXML.setController(controller) val mainRoot = mainFXML.load[Parent] val datadir = new File(getParameters.getUnnamed.get(0)) - implicit val system = ActorSystem("eclair-node-gui") + val config = NodeParams.loadConfiguration(datadir) + implicit val system = ActorSystem("eclair-node-gui", config) val setup = new Setup(datadir) val unitConf = setup.config.getString("gui.unit") @@ -103,7 +104,7 @@ class FxApp extends Application with Logging { pKit.completeWith(setup.bootstrap) pKit.future.onComplete { case Success(kit) => - Boot.startApiServiceIfEnabled(setup.config, kit) + Boot.startApiServiceIfEnabled(kit) Platform.runLater(new Runnable { override def run(): Unit = { val scene = new Scene(mainRoot) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index 88d262adaf..069b5e312f 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -201,7 +201,7 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging val distilledFailures = PaymentFailure.transformForUser(p.failures) val message = s"${distilledFailures.size} attempts:\n${ distilledFailures.map { - case LocalFailure(t) => s"- (local) ${t.getMessage}" + case LocalFailure(_, t) => s"- (local) ${t.getMessage}" case RemoteFailure(_, e) => s"- (remote) ${e.failureMessage.message}" case _ => "- Unknown error" }.mkString("\n") diff --git a/eclair-node/src/main/resources/application.conf b/eclair-node/src/main/resources/application.conf index 6b18351a63..a55e28644b 100644 --- a/eclair-node/src/main/resources/application.conf +++ b/eclair-node/src/main/resources/application.conf @@ -17,40 +17,8 @@ kamon.instrumentation.akka { } akka { - loggers = ["akka.event.slf4j.Slf4jLogger"] logger-startup-timeout = 30s loglevel = "DEBUG" # akka doc: You can enable DEBUG level for akka.loglevel and control the actual level in the SLF4J backend without any significant overhead, also for production. logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" - - io { - tcp { - - # The maximum number of bytes delivered by a `Received` message. Before - # more data is read from the network the connection actor will try to - # do other work. - # The purpose of this setting is to impose a smaller limit than the - # configured receive buffer size. When using value 'unlimited' it will - # try to read all from the receive buffer. - # As per BOLT#8 lightning messages are at most 2 + 16 + 65535 + 16 = 65569bytes - # Currently the largest message is update_add_htlc (~1500b). - # As a tradeoff to reduce the RAM consumption, in conjunction with tcp pull mode, - # the default value is chosen to allow for a decent number of messages to be prefetched. - max-received-message-size = 16384b - - } - } - - # Default maximum content length which should not be exceeded by incoming request entities. - # Can be changed at runtime (to a higher or lower value) via the `HttpEntity::withSizeLimit` method. - # Note that it is not necessarily a problem to set this to a high value as all stream operations - # are always properly backpressured. - # Nevertheless you might want to apply some limit in order to prevent a single client from consuming - # an excessive amount of server resources. - # - # Set to `infinite` to completely disable entity length checks. (Even then you can still apply one - # programmatically via `withSizeLimit`.) - # - # We disable the size check, because the batching bitcoin json-rpc client may return very large results - http.client.parsing.max-content-length=infinite } \ No newline at end of file diff --git a/eclair-node/src/main/resources/eclair-node.sh b/eclair-node/src/main/resources/eclair-node.sh index 1d32a55101..6e1c190081 100755 --- a/eclair-node/src/main/resources/eclair-node.sh +++ b/eclair-node/src/main/resources/eclair-node.sh @@ -1,3 +1,5 @@ +#!/usr/bin/env bash + # Copyright (c) 2012, Joshua Suereth # All rights reserved. # @@ -7,8 +9,6 @@ # Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#!/usr/bin/env bash - ### ------------------------------- ### ### Helper methods for BASH scripts ### ### ------------------------------- ### @@ -346,7 +346,7 @@ declare -a java_args declare -a app_commands declare -r real_script_path="$(realpath "$0")" declare -r app_home="$(realpath "$(dirname "$real_script_path")")" -declare -r lib_dir="$(realpath "${app_home::-4}/lib")" # {app_home::-4} transforms ../bin in ../ +declare -r lib_dir="$(realpath "${app_home:0:${#app_home}-4}/lib")" # {app_home:0:${#app_home}-4} transforms ../bin in ../ declare -a app_mainclass=("fr.acinq.eclair.Boot") declare -a app_entrypoint=$(ls $lib_dir |grep eclair-node) # TODO: improve this declare -a app_classpath=("$lib_dir:$lib_dir/$app_entrypoint") diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala index 664f08009c..522ae9219d 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/Boot.scala @@ -21,7 +21,6 @@ import java.io.File import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.stream.{ActorMaterializer, BindFailedException} -import com.typesafe.config.Config import fr.acinq.eclair.api.Service import grizzled.slf4j.Logging import kamon.Kamon @@ -33,24 +32,24 @@ import scala.util.{Failure, Success} * Created by PM on 25/01/2016. */ object Boot extends App with Logging { - - val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) - try { + val datadir = new File(System.getProperty("eclair.datadir", System.getProperty("user.home") + "/.eclair")) + val config = NodeParams.loadConfiguration(datadir) + val plugins = Plugin.loadPlugins(args.map(new File(_))) plugins.foreach(plugin => logger.info(s"loaded plugin ${plugin.getClass.getSimpleName}")) - implicit val system: ActorSystem = ActorSystem("eclair-node") + implicit val system: ActorSystem = ActorSystem("eclair-node", config) implicit val ec: ExecutionContext = system.dispatcher val setup = new Setup(datadir) - if (setup.config.getBoolean("enable-kamon")) { - Kamon.init(setup.appConfig) + if (config.getBoolean("eclair.enable-kamon")) { + Kamon.init(config) } plugins.foreach(_.onSetup(setup)) setup.bootstrap onComplete { case Success(kit) => - startApiServiceIfEnabled(setup.config, kit) + startApiServiceIfEnabled(kit) plugins.foreach(_.onKit(kit)) case Failure(t) => onError(t) } @@ -61,12 +60,12 @@ object Boot extends App with Logging { /** * Starts the http APIs service if enabled in the configuration * - * @param config * @param kit * @param system * @param ec */ - def startApiServiceIfEnabled(config: Config, kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = { + def startApiServiceIfEnabled(kit: Kit)(implicit system: ActorSystem, ec: ExecutionContext) = { + val config = system.settings.config.getConfig("eclair") if(config.getBoolean("api.enabled")){ logger.info(s"json API enabled on port=${config.getInt("api.port")}") implicit val materializer = ActorMaterializer() diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index c05abc8e64..7742cb298a 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.router.Router.RouteResponse import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64} +import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64} import org.json4s.JsonAST._ import org.json4s.{CustomKeySerializer, CustomSerializer, DefaultFormats, Extraction, TypeHints, jackson} import scodec.bits.ByteVector @@ -182,7 +182,7 @@ class RouteResponseSerializer extends CustomSerializer[RouteResponse](_ => ( { null }, { case route: RouteResponse => - val nodeIds = route.hops match { + val nodeIds = route.routes.head.hops match { case rest :+ last => rest.map(_.nodeId) :+ last.nodeId :+ last.nextNodeId case Nil => Nil } @@ -227,6 +227,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( { val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq + val features = JField("features", JsonSupport.featuresToJson(Features(p.features.bitmask))) val fieldList = List(JField("prefix", JString(p.prefix)), JField("timestamp", JLong(p.timestamp)), JField("nodeId", JString(p.nodeId.toString())), @@ -238,10 +239,17 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](_ => ( { JField("paymentHash", JString(p.paymentHash.toString()))) ++ expiry ++ minFinalCltvExpiry ++ - amount + amount :+ + features JObject(fieldList) })) +class FeaturesSerializer extends CustomSerializer[Features](_ => ( { + null +}, { + case features: Features => JsonSupport.featuresToJson(features) +})) + class JavaUUIDSerializer extends CustomSerializer[UUID](_ => ( { null }, { @@ -317,8 +325,20 @@ object JsonSupport extends Json4sSupport { new DirectedHtlcSerializer + new PaymentRequestSerializer + new JavaUUIDSerializer + + new FeaturesSerializer + CustomTypeHints.incomingPaymentStatus + CustomTypeHints.outgoingPaymentStatus + CustomTypeHints.paymentEvent).withTypeHintFieldName("type") + def featuresToJson(features: Features) = JObject( + JField("activated", JArray(features.activated.map { a => + JObject( + JField("name", JString(a.feature.rfcName)), + JField("support", JString(a.support.toString)) + )}.toList)), + JField("unknown", JArray(features.unknown.map { i => + JObject( + JField("featureBit", JInt(i.bitIndex)) + )}.toList)) + ) } \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/getinfo b/eclair-node/src/test/resources/api/getinfo index 2f18eee7c4..e72ff0b185 100644 --- a/eclair-node/src/test/resources/api/getinfo +++ b/eclair-node/src/test/resources/api/getinfo @@ -1 +1 @@ -{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":"","chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]} \ No newline at end of file +{"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":[{"name":"option_data_loss_protect","support":"mandatory"},{"name":"gossip_queries_ex","support":"optional"}],"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","blockHeight":9999,"publicAddresses":["localhost:9731"]} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-expired b/eclair-node/src/test/resources/api/received-expired index 1b9eeeb70a..54103dcf0e 100644 --- a/eclair-node/src/test/resources/api/received-expired +++ b/eclair-node/src/test/resources/api/received-expired @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"expired"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-pending b/eclair-node/src/test/resources/api/received-pending index c73798a7c8..3d9198127e 100644 --- a/eclair-node/src/test/resources/api/received-pending +++ b/eclair-node/src/test/resources/api/received-pending @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"pending"}} \ No newline at end of file diff --git a/eclair-node/src/test/resources/api/received-success b/eclair-node/src/test/resources/api/received-success index 121cb9a6ce..5cddd0d697 100644 --- a/eclair-node/src/test/resources/api/received-success +++ b/eclair-node/src/test/resources/api/received-success @@ -1 +1 @@ -{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}} \ No newline at end of file +{"paymentRequest":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":[],"unknown":[]}},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":42,"status":{"type":"received","amount":42,"receivedAt":45}} \ No newline at end of file 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 a12ec88666..e52ac78387 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 @@ -29,6 +29,8 @@ import akka.util.Timeout import de.heikoseeberger.akkahttpjson4s.Json4sSupport import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32} +import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} +import fr.acinq.eclair.Features.{ChannelRangeQueriesExtended, OptionDataLossProtect} import fr.acinq.eclair._ import fr.acinq.eclair.channel.ChannelCommandResponse import fr.acinq.eclair.db._ @@ -171,7 +173,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM eclair.getInfoResponse()(any[Timeout]) returns Future.successful(GetInfoResponse( version = "1.0.0-SNAPSHOT-e3f1ec0", color = Color(0.toByte, 1.toByte, 2.toByte).toString, - features = "", + features = Features(Set(ActivatedFeature(OptionDataLossProtect, Mandatory), ActivatedFeature(ChannelRangeQueriesExtended, Optional))), nodeId = aliceNodeId, alias = "alice", chainHash = ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index b84f8d49d1..971314c062 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -83,9 +83,9 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { } test("Payment Request") { - val ref = "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp" + val ref = "lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy" val pr = PaymentRequest.read(ref) - JsonSupport.serialization.write(pr)(JsonSupport.formats) shouldBe """{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","serialized":"lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000}""" + JsonSupport.serialization.write(pr)(JsonSupport.formats) shouldBe """{"prefix":"lnbcrt","timestamp":1587386125,"nodeId":"03b207771ddba774e318970e9972da2491ff8e54f777ad0528b6526773730248a0","serialized":"lnbcrt50n1p0fm9cdpp5al3wvsfkc6p7fxy89eu8gm4aww9mseu9syrcqtpa4mvx42qelkwqdq9v9ekgxqrrss9qypqsqsp5wl2t45v0hj4lgud0zjxcnjccd29ts0p2kh4vpw75vnhyyzyjtjtqarpvqg33asgh3z5ghfuvhvtf39xtnu9e7aqczpgxa9quwsxkd9rnwmx06pve9awgeewxqh90dqgrhzgsqc09ek6uejr93z8puafm6gsqgrk0hy","description":"asd","paymentHash":"efe2e64136c683e498872e78746ebd738bb867858107802c3daed86aa819fd9c","expiry":3600,"amount":5000,"features":{"activated":[{"name":"var_onion_optin","support":"optional"},{"name":"payment_secret","support":"optional"}],"unknown":[]}}""" } test("type hints") { diff --git a/pom.xml b/pom.xml index cd62300f6a..c9a897f8b2 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 1.7.2 0.18 24.0-android - 2.0.0 + 2.1.0 @@ -160,6 +160,7 @@ doc-jar + ${maven.test.skip} -no-link-warnings @@ -246,6 +247,7 @@ scoverage-maven-plugin 1.4.0-RC1 + ${maven.test.skip} ${scala.version} From 743767c494d805ea79a36629e6a4430d07f7ed36 Mon Sep 17 00:00:00 2001 From: Andrea Date: Fri, 29 May 2020 18:21:05 +0200 Subject: [PATCH 10/12] Update transactions RFC test vectors for option_static_remotekey --- .../test/resources/bolt3-tx-test-vectors.txt | 432 +++++++++--------- .../eclair/transactions/TestVectorsSpec.scala | 96 ++-- 2 files changed, 268 insertions(+), 260 deletions(-) diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt index 9a37f098f9..b8d6430289 100644 --- a/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt +++ b/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt @@ -4,14 +4,14 @@ local_feerate_per_kw: 15000 # base commitment transaction fee = 10860 # actual commitment transaction fee = 10860 - # to-local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c0 - # local_signature = 3044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c3836939 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100c3127b33dcc741dd6b05b1e63cbd1a9a7d816f37af9b6756fa2376b056f032370220408b96279808fe57eb7e463710804cdf4f108388bc5cf722d8c848d2c7f9f3b0 + # local_signature = 30440220616210b2cc4d3afb601013c373bbd8aac54febd9f15400379a8cb65ce7deca60022034236c010991beb7ff770510561ae8dc885b8d38d1947248c38f2ae055647142 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48454a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220616210b2cc4d3afb601013c373bbd8aac54febd9f15400379a8cb65ce7deca60022034236c010991beb7ff770510561ae8dc885b8d38d1947248c38f2ae05564714201483045022100c3127b33dcc741dd6b05b1e63cbd1a9a7d816f37af9b6756fa2376b056f032370220408b96279808fe57eb7e463710804cdf4f108388bc5cf722d8c848d2c7f9f3b001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 - name: commitment tx with all 5 htlcs untrimmed (minimum feerate) + name: commitment tx with all five HTLCs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 0 @@ -22,34 +22,34 @@ # HTLC 0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606 - # local_signature = 30440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f06 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022009b048187705a8cbc9ad73adbe5af148c3d012e1f067961486c822c7af08158c022006d66f3704cfab3eb2dc49dae24e4aa22a6910fc9b424007583204e3621af2e5 + # local_signature = 304402206fc2d1f10ea59951eefac0b4b7c396a3c3d87b71ff0b019796ef4535beaf36f902201765b0181e514d04f4c8ad75659d7037be26cdb3f8bb6f78fe61decef484c3ea + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402206fc2d1f10ea59951eefac0b4b7c396a3c3d87b71ff0b019796ef4535beaf36f902201765b0181e514d04f4c8ad75659d7037be26cdb3f8bb6f78fe61decef484c3ea01473044022009b048187705a8cbc9ad73adbe5af148c3d012e1f067961486c822c7af08158c022006d66f3704cfab3eb2dc49dae24e4aa22a6910fc9b424007583204e3621af2e501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 5 - # signature for output 0 (htlc 0) - remote_htlc_signature = 304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6 - # signature for output 1 (htlc 2) - remote_htlc_signature = 3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b - # signature for output 2 (htlc 1) - remote_htlc_signature = 304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202 - # signature for output 3 (htlc 3) - remote_htlc_signature = 3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554 - # signature for output 4 (htlc 4) - remote_htlc_signature = 304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d - # local_signature = 304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5 - output htlc_success_tx 0: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # local_signature = 3045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be5 - output htlc_timeout_tx 2: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 3045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da - output htlc_success_tx 1: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # local_signature = 30440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac08727 - output htlc_timeout_tx 3: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 30440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e - output htlc_success_tx 4: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 0) + remote_htlc_signature = 3045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b + # signature for output 1 (HTLC 2) + remote_htlc_signature = 30440220649fe8b20e67e46cbb0d09b4acea87dbec001b39b08dee7bdd0b1f03922a8640022037c462dff79df501cecfdb12ea7f4de91f99230bb544726f6e04527b1f896004 + # signature for output 2 (HTLC 1) + remote_htlc_signature = 30440220770fc321e97a19f38985f2e7732dd9fe08d16a2efa4bcbc0429400a447faf49102204d40b417f3113e1b0944ae0986f517564ab4acd3d190503faf97a6e420d43352 + # signature for output 3 (HTLC 3) + remote_htlc_signature = 304402207bcbf4f60a9829b05d2dbab84ed593e0291836be715dc7db6b72a64caf646af802201e489a5a84f7c5cc130398b841d138d031a5137ac8f4c49c770a4959dc3c1363 + # signature for output 4 (HTLC 4) + remote_htlc_signature = 3044022076dca5cb81ba7e466e349b7128cdba216d4d01659e29b96025b9524aaf0d1899022060de85697b88b21c749702b7d2cfa7dfeaa1f472c8f1d7d9c23f2bf968464b87 + # local_signature = 30440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce + output htlc_success_tx 0: 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b00000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d9e29616b8f3959f1d3d7f7ce893ffedcdc407717d0de8e37d808c91d3a7c50d022078c3033f6d00095c8720a4bc943c1b45727818c082e4e3ddbc6d3116435b624b014730440220636de5682ef0c5b61f124ec74e8aa2461a69777521d6998295dcea36bc3338110220165285594b23c50b28b82df200234566628a27bcd17f7f14404bd865354eb3ce012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_signature = 3045022100803159dee7935dba4a1d36a61055ce8fd62caa528573cc221ae288515405a252022029c59e7cffce374fe860100a4a63787e105c3cf5156d40b12dd53ff55ac8cf3f + output htlc_timeout_tx 2: 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b01000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220649fe8b20e67e46cbb0d09b4acea87dbec001b39b08dee7bdd0b1f03922a8640022037c462dff79df501cecfdb12ea7f4de91f99230bb544726f6e04527b1f89600401483045022100803159dee7935dba4a1d36a61055ce8fd62caa528573cc221ae288515405a252022029c59e7cffce374fe860100a4a63787e105c3cf5156d40b12dd53ff55ac8cf3f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100a437cc2ce77400ecde441b3398fea3c3ad8bdad8132be818227fe3c5b8345989022069d45e7fa0ae551ec37240845e2c561ceb2567eacf3076a6a43a502d05865faa + output htlc_success_tx 1: 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b02000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220770fc321e97a19f38985f2e7732dd9fe08d16a2efa4bcbc0429400a447faf49102204d40b417f3113e1b0944ae0986f517564ab4acd3d190503faf97a6e420d4335201483045022100a437cc2ce77400ecde441b3398fea3c3ad8bdad8132be818227fe3c5b8345989022069d45e7fa0ae551ec37240845e2c561ceb2567eacf3076a6a43a502d05865faa012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 304402203121d9b9c055f354304b016a36662ee99e1110d9501cb271b087ddb6f382c2c80220549882f3f3b78d9c492de47543cb9a697cecc493174726146536c5954dac7487 + output htlc_timeout_tx 3: 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b03000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207bcbf4f60a9829b05d2dbab84ed593e0291836be715dc7db6b72a64caf646af802201e489a5a84f7c5cc130398b841d138d031a5137ac8f4c49c770a4959dc3c13630147304402203121d9b9c055f354304b016a36662ee99e1110d9501cb271b087ddb6f382c2c80220549882f3f3b78d9c492de47543cb9a697cecc493174726146536c5954dac748701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3045022100d9080f103cc92bac15ec42464a95f070c7fb6925014e673ee2ea1374d36a7f7502200c65294d22eb20d48564954d5afe04a385551919d8b2ddb4ae2459daaeee1d95 + output htlc_success_tx 4: 02000000000101ab84ff284f162cfbfef241f853b47d4368d171f9e2a1445160cd591c4c7d882b04000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022076dca5cb81ba7e466e349b7128cdba216d4d01659e29b96025b9524aaf0d1899022060de85697b88b21c749702b7d2cfa7dfeaa1f472c8f1d7d9c23f2bf968464b8701483045022100d9080f103cc92bac15ec42464a95f070c7fb6925014e673ee2ea1374d36a7f7502200c65294d22eb20d48564954d5afe04a385551919d8b2ddb4ae2459daaeee1d95012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 7 outputs untrimmed (maximum feerate) + name: commitment tx with seven outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 647 @@ -60,34 +60,34 @@ # HTLC 0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6986976 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b - # local_signature = 304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d1163 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e09c6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040048304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d116301483045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6986976 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100a135f9e8a5ed25f7277446c67956b00ce6f610ead2bdec2c2f686155b7814772022059f1f6e1a8b336a68efcc1af3fe4d422d4827332b5b067501b099c47b7b5b5ee + # local_signature = 30450221009ec15c687898bb4da8b3a833e5ab8bfc51ec6e9202aaa8e66611edfd4a85ed1102203d7183e45078b9735c93450bc3415d3e5a8c576141a711ec6ddcb4a893926bb7 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484e09c6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221009ec15c687898bb4da8b3a833e5ab8bfc51ec6e9202aaa8e66611edfd4a85ed1102203d7183e45078b9735c93450bc3415d3e5a8c576141a711ec6ddcb4a893926bb701483045022100a135f9e8a5ed25f7277446c67956b00ce6f610ead2bdec2c2f686155b7814772022059f1f6e1a8b336a68efcc1af3fe4d422d4827332b5b067501b099c47b7b5b5ee01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 5 - # signature for output 0 (htlc 0) - remote_htlc_signature = 30440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab343740 - # signature for output 1 (htlc 2) - remote_htlc_signature = 304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b0 - # signature for output 2 (htlc 1) - remote_htlc_signature = 304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d833 - # signature for output 3 (htlc 3) - remote_htlc_signature = 30450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d - # signature for output 4 (htlc 4) - remote_htlc_signature = 3045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f0 - # local_signature = 304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a - output htlc_success_tx 0: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab3437400147304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # local_signature = 304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac - output htlc_timeout_tx 2: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b00147304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 3045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877 - output htlc_success_tx 1: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d83301483045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # local_signature = 3045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb - output htlc_timeout_tx 3: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d01483045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f - output htlc_success_tx 4: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb604000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f00147304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 0) + remote_htlc_signature = 30450221008437627f9ad84ac67052e2a414a4367b8556fd1f94d8b02590f89f50525cd33502205b9c21ff6e7fc864f2352746ad8ba59182510819acb644e25b8a12fc37bbf24f + # signature for output 1 (HTLC 2) + remote_htlc_signature = 304402205a67f92bf6845cf2892b48d874ac1daf88a36495cf8a06f93d83180d930a6f75022031da1621d95c3f335cc06a3056cf960199dae600b7cf89088f65fc53cdbef28c + # signature for output 2 (HTLC 1) + remote_htlc_signature = 30440220437e21766054a3eef7f65690c5bcfa9920babbc5af92b819f772f6ea96df6c7402207173622024bd97328cfb26c6665e25c2f5d67c319443ccdc60c903217005d8c8 + # signature for output 3 (HTLC 3) + remote_htlc_signature = 304402207436e10737e4df499fc051686d3e11a5bb2310e4d1f1e691d287cef66514791202207cb58e71a6b7a42dd001b7e3ae672ea4f71ea3e1cd412b742e9124abb0739c64 + # signature for output 4 (HTLC 4) + remote_htlc_signature = 30450221009acd6a827a76bfee50806178dfe0495cd4e1d9c58279c194c7b01520fe68cb8d022024d439047c368883e570997a7d40f0b430cb5a742f507965e7d3063ae3feccca + # local_signature = 30440220344b0deb055230d01703e6c7acd45853c4af2328b49b5d8af4f88a060733406602202ea64f2a43d5751edfe75503cbc35a62e3141b5ed032fa03360faf4ca66f670b + output htlc_success_tx 0: 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008437627f9ad84ac67052e2a414a4367b8556fd1f94d8b02590f89f50525cd33502205b9c21ff6e7fc864f2352746ad8ba59182510819acb644e25b8a12fc37bbf24f014730440220344b0deb055230d01703e6c7acd45853c4af2328b49b5d8af4f88a060733406602202ea64f2a43d5751edfe75503cbc35a62e3141b5ed032fa03360faf4ca66f670b012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_signature = 30450221009e5e3822b0185c6799a95288c597b671d6cc69ab80f43740f00c6c3d0752bdda02206da947a74bd98f3175324dc56fdba86cc783703a120a6f0297537e60632f4c7f + output htlc_timeout_tx 2: 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe0100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205a67f92bf6845cf2892b48d874ac1daf88a36495cf8a06f93d83180d930a6f75022031da1621d95c3f335cc06a3056cf960199dae600b7cf89088f65fc53cdbef28c014830450221009e5e3822b0185c6799a95288c597b671d6cc69ab80f43740f00c6c3d0752bdda02206da947a74bd98f3175324dc56fdba86cc783703a120a6f0297537e60632f4c7f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100fcfc47e36b712624677626cef3dc1d67f6583bd46926a6398fe6b00b0c9a37760220525788257b187fc775c6370d04eadf34d06f3650a63f8df851cee0ecb47a1673 + output htlc_success_tx 1: 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220437e21766054a3eef7f65690c5bcfa9920babbc5af92b819f772f6ea96df6c7402207173622024bd97328cfb26c6665e25c2f5d67c319443ccdc60c903217005d8c801483045022100fcfc47e36b712624677626cef3dc1d67f6583bd46926a6398fe6b00b0c9a37760220525788257b187fc775c6370d04eadf34d06f3650a63f8df851cee0ecb47a1673012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 3045022100e78211b8409afb7255ffe37337da87f38646f1faebbdd61bc1920d69e3ead67a02201a626305adfcd16bfb7e9340928d9b6305464eab4aa4c4a3af6646e9b9f69dee + output htlc_timeout_tx 3: 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207436e10737e4df499fc051686d3e11a5bb2310e4d1f1e691d287cef66514791202207cb58e71a6b7a42dd001b7e3ae672ea4f71ea3e1cd412b742e9124abb0739c6401483045022100e78211b8409afb7255ffe37337da87f38646f1faebbdd61bc1920d69e3ead67a02201a626305adfcd16bfb7e9340928d9b6305464eab4aa4c4a3af6646e9b9f69dee01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3044022048762cf546bbfe474f1536365ea7c416e3c0389d60558bc9412cb148fb6ab68202207215d7083b75c96ff9d2b08c59c34e287b66820f530b486a9aa4cdd9c347d5b9 + output htlc_success_tx 4: 020000000001012cfb3e4788c206881d38f2996b6cb2109b5935acb527d14bdaa7b908afa9b2fe04000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009acd6a827a76bfee50806178dfe0495cd4e1d9c58279c194c7b01520fe68cb8d022024d439047c368883e570997a7d40f0b430cb5a742f507965e7d3063ae3feccca01473044022048762cf546bbfe474f1536365ea7c416e3c0389d60558bc9412cb148fb6ab68202207215d7083b75c96ff9d2b08c59c34e287b66820f530b486a9aa4cdd9c347d5b9012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 6 outputs untrimmed (minimum feerate) + name: commitment tx with six outputs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 648 @@ -97,30 +97,30 @@ # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6987086 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b057 - # local_signature = 3045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431104e9d6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de01473044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b05701475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6987086 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402203948f900a5506b8de36a4d8502f94f21dd84fd9c2314ab427d52feaa7a0a19f2022059b6a37a4adaa2c5419dc8aea63c6e2a2ec4c4bde46207f6dc1fcd22152fc6e5 + # local_signature = 3045022100b15f72908ba3382a34ca5b32519240a22300cc6015b6f9418635fb41f3d01d8802207adb331b9ed1575383dca0f2355e86c173802feecf8298fbea53b9d4610583e9 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4844e9d6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100b15f72908ba3382a34ca5b32519240a22300cc6015b6f9418635fb41f3d01d8802207adb331b9ed1575383dca0f2355e86c173802feecf8298fbea53b9d4610583e90147304402203948f900a5506b8de36a4d8502f94f21dd84fd9c2314ab427d52feaa7a0a19f2022059b6a37a4adaa2c5419dc8aea63c6e2a2ec4c4bde46207f6dc1fcd22152fc6e501475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 4 - # signature for output 0 (htlc 2) - remote_htlc_signature = 3044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada8 - # signature for output 1 (htlc 1) - remote_htlc_signature = 3045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d4 - # signature for output 2 (htlc 3) - remote_htlc_signature = 3045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c2 - # signature for output 3 (htlc 4) - remote_htlc_signature = 3044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f - # local_signature = 3045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc098 - output htlc_timeout_tx 2: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada801483045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc09801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0 - output htlc_success_tx 1: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d40147304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # local_signature = 304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda3 - output htlc_timeout_tx 3: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c20147304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda301008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6 - output htlc_success_tx 4: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f0147304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3045022100a031202f3be94678f0e998622ee95ebb6ada8da1e9a5110228b5e04a747351e4022010ca6a21e18314ed53cfaae3b1f51998552a61a468e596368829a50ce40110e0 + # signature for output 1 (HTLC 1) + remote_htlc_signature = 304402202361012a634aee7835c5ecdd6413dcffa8f404b7e77364c792cff984e4ee71e90220715c5e90baa08daa45a7439b1ee4fa4843ed77b19c058240b69406606d384124 + # signature for output 2 (HTLC 3) + remote_htlc_signature = 304402207e8e82cd71ed4febeb593732c260456836e97d81896153ecd2b3cf320ca6861702202dd4a30f68f98ced7cc56a36369ac1fdd978248c5ff4ed204fc00cc625532989 + # signature for output 3 (HTLC 4) + remote_htlc_signature = 3044022024cd52e4198c8ae0e414a86d86b5a65ea7450f2eb4e783096736d93395eca5ce022078f0094745b45be4d4b2b04dd5978c9e66ba49109e5704403e84aaf5f387d6be + # local_signature = 304502210097e1873b57267730154595187a34949d3744f52933070c74757005e61ce2112e02204ecfba2aa42d4f14bdf8bad4206bb97217b702e6c433e0e1b0ce6587e6d46ec6 + output htlc_timeout_tx 2: 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a031202f3be94678f0e998622ee95ebb6ada8da1e9a5110228b5e04a747351e4022010ca6a21e18314ed53cfaae3b1f51998552a61a468e596368829a50ce40110e00148304502210097e1873b57267730154595187a34949d3744f52933070c74757005e61ce2112e02204ecfba2aa42d4f14bdf8bad4206bb97217b702e6c433e0e1b0ce6587e6d46ec601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3044022019de73b00f1d818fb388e83b2c8c31f6bce35ac624e215bc12f88f9dc33edf48022006ff814bb9f700ee6abc3294e146fac3efd4f13f0005236b41c0a946ee00c9ae + output htlc_success_tx 1: 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402202361012a634aee7835c5ecdd6413dcffa8f404b7e77364c792cff984e4ee71e90220715c5e90baa08daa45a7439b1ee4fa4843ed77b19c058240b69406606d38412401473044022019de73b00f1d818fb388e83b2c8c31f6bce35ac624e215bc12f88f9dc33edf48022006ff814bb9f700ee6abc3294e146fac3efd4f13f0005236b41c0a946ee00c9ae012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 3045022100bd0be6100c4fd8f102ec220e1b053e4c4e2ecca25615490150007b40d314dc3902201a1e0ea266965b43164d9e6576f58fa6726d42883dd1c3996d2925c2e2260796 + output htlc_timeout_tx 3: 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e8e82cd71ed4febeb593732c260456836e97d81896153ecd2b3cf320ca6861702202dd4a30f68f98ced7cc56a36369ac1fdd978248c5ff4ed204fc00cc62553298901483045022100bd0be6100c4fd8f102ec220e1b053e4c4e2ecca25615490150007b40d314dc3902201a1e0ea266965b43164d9e6576f58fa6726d42883dd1c3996d2925c2e226079601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3045022100bbfb9d0a946d420807c86e985d636cceb16e71c3694ed186316251a00cbd807202207773223f9a337e145f64673825be9b30d07ef1542c82188b264bedcf7cda78c6 + output htlc_success_tx 4: 020000000001010f44041fdfba175987cf4e6135ba2a154e3b7fb96483dc0ed5efc0678e5b6bf103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022024cd52e4198c8ae0e414a86d86b5a65ea7450f2eb4e783096736d93395eca5ce022078f0094745b45be4d4b2b04dd5978c9e66ba49109e5704403e84aaf5f387d6be01483045022100bbfb9d0a946d420807c86e985d636cceb16e71c3694ed186316251a00cbd807202207773223f9a337e145f64673825be9b30d07ef1542c82188b264bedcf7cda78c6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 6 outputs untrimmed (maximum feerate) + name: commitment tx with six outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 2069 @@ -130,30 +130,30 @@ # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6985079 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb4 - # local_signature = 304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311077956a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea01473044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6985079 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304502210090b96a2498ce0c0f2fadbec2aab278fed54c1a7838df793ec4d2c78d96ec096202204fdd439c50f90d483baa7b68feeef4bd33bc277695405447bcd0bfb2ca34d7bc + # local_signature = 3045022100ad9a9bbbb75d506ca3b716b336ee3cf975dd7834fcf129d7dd188146eb58a8b4022061a759ee417339f7fe2ea1e8deb83abb6a74db31a09b7648a932a639cda23e33 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48477956a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100ad9a9bbbb75d506ca3b716b336ee3cf975dd7834fcf129d7dd188146eb58a8b4022061a759ee417339f7fe2ea1e8deb83abb6a74db31a09b7648a932a639cda23e330148304502210090b96a2498ce0c0f2fadbec2aab278fed54c1a7838df793ec4d2c78d96ec096202204fdd439c50f90d483baa7b68feeef4bd33bc277695405447bcd0bfb2ca34d7bc01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 4 - # signature for output 0 (htlc 2) - remote_htlc_signature = 3045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f992 - # signature for output 1 (htlc 1) - remote_htlc_signature = 3045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f4 - # signature for output 2 (htlc 3) - remote_htlc_signature = 3045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef18 - # signature for output 3 (htlc 4) - remote_htlc_signature = 30450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c - # local_signature = 3044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae66402 - output htlc_timeout_tx 2: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f99201473044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae6640201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 3045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f - output htlc_success_tx 1: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f401483045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 - # local_signature = 304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f9 - output htlc_timeout_tx 3: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef180147304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5 - output htlc_success_tx 4: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c0147304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3045022100f33513ee38abf1c582876f921f8fddc06acff48e04515532a32d3938de938ffd02203aa308a2c1863b7d6fdf53159a1465bf2e115c13152546cc5d74483ceaa7f699 + # signature for output 1 (HTLC 1) + remote_htlc_signature = 3045022100ce07682cf4b90093c22dc2d9ab2a77ad6803526b655ef857221cc96af5c9e0bf02200f501cee22e7a268af40b555d15a8237c9f36ad67ef1841daf9f6a0267b1e6df + # signature for output 2 (HTLC 3) + remote_htlc_signature = 3045022100e3e35492e55f82ec0bc2f317ffd7a486d1f7024330fe9743c3559fc39f32ef0c02203d1d4db651fc388a91d5ad8ecdd8e83673063bc8eefe27cfd8c189090e3a23e0 + # signature for output 3 (HTLC 4) + remote_htlc_signature = 304402207475aeb0212ef9bf5130b60937817ad88c9a87976988ef1f323f026148cc4a850220739fea17ad3257dcad72e509c73eebe86bee30b178467b9fdab213d631b109df + # local_signature = 3045022100a637902a5d4c9ba9e7c472a225337d5aac9e2e3f6744f76e237132e7619ba0400220035c60d784a031c0d9f6df66b7eab8726a5c25397399ee4aa960842059eb3f9d + output htlc_timeout_tx 2: 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f33513ee38abf1c582876f921f8fddc06acff48e04515532a32d3938de938ffd02203aa308a2c1863b7d6fdf53159a1465bf2e115c13152546cc5d74483ceaa7f69901483045022100a637902a5d4c9ba9e7c472a225337d5aac9e2e3f6744f76e237132e7619ba0400220035c60d784a031c0d9f6df66b7eab8726a5c25397399ee4aa960842059eb3f9d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100e57e46234f8782d3ff7aa593b4f7446fb5316c842e693dc63ee324fd49f6a1c302204a2f7b44c48bd26e1554422afae13153eb94b29d3687b733d18930615fb2db61 + output htlc_success_tx 1: 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ce07682cf4b90093c22dc2d9ab2a77ad6803526b655ef857221cc96af5c9e0bf02200f501cee22e7a268af40b555d15a8237c9f36ad67ef1841daf9f6a0267b1e6df01483045022100e57e46234f8782d3ff7aa593b4f7446fb5316c842e693dc63ee324fd49f6a1c302204a2f7b44c48bd26e1554422afae13153eb94b29d3687b733d18930615fb2db61012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 3044022068613fb1b98eb3aec7f44c5b115b12343c2f066c4277c82b5f873dfe68f37f50022028109b4650f3f528ca4bfe9a467aff2e3e43893b61b5159157119d5d95cf1c18 + output htlc_timeout_tx 3: 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e3e35492e55f82ec0bc2f317ffd7a486d1f7024330fe9743c3559fc39f32ef0c02203d1d4db651fc388a91d5ad8ecdd8e83673063bc8eefe27cfd8c189090e3a23e001473044022068613fb1b98eb3aec7f44c5b115b12343c2f066c4277c82b5f873dfe68f37f50022028109b4650f3f528ca4bfe9a467aff2e3e43893b61b5159157119d5d95cf1c1801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3045022100d315522e09e7d53d2a659a79cb67fef56d6c4bddf3f46df6772d0d20a7beb7c8022070bcc17e288607b6a72be0bd83368bb6d53488db266c1cdb4d72214e4f02ac33 + output htlc_success_tx 4: 02000000000101adbe717a63fb658add30ada1e6e12ed257637581898abe475c11d7bbcd65bd4d03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207475aeb0212ef9bf5130b60937817ad88c9a87976988ef1f323f026148cc4a850220739fea17ad3257dcad72e509c73eebe86bee30b178467b9fdab213d631b109df01483045022100d315522e09e7d53d2a659a79cb67fef56d6c4bddf3f46df6772d0d20a7beb7c8022070bcc17e288607b6a72be0bd83368bb6d53488db266c1cdb4d72214e4f02ac33012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 5 outputs untrimmed (minimum feerate) + name: commitment tx with five outputs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 2070 @@ -162,26 +162,26 @@ # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6985434 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f37526 - # local_signature = 30440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd0 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110da966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd001483045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f3752601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6985434 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402204ca1ba260dee913d318271d86e10ca0f5883026fb5653155cff600fb40895223022037b145204b7054a40e08bb1fefbd826f827b40838d3e501423bcc57924bcb50c + # local_signature = 3044022001014419b5ba00e083ac4e0a85f19afc848aacac2d483b4b525d15e2ae5adbfe022015ebddad6ee1e72b47cb09f3e78459da5be01ccccd95dceca0e056a00cc773c1 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484da966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022001014419b5ba00e083ac4e0a85f19afc848aacac2d483b4b525d15e2ae5adbfe022015ebddad6ee1e72b47cb09f3e78459da5be01ccccd95dceca0e056a00cc773c10147304402204ca1ba260dee913d318271d86e10ca0f5883026fb5653155cff600fb40895223022037b145204b7054a40e08bb1fefbd826f827b40838d3e501423bcc57924bcb50c01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 3 - # signature for output 0 (htlc 2) - remote_htlc_signature = 3045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f891 - # signature for output 1 (htlc 3) - remote_htlc_signature = 3044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf6 - # signature for output 2 (htlc 4) - remote_htlc_signature = 3045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a4 - # local_signature = 3045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb8 - output htlc_timeout_tx 2: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a2180000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f89101483045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 3045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d - output htlc_timeout_tx 3: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a218010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf601483045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04 - output htlc_success_tx 4: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a21802000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a40147304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 304402205f6b6d12d8d2529fb24f4445630566cf4abbd0f9330ab6c2bdb94222d6a2a0c502202f556258ae6f05b193749e4c541dfcc13b525a5422f6291f073f15617ba8579b + # signature for output 1 (HTLC 3) + remote_htlc_signature = 3045022100f960dfb1c9aee7ce1437efa65b523e399383e8149790e05d8fed27ff6e42fe0002202fe8613e062ffe0b0c518cc4101fba1c6de70f64a5bcc7ae663f2efae43b8546 + # signature for output 2 (HTLC 4) + remote_htlc_signature = 3045022100ae5fc7717ae684bc1fcf9020854e5dbe9842c9e7472879ac06ff95ac2bb10e4e022057728ada4c00083a3e65493fb5d50a232165948a1a0f530ef63185c2c8c56504 + # local_signature = 30440220150b11069454da70caf2492ded9e0065c9a57f25ac2a4c52657b1d15b6c6ed85022068a38833b603c8892717206383611bad210f1cbb4b1f87ea29c6c65b9e1cb3e5 + output htlc_timeout_tx 2: 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff0000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402205f6b6d12d8d2529fb24f4445630566cf4abbd0f9330ab6c2bdb94222d6a2a0c502202f556258ae6f05b193749e4c541dfcc13b525a5422f6291f073f15617ba8579b014730440220150b11069454da70caf2492ded9e0065c9a57f25ac2a4c52657b1d15b6c6ed85022068a38833b603c8892717206383611bad210f1cbb4b1f87ea29c6c65b9e1cb3e501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 30450221009a6ed18e6873bc3644332a6ee21c152a5b102821865350df7a8c74451a51f9f2022050d801fb4895d7d7fbf452824c0168347f5c0cbe821cf6a97a63af5b8b2563c6 + output htlc_timeout_tx 3: 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f960dfb1c9aee7ce1437efa65b523e399383e8149790e05d8fed27ff6e42fe0002202fe8613e062ffe0b0c518cc4101fba1c6de70f64a5bcc7ae663f2efae43b8546014830450221009a6ed18e6873bc3644332a6ee21c152a5b102821865350df7a8c74451a51f9f2022050d801fb4895d7d7fbf452824c0168347f5c0cbe821cf6a97a63af5b8b2563c601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 30440220408ad3009827a8fccf774cb285587686bfb2ed041f89a89453c311ce9c8ee0f902203c7392d9f8306d3a46522a66bd2723a7eb2628cb2d9b34d4c104f1766bf37502 + output htlc_success_tx 4: 02000000000101403ad7602b43293497a3a2235a12ecefda4f3a1f1d06e49b1786d945685de1ff02000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ae5fc7717ae684bc1fcf9020854e5dbe9842c9e7472879ac06ff95ac2bb10e4e022057728ada4c00083a3e65493fb5d50a232165948a1a0f530ef63185c2c8c56504014730440220408ad3009827a8fccf774cb285587686bfb2ed041f89a89453c311ce9c8ee0f902203c7392d9f8306d3a46522a66bd2723a7eb2628cb2d9b34d4c104f1766bf37502012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 5 outputs untrimmed (maximum feerate) + name: commitment tx with five outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 2194 @@ -190,26 +190,26 @@ # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6985280 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec86203953348 - # local_signature = 304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f7061 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311040966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f706101483045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec8620395334801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6985280 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402204bb3d6e279d71d9da414c82de42f1f954267c762b2e2eb8b76bc3be4ea07d4b0022014febc009c5edc8c3fc5d94015de163200f780046f1c293bfed8568f08b70fb3 + # local_signature = 3044022072c2e2b1c899b2242656a537dde2892fa3801be0d6df0a87836c550137acde8302201654aa1974d37a829083c3ba15088689f30b56d6a4f6cb14c7bad0ee3116d398 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e48440966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022072c2e2b1c899b2242656a537dde2892fa3801be0d6df0a87836c550137acde8302201654aa1974d37a829083c3ba15088689f30b56d6a4f6cb14c7bad0ee3116d3980147304402204bb3d6e279d71d9da414c82de42f1f954267c762b2e2eb8b76bc3be4ea07d4b0022014febc009c5edc8c3fc5d94015de163200f780046f1c293bfed8568f08b70fb301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 3 - # signature for output 0 (htlc 2) - remote_htlc_signature = 30450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf44 - # signature for output 1 (htlc 3) - remote_htlc_signature = 30440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d - # signature for output 2 (htlc 4) - remote_htlc_signature = 3045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a - # local_signature = 3044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f - output htlc_timeout_tx 2: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf4401473044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 - # local_signature = 304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c915 - output htlc_timeout_tx 3: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d0147304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c91501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 3045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0 - output htlc_success_tx 4: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a01483045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3045022100939726680351a7856c1bc386d4a1f422c7d29bd7b56afc139570f508474e6c40022023175a799ccf44c017fbaadb924c40b2a12115a5b7d0dfd3228df803a2de8450 + # signature for output 1 (HTLC 3) + remote_htlc_signature = 3044022021bb883bf324553d085ba2e821cad80c28ef8b303dbead8f98e548783c02d1600220638f9ef2a9bba25869afc923f4b5dc38be3bb459f9efa5d869392d5f7779a4a0 + # signature for output 2 (HTLC 4) + remote_htlc_signature = 3045022100c9e6f0454aa598b905a35e641a70cc9f67b5f38cc4b00843a041238c4a9f1c4a0220260a2822a62da97e44583e837245995ca2e36781769c52f19e498efbdcca262b + # local_signature = 304502210099c98c2edeeee6ec0fb5f3bea8b79bb016a2717afa9b5072370f34382de281d302206f5e2980a995e045cf90a547f0752a7ee99d48547bc135258fe7bc07e0154301 + output htlc_timeout_tx 2: 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100939726680351a7856c1bc386d4a1f422c7d29bd7b56afc139570f508474e6c40022023175a799ccf44c017fbaadb924c40b2a12115a5b7d0dfd3228df803a2de84500148304502210099c98c2edeeee6ec0fb5f3bea8b79bb016a2717afa9b5072370f34382de281d302206f5e2980a995e045cf90a547f0752a7ee99d48547bc135258fe7bc07e015430101008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100fd85bd7697b89c08ec12acc8ba89b23090637d83abd26ca37e01ae93e67c367302202b551fe69386116c47f984aab9c8dfd25d864dcde5d3389cfbef2447a85c4b77 + output htlc_timeout_tx 3: 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022021bb883bf324553d085ba2e821cad80c28ef8b303dbead8f98e548783c02d1600220638f9ef2a9bba25869afc923f4b5dc38be3bb459f9efa5d869392d5f7779a4a001483045022100fd85bd7697b89c08ec12acc8ba89b23090637d83abd26ca37e01ae93e67c367302202b551fe69386116c47f984aab9c8dfd25d864dcde5d3389cfbef2447a85c4b7701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 30450221008a9f2ea24cd455c2b64c1472a5fa83865b0a5f49a62b661801e884cf2849af8302204d44180e50bf6adfcf1c1e581d75af91aba4e28681ce4a5ee5f3cbf65eca10f3 + output htlc_success_tx 4: 02000000000101153cd825fdb3aa624bfe513e8031d5d08c5e582fb3d1d1fe8faf27d3eed410cd020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9e6f0454aa598b905a35e641a70cc9f67b5f38cc4b00843a041238c4a9f1c4a0220260a2822a62da97e44583e837245995ca2e36781769c52f19e498efbdcca262b014830450221008a9f2ea24cd455c2b64c1472a5fa83865b0a5f49a62b661801e884cf2849af8302204d44180e50bf6adfcf1c1e581d75af91aba4e28681ce4a5ee5f3cbf65eca10f3012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 4 outputs untrimmed (minimum feerate) + name: commitment tx with four outputs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 2195 @@ -217,22 +217,22 @@ # actual commitment transaction fee = 7344 # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6985656 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb7924298 - # local_signature = 304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a5429 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110b8976a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a54290147304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb792429801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6985656 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402201a8c1b1f9671cd9e46c7323a104d7047cc48d3ee80d40d4512e0c72b8dc65666022066d7f9a2ce18c9eb22d2739ffcce05721c767f9b607622a31b6ea5793ddce403 + # local_signature = 3044022044d592025b610c0d678f65032e87035cdfe89d1598c522cc32524ae8172417c30220749fef9d5b2ae8cdd91ece442ba8809bc891efedae2291e578475f97715d1767 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484b8976a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022044d592025b610c0d678f65032e87035cdfe89d1598c522cc32524ae8172417c30220749fef9d5b2ae8cdd91ece442ba8809bc891efedae2291e578475f97715d17670147304402201a8c1b1f9671cd9e46c7323a104d7047cc48d3ee80d40d4512e0c72b8dc65666022066d7f9a2ce18c9eb22d2739ffcce05721c767f9b607622a31b6ea5793ddce40301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 2 - # signature for output 0 (htlc 3) - remote_htlc_signature = 3045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc - # signature for output 1 (htlc 4) - remote_htlc_signature = 3045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92 - # local_signature = 3045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc704390 - output htlc_timeout_tx 3: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc01483045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc70439001008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 30440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8 - output htlc_success_tx 4: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92014730440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 3) + remote_htlc_signature = 3045022100e57b845066a06ee7c2cbfc29eabffe52daa9bf6f6de760066d04df9f9b250e0002202ffb197f0e6e0a77a75a9aff27014bd3de83b7f748d7efef986abe655e1dd50e + # signature for output 1 (HTLC 4) + remote_htlc_signature = 3045022100d193b7ecccad8057571620a0b1ffa6c48e9483311723b59cf536043b20bc51550220546d4bd37b3b101ecda14f6c907af46ec391abce1cd9c7ce22b1a62b534f2f2a + # local_signature = 3045022100ecc8c6529d0b2316d046f0f0757c1e1c25a636db168ec4f3aa1b9278df685dc0022067ae6b65e936f1337091f7b18a15935b608c5f2cdddb2f892ed0babfdd376d76 + output htlc_timeout_tx 3: 020000000001018130a10f09b13677ba2885a8bca32860f3a952e5912b829a473639b5a2c07b900000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e57b845066a06ee7c2cbfc29eabffe52daa9bf6f6de760066d04df9f9b250e0002202ffb197f0e6e0a77a75a9aff27014bd3de83b7f748d7efef986abe655e1dd50e01483045022100ecc8c6529d0b2316d046f0f0757c1e1c25a636db168ec4f3aa1b9278df685dc0022067ae6b65e936f1337091f7b18a15935b608c5f2cdddb2f892ed0babfdd376d7601008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3044022014d66f11f9cacf923807eba49542076c5fe5cccf252fb08fe98c78ef3ca6ab5402201b290dbe043cc512d9d78de074a5a129b8759bc6a6c546b190d120b690bd6e82 + output htlc_success_tx 4: 020000000001018130a10f09b13677ba2885a8bca32860f3a952e5912b829a473639b5a2c07b900100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d193b7ecccad8057571620a0b1ffa6c48e9483311723b59cf536043b20bc51550220546d4bd37b3b101ecda14f6c907af46ec391abce1cd9c7ce22b1a62b534f2f2a01473044022014d66f11f9cacf923807eba49542076c5fe5cccf252fb08fe98c78ef3ca6ab5402201b290dbe043cc512d9d78de074a5a129b8759bc6a6c546b190d120b690bd6e82012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 4 outputs untrimmed (maximum feerate) + name: commitment tx with four outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 3702 @@ -240,93 +240,93 @@ # actual commitment transaction fee = 8953 # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6984047 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c1 - # local_signature = 304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b0 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431106f916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b001483045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c101475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6984047 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304502210092a587aeb777f869e7ff0d7898ea619ee26a3dacd1f3672b945eea600be431100220077ee9eae3528d15251f2a52b607b189820e57a6ccfac8d1af502b132ee40169 + # local_signature = 3045022100e5efb73c32d32da2d79702299b6317de6fb24a60476e3855926d78484dd1b3c802203557cb66a42c944ef06e00bcc4da35a5bcb2f185aab0f8e403e519e1d66aaf75 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4846f916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100e5efb73c32d32da2d79702299b6317de6fb24a60476e3855926d78484dd1b3c802203557cb66a42c944ef06e00bcc4da35a5bcb2f185aab0f8e403e519e1d66aaf750148304502210092a587aeb777f869e7ff0d7898ea619ee26a3dacd1f3672b945eea600be431100220077ee9eae3528d15251f2a52b607b189820e57a6ccfac8d1af502b132ee4016901475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 2 - # signature for output 0 (htlc 3) - remote_htlc_signature = 3045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb - # signature for output 1 (htlc 4) - remote_htlc_signature = 3045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9 - # local_signature = 304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a - output htlc_timeout_tx 3: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb0147304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 - # local_signature = 30440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c - output htlc_success_tx 4: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9014730440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 3) + remote_htlc_signature = 304402206fa54c11f98c3bae1e93df43fc7affeb05b476bf8060c03e29c377c69bc08e8b0220672701cce50d5c379ff45a5d2cfe48ac44973adb066ac32608e21221d869bb89 + # signature for output 1 (HTLC 4) + remote_htlc_signature = 3044022057649739b0eb74d541ead0dfdb3d4b2c15aa192720031044c3434c67812e5ca902201e5ede42d960ae551707f4a6b34b09393cf4dee2418507daa022e3550dbb5817 + # local_signature = 304402206e36c683ebf2cb16bcef3d5439cf8b53cd97280a365ed8acd7abb85a8ba5f21c02206e8621edfc2a5766cbc96eb67fd501127ff163eb6b85518a39f7d4974aef126f + output htlc_timeout_tx 3: 020000000001018db483bff65c70ee71d8282aeec5a880e2e2b39e45772bda5460403095c62e3f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206fa54c11f98c3bae1e93df43fc7affeb05b476bf8060c03e29c377c69bc08e8b0220672701cce50d5c379ff45a5d2cfe48ac44973adb066ac32608e21221d869bb890147304402206e36c683ebf2cb16bcef3d5439cf8b53cd97280a365ed8acd7abb85a8ba5f21c02206e8621edfc2a5766cbc96eb67fd501127ff163eb6b85518a39f7d4974aef126f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 304402207faad26678c8850e01b4a0696d60841f7305e1832b786110ee9075cb92ed14a30220516ef8ee5dfa80824ea28cbcec0dd95f8b847146257c16960db98507db15ffdc + output htlc_success_tx 4: 020000000001018db483bff65c70ee71d8282aeec5a880e2e2b39e45772bda5460403095c62e3f0100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022057649739b0eb74d541ead0dfdb3d4b2c15aa192720031044c3434c67812e5ca902201e5ede42d960ae551707f4a6b34b09393cf4dee2418507daa022e3550dbb58170147304402207faad26678c8850e01b4a0696d60841f7305e1832b786110ee9075cb92ed14a30220516ef8ee5dfa80824ea28cbcec0dd95f8b847146257c16960db98507db15ffdc012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 3 outputs untrimmed (minimum feerate) + name: commitment tx with three outputs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 3703 # base commitment transaction fee = 3317 # actual commitment transaction fee = 11317 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6984683 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 30450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb0 - # local_signature = 3044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110eb936a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506014830450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6984683 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100b495d239772a237ff2cf354b1b11be152fd852704cb184e7356d13f2fb1e5e430220723db5cdb9cbd6ead7bfd3deb419cf41053a932418cbb22a67b581f40bc1f13e + # local_signature = 304402201b736d1773a124c745586217a75bed5f66c05716fbe8c7db4fdb3c3069741cdd02205083f39c321c1bcadfc8d97e3c791a66273d936abac0c6a2fde2ed46019508e1 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484eb936a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402201b736d1773a124c745586217a75bed5f66c05716fbe8c7db4fdb3c3069741cdd02205083f39c321c1bcadfc8d97e3c791a66273d936abac0c6a2fde2ed46019508e101483045022100b495d239772a237ff2cf354b1b11be152fd852704cb184e7356d13f2fb1e5e430220723db5cdb9cbd6ead7bfd3deb419cf41053a932418cbb22a67b581f40bc1f13e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 1 - # signature for output 0 (htlc 4) - remote_htlc_signature = 3044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd - # local_signature = 3045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724 - output htlc_success_tx 4: 020000000001011c076aa7fb3d7460d10df69432c904227ea84bbf3134d4ceee5fb0f135ef206d0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd01483045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 4) + remote_htlc_signature = 3045022100c34c61735f93f2e324cc873c3b248111ccf8f6db15d5969583757010d4ad2b4602207867bb919b2ddd6387873e425345c9b7fd18d1d66aba41f3607bc2896ef3c30a + # local_signature = 3045022100988c143e2110067117d2321bdd4bd16ca1734c98b29290d129384af0962b634e02206c1b02478878c5f547018b833986578f90c3e9be669fe5788ad0072a55acbb05 + output htlc_success_tx 4: 0200000000010120060e4a29579d429f0f27c17ee5f1ee282f20d706d6f90b63d35946d8f3029a0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c34c61735f93f2e324cc873c3b248111ccf8f6db15d5969583757010d4ad2b4602207867bb919b2ddd6387873e425345c9b7fd18d1d66aba41f3607bc2896ef3c30a01483045022100988c143e2110067117d2321bdd4bd16ca1734c98b29290d129384af0962b634e02206c1b02478878c5f547018b833986578f90c3e9be669fe5788ad0072a55acbb05012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 3 outputs untrimmed (maximum feerate) + name: commitment tx with three outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 4914 # base commitment transaction fee = 4402 # actual commitment transaction fee = 12402 # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 - # to-local amount 6983598 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e - # local_signature = 304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe26 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110ae8f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe260147304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6983598 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100b4b16d5f8cc9fc4c1aff48831e832a0d8990e133978a66e302c133550954a44d022073573ce127e2200d316f6b612803a5c0c97b8d20e1e44dbe2ac0dd2fb8c95244 + # local_signature = 3045022100d72638bc6308b88bb6d45861aae83e5b9ff6e10986546e13bce769c70036e2620220320be7c6d66d22f30b9fcd52af66531505b1310ca3b848c19285b38d8a1a8c19 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484ae8f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100d72638bc6308b88bb6d45861aae83e5b9ff6e10986546e13bce769c70036e2620220320be7c6d66d22f30b9fcd52af66531505b1310ca3b848c19285b38d8a1a8c1901483045022100b4b16d5f8cc9fc4c1aff48831e832a0d8990e133978a66e302c133550954a44d022073573ce127e2200d316f6b612803a5c0c97b8d20e1e44dbe2ac0dd2fb8c9524401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 1 - # signature for output 0 (htlc 4) - remote_htlc_signature = 3045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf - # local_signature = 304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68 - output htlc_success_tx 4: 0200000000010110a3fdcbcd5db477cd3ad465e7f501ffa8c437e8301f00a6061138590add757f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf0148304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + # signature for output 0 (HTLC 4) + remote_htlc_signature = 3045022100f43591c156038ba217756006bb3c55f7d113a325cdd7d9303c82115372858d68022016355b5aadf222bc8d12e426c75f4a03423917b2443a103eb2a498a3a2234374 + # local_signature = 30440220585dee80fafa264beac535c3c0bb5838ac348b156fdc982f86adc08dfc9bfd250220130abb82f9f295cc9ef423dcfef772fde2acd85d9df48cc538981d26a10a9c10 + output htlc_success_tx 4: 02000000000101a9172908eace869cc35128c31fc2ab502f72e4dff31aab23e0244c4b04b11ab00000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100f43591c156038ba217756006bb3c55f7d113a325cdd7d9303c82115372858d68022016355b5aadf222bc8d12e426c75f4a03423917b2443a103eb2a498a3a2234374014730440220585dee80fafa264beac535c3c0bb5838ac348b156fdc982f86adc08dfc9bfd250220130abb82f9f295cc9ef423dcfef772fde2acd85d9df48cc538981d26a10a9c10012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 - name: commitment tx with 2 outputs untrimmed (minimum feerate) + name: commitment tx with two outputs untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 4915 # base commitment transaction fee = 3558 # actual commitment transaction fee = 15558 - # to-local amount 6984442 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f8765552 - # local_signature = 3045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b9 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110fa926a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b90147304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f876555201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 6984442 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402203a286936e74870ca1459c700c71202af0381910a6bfab687ef494ef1bc3e02c902202506c362d0e3bee15e802aa729bf378e051644648253513f1c085b264cc2a720 + # local_signature = 30450221008a953551f4d67cb4df3037207fc082ddaf6be84d417b0bd14c80aab66f1b01a402207508796dc75034b2dee876fe01dc05a08b019f3e5d689ac8842ade2f1befccf5 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484fa926a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004830450221008a953551f4d67cb4df3037207fc082ddaf6be84d417b0bd14c80aab66f1b01a402207508796dc75034b2dee876fe01dc05a08b019f3e5d689ac8842ade2f1befccf50147304402203a286936e74870ca1459c700c71202af0381910a6bfab687ef494ef1bc3e02c902202506c362d0e3bee15e802aa729bf378e051644648253513f1c085b264cc2a72001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 - name: commitment tx with 2 outputs untrimmed (maximum feerate) + name: commitment tx with two outputs untrimmed (maximum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 9651180 # base commitment transaction fee = 6987454 # actual commitment transaction fee = 6999454 - # to-local amount 546 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd - # local_signature = 30440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b9 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b800222020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80ec0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311004004730440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b901473044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_local amount 546 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402200a8544eba1d216f5c5e530597665fa9bec56943c0f66d98fc3d028df52d84f7002201e45fa5c6bc3a506cc2553e7d1c0043a9811313fc39c954692c0d47cfce2bbd3 + # local_signature = 3045022100e11b638c05c650c2f63a421d36ef8756c5ce82f2184278643520311cdf50aa200220259565fb9c8e4a87ccaf17f27a3b9ca4f20625754a0920d9c6c239d8156a11de + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b800222020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80ec0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e4840400483045022100e11b638c05c650c2f63a421d36ef8756c5ce82f2184278643520311cdf50aa200220259565fb9c8e4a87ccaf17f27a3b9ca4f20625754a0920d9c6c239d8156a11de0147304402200a8544eba1d216f5c5e530597665fa9bec56943c0f66d98fc3d028df52d84f7002201e45fa5c6bc3a506cc2553e7d1c0043a9811313fc39c954692c0d47cfce2bbd301475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 - name: commitment tx with 1 output untrimmed (minimum feerate) + name: commitment tx with one output untrimmed (minimum feerate) to_local_msat: 6988000000 to_remote_msat: 3000000000 local_feerate_per_kw: 9651181 # base commitment transaction fee = 6987455 # actual commitment transaction fee = 7000000 - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e - # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a2 + # local_signature = 304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a379 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484040047304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a3790147304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 name: commitment tx with fee greater than funder amount @@ -335,10 +335,10 @@ local_feerate_per_kw: 9651936 # base commitment transaction fee = 6988001 # actual commitment transaction fee = 7000000 - # to-remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) - remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e - # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a2 + # local_signature = 304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a379 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484040047304402207e8d51e0c570a5868a78414f4e0cbfaed1106b171b9581542c30718ee4eb95ba02203af84194c97adf98898c9afe2f2ed4a7f8dba05a2dfab28ac9d9c604aa49a3790147304402202ade0142008309eb376736575ad58d03e5b115499709c6db0b46e36ff394b492022037b63d78d66404d6504d4c4ac13be346f3d1802928a6d3ad95a6a944227161a201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 num_htlcs: 0 name: commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage @@ -349,14 +349,14 @@ # HTLC 5 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 # HTLC 6 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 # HTLC 5 and 6 have CLTV 505 and 506, respectively, and preimage 0505050505050505050505050505050505050505050505050505050505050505 - output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2a8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110a79f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220586f17b3f3f6eec96a0dc7040f1a33174c707e032010421fa1965497cd1c81e602204476e6a4cc17d433e8ad82edff2ffa47c954ecb52aad9cc34382befb66f6e00001473044022069797b84fad1e0e7b02e1edeec80cb6e0b6f01170700bca273788715f55e0c560220260833550768b1db3ed88f8c005cf2560821b1879ec69834cd9765de25ec350601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 - # local_htlc_signature = 3044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2 - # remote_htlc_signature = 304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed69 - output htlc_success_tx 0: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a67440000000000000000000137030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050048304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed6901473044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 - # local_htlc_signature = 30440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f39 - # remote_htlc_signature = 3045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f - output htlc_timeout_tx 1: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674401000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f014730440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f3901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 - # local_htlc_signature = 3045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca9352 - # remote_htlc_signature = 304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f308 - output htlc_timeout_tx 2: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674402000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f30801483045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca935201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2a8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014cc1b07838e387deacd0e5232e1e8b49f4c29e484a79f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100ef89cb399f7ced1b146709599aa8400927f1bdb673103cf1852a87c0e98f8c7c02202e63734c5dec54694fcd6c63907f7d2d73c28626771edf3c999d195d3e0711370147304402206a3e262d6dea521491f84d18abc6abe574e95b036d80fbafb97118651e69646c02205d9cf95aaad1bd5d08e1f64c306a719a606273172a343bd10ec073dcabbff26401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # local_htlc_signature = 3045022100ecdff5abb3e083ca99fa6b24b37378bcb13416c111d870e7d1158a1f010d1e0002205a26b4f15e61965e130aabc9a538228d5706faf76ed1f7018962771428c9fa3f01 + # remote_htlc_signature = 304402205526a4f4f5ec29f48133e35b90b3acb10d498c0e984230356d36a232ec3a053e0220495af5d99e309440f9d5cf728ae241316ad0e92f57854989c3b25aba31197d2401 + output htlc_success_tx 0: 02000000000101a96af09bc9158e37a37fc40e7da5b8409be20e16775f360265aa6d38f7c273080000000000000000000137030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ecdff5abb3e083ca99fa6b24b37378bcb13416c111d870e7d1158a1f010d1e0002205a26b4f15e61965e130aabc9a538228d5706faf76ed1f7018962771428c9fa3f0147304402205526a4f4f5ec29f48133e35b90b3acb10d498c0e984230356d36a232ec3a053e0220495af5d99e309440f9d5cf728ae241316ad0e92f57854989c3b25aba31197d24012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_htlc_signature = 3045022100a0cf1044d519eeeb1de2dee50e87eefe0bb5b3db4559b59eab77cb09903b4dad02205d966470d3a667a4fe66ce51a546a648b07991def0cf2d7b7c2914ca0376919601 + # remote_htlc_signature = 3045022100d151eb6e30fce5809c21241519fabecec7ab2d85cf8b33aa8ded2124fd2fa90d02207b26497b401f46247a8e3a6faba5491f2b29b19ac1e9564d0e7e19449a44647f01 + output htlc_timeout_tx 1: 02000000000101a96af09bc9158e37a37fc40e7da5b8409be20e16775f360265aa6d38f7c2730801000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a0cf1044d519eeeb1de2dee50e87eefe0bb5b3db4559b59eab77cb09903b4dad02205d966470d3a667a4fe66ce51a546a648b07991def0cf2d7b7c2914ca0376919601483045022100d151eb6e30fce5809c21241519fabecec7ab2d85cf8b33aa8ded2124fd2fa90d02207b26497b401f46247a8e3a6faba5491f2b29b19ac1e9564d0e7e19449a44647f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 + # local_htlc_signature = 304402200f16c551721da544eca3fc513140b531b252e40119131f4aff099e08e3efcab702207a1717dcbdec0fe18d2b8400284678703e3a0da094392574c444624a9b70a82101 + # remote_htlc_signature = 304402206c82cb48cd9780b52cf0ba8bca538764497940406c3fc308a9ab6f4d401e1c8a02204779d819f8923c74e4cde80e1d70101673109e9e4096ee5c30909535be707a9801 + output htlc_timeout_tx 2: 02000000000101a96af09bc9158e37a37fc40e7da5b8409be20e16775f360265aa6d38f7c2730802000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402200f16c551721da544eca3fc513140b531b252e40119131f4aff099e08e3efcab702207a1717dcbdec0fe18d2b8400284678703e3a0da094392574c444624a9b70a8210147304402206c82cb48cd9780b52cf0ba8bca538764497940406c3fc308a9ab6f4d401e1c8a02204779d819f8923c74e4cde80e1d70101673109e9e4096ee5c30909535be707a9801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 num_htlcs: 3 \ No newline at end of file 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 1e40b34e0b..3ba16195ff 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 @@ -75,9 +75,9 @@ class TestVectorsSpec extends AnyFunSuite with Logging { val delayed_payment_basepoint = delayed_payment_basepoint_secret.publicKey val funding_privkey = PrivateKey(hex"30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901") val funding_pubkey = funding_privkey.publicKey - val per_commitment_point = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") - val payment_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) + val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) + val payment_privkey = payment_basepoint_secret val delayed_payment_privkey = Generators.derivePrivKey(delayed_payment_basepoint_secret, per_commitment_point) val revocation_pubkey = PublicKey(hex"0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19") val feerate_per_kw = 15000 @@ -115,8 +115,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { val revocation_basepoint = revocation_basepoint_secret.publicKey val funding_privkey = PrivateKey(hex"1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e1301") val funding_pubkey = funding_privkey.publicKey - val payment_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) - val per_commitment_point = PublicKey(hex"022c76692fd70814a8d1ed9dedc833318afaaed8188db4d14727e2e99bc619d325") + val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) + val payment_privkey = payment_basepoint_secret } val coinbaseTx = Transaction.read("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0100f2052a010000001976a9143ca33c2e4446f4a305f23c80df8ad1afdcf652f988ac00000000") @@ -161,8 +161,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { IncomingHtlc(UpdateAddHtlc(ByteVector32.Zeroes, 0, 4000000 msat, Crypto.sha256(paymentPreimages(4)), CltvExpiry(504), TestConstants.emptyOnionPacket)) ) val htlcScripts = htlcs.map { - case OutgoingHtlc(add) => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash)) - case IncomingHtlc(add) => Scripts.htlcReceived(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash), add.cltvExpiry) + case OutgoingHtlc(add) => Scripts.htlcOffered(Local.htlc_privkey.publicKey, Remote.htlc_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash)) + case IncomingHtlc(add) => Scripts.htlcReceived(Local.htlc_privkey.publicKey, Remote.htlc_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(add.paymentHash), add.cltvExpiry) } def dir2string(htlc: DirectedHtlc) = htlc match { @@ -183,16 +183,24 @@ class TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"local_feerate_per_kw: ${spec.feeratePerKw}") val outputs = Transactions.makeCommitTxOutputs( - true, Local.dustLimit, Local.revocation_pubkey, Local.toSelfDelay, - Local.delayed_payment_privkey.publicKey, Remote.payment_privkey.publicKey, - Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, // note: we have payment_key = htlc_key + localIsFunder = true, + localDustLimit = Local.dustLimit, + localRevocationPubkey = Local.revocation_pubkey, + toLocalDelay = Local.toSelfDelay, + localDelayedPaymentPubkey = Local.delayed_payment_privkey.publicKey, + remotePaymentPubkey = Remote.payment_privkey.publicKey, + localHtlcPubkey = Local.htlc_privkey.publicKey, + remoteHtlcPubkey = Remote.htlc_privkey.publicKey, spec) val commitTx = { val tx = Transactions.makeCommitTx( - commitmentInput, - Local.commitTxNumber, Local.payment_basepoint, Remote.payment_basepoint, - true, outputs) + commitTxInput = commitmentInput, + commitTxNumber = Local.commitTxNumber, + localPaymentBasePoint = Local.payment_basepoint, + remotePaymentBasePoint = Remote.payment_basepoint, + localIsFunder = true, + outputs = outputs) val local_sig = Transactions.sign(tx, Local.funding_privkey) val remote_sig = Transactions.sign(tx, Remote.funding_privkey) @@ -245,12 +253,12 @@ class TestVectorsSpec extends AnyFunSuite with Logging { htlcTxs.collect { case tx: HtlcSuccessTx => - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${remoteSig.dropRight(1).toHex}") case tx: HtlcTimeoutTx => - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.redeemScript)) logger.info(s"# signature for output ${tx.input.outPoint.index} (htlc $htlcIndex)") logger.info(s"remote_htlc_signature: ${remoteSig.dropRight(1).toHex}") @@ -259,8 +267,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { val signedTxs = htlcTxs collect { case tx: HtlcSuccessTx => //val tx = tx0.copy(tx = tx0.tx.copy(txOut = tx0.tx.txOut(0).copy(amount = Satoshi(545)) :: Nil)) - val localSig = Transactions.sign(tx, Local.payment_privkey) - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val localSig = Transactions.sign(tx, Local.htlc_privkey) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey) val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -269,8 +277,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"output htlc_success_tx ${htlcIndex}: ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => - val localSig = Transactions.sign(tx, Local.payment_privkey) - val remoteSig = Transactions.sign(tx, Remote.payment_privkey) + val localSig = Transactions.sign(tx, Local.htlc_privkey) + val remoteSig = Transactions.sign(tx, Remote.htlc_privkey) val tx1 = Transactions.addSigs(tx, localSig, remoteSig) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) logger.info(s"# local_signature = ${localSig.dropRight(1).toHex}") @@ -293,8 +301,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { } - test("commitment tx with all 5 htlcs untrimmed (minimum feerate)") { - val name = "commitment tx with all 5 htlcs untrimmed (minimum feerate)" + test("commitment tx with all five HTLCs untrimmed (minimum feerate)") { + val name = "commitment tx with all five HTLCs untrimmed (minimum feerate)" logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 0, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -303,8 +311,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(commitTx.tx == Transaction.read(results(name)("output commit_tx"))) } - test("commitment tx with 7 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 7 outputs untrimmed (maximum feerate)" + test("commitment tx with seven outputs untrimmed (maximum feerate)") { + val name = "commitment tx with seven outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -317,8 +325,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 6 outputs untrimmed (minimum feerate)") { - val name = "commitment tx with 6 outputs untrimmed (minimum feerate)" + test("commitment tx with six outputs untrimmed (minimum feerate)") { + val name = "commitment tx with six outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -331,8 +339,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 6 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 6 outputs untrimmed (maximum feerate)" + test("commitment tx with six outputs untrimmed (maximum feerate)") { + val name = "commitment tx with six outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -345,8 +353,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 5 outputs untrimmed (minimum feerate)") { - val name = "commitment tx with 5 outputs untrimmed (minimum feerate)" + test("commitment tx with five outputs untrimmed (minimum feerate)") { + val name = "commitment tx with five outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -359,8 +367,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 5 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 5 outputs untrimmed (maximum feerate)" + test("commitment tx with five outputs untrimmed (maximum feerate)") { + val name = "commitment tx with five outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -373,8 +381,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 4 outputs untrimmed (minimum feerate)") { - val name = "commitment tx with 4 outputs untrimmed (minimum feerate)" + test("commitment tx with four outputs untrimmed (minimum feerate)") { + val name = "commitment tx with four outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 1454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -387,8 +395,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 4 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 4 outputs untrimmed (maximum feerate)" + test("commitment tx with four outputs untrimmed (maximum feerate)") { + val name = "commitment tx with four outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -401,8 +409,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 3 outputs untrimmed (minimum feerate)") { - val name = "commitment tx with 3 outputs untrimmed (minimum feerate)" + test("commitment tx with three outputs untrimmed (minimum feerate)") { + val name = "commitment tx with three outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 2454999 / Transactions.htlcTimeoutWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -415,8 +423,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 3 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 3 outputs untrimmed (maximum feerate)" + test("commitment tx with three outputs untrimmed (maximum feerate)") { + val name = "commitment tx with three outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -429,8 +437,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 2 outputs untrimmed (minimum feerate)") { - val name = "commitment tx with 2 outputs untrimmed (minimum feerate)" + test("commitment tx with two outputs untrimmed (minimum feerate)") { + val name = "commitment tx with two outputs untrimmed (minimum feerate)" logger.info(s"name: $name") val feeratePerKw = 3454999 / Transactions.htlcSuccessWeight val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = feeratePerKw + 1, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -443,8 +451,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 2 outputs untrimmed (maximum feerate)") { - val name = "commitment tx with 2 outputs untrimmed (maximum feerate)" + test("commitment tx with two outputs untrimmed (maximum feerate)") { + val name = "commitment tx with two outputs untrimmed (maximum feerate)" logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651180, toLocal = 6988000000L msat, toRemote = 3000000000L msat) @@ -456,8 +464,8 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs.map(_.tx).toSet == check) } - test("commitment tx with 1 output untrimmed (minimum feerate)") { - val name = "commitment tx with 1 output untrimmed (minimum feerate)" + test("commitment tx with one output untrimmed (minimum feerate)") { + val name = "commitment tx with one output untrimmed (minimum feerate)" logger.info(s"name: $name") val spec = CommitmentSpec(htlcs = htlcs.toSet, feeratePerKw = 9651181, toLocal = 6988000000L msat, toRemote = 3000000000L msat) From e04b60e4912ef5ad787b57ab07814d3af63243cb Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Jun 2020 10:23:41 +0200 Subject: [PATCH 11/12] Add transaction test vectors for both default commitment type and static remotekey. --- ...test-vectors-default-commitment-format.txt | 362 ++++++++++++++++++ ...-test-vectors-static-remotekey-format.txt} | 0 .../eclair/transactions/TestVectorsSpec.scala | 51 ++- 3 files changed, 387 insertions(+), 26 deletions(-) create mode 100644 eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt rename eclair-core/src/test/resources/{bolt3-tx-test-vectors.txt => bolt3-tx-test-vectors-static-remotekey-format.txt} (100%) diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt new file mode 100644 index 0000000000..3a282cc49c --- /dev/null +++ b/eclair-core/src/test/resources/bolt3-tx-test-vectors-default-commitment-format.txt @@ -0,0 +1,362 @@ + name: simple commitment tx with no HTLCs + to_local_msat: 7000000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 15000 + # base commitment transaction fee = 10860 + # actual commitment transaction fee = 10860 + # to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c0 + # local_signature = 3044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c3836939 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 0 + + name: commitment tx with all five HTLCs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 0 + # base commitment transaction fee = 0 + # actual commitment transaction fee = 0 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 + # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606 + # local_signature = 30440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f06 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 5 + # signature for output 0 (HTLC 0) + remote_htlc_signature = 304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6 + # signature for output 1 (HTLC 2) + remote_htlc_signature = 3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b + # signature for output 2 (HTLC 1) + remote_htlc_signature = 304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202 + # signature for output 3 (HTLC 3) + remote_htlc_signature = 3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554 + # signature for output 4 (HTLC 4) + remote_htlc_signature = 304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d + # local_signature = 304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5 + output htlc_success_tx 0: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_signature = 3045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be5 + output htlc_timeout_tx 2: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da + output htlc_success_tx 1: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 30440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac08727 + output htlc_timeout_tx 3: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 30440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e + output htlc_success_tx 4: 020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with seven outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 647 + # base commitment transaction fee = 1024 + # actual commitment transaction fee = 1024 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 0 received amount 1000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 + # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6986976 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b + # local_signature = 304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d1163 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e09c6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040048304502210094bfd8f5572ac0157ec76a9551b6c5216a4538c07cd13a51af4a54cb26fa14320220768efce8ce6f4a5efac875142ff19237c011343670adf9c7ac69704a120d116301483045022100a5c01383d3ec646d97e40f44318d49def817fcd61a0ef18008a665b3e151785502203e648efddd5838981ef55ec954be69c4a652d021e6081a100d034de366815e9b01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 5 + # signature for output 0 (HTLC 0) + remote_htlc_signature = 30440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab343740 + # signature for output 1 (HTLC 2) + remote_htlc_signature = 304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b0 + # signature for output 2 (HTLC 1) + remote_htlc_signature = 304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d833 + # signature for output 3 (HTLC 3) + remote_htlc_signature = 30450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d + # signature for output 4 (HTLC 4) + remote_htlc_signature = 3045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f0 + # local_signature = 304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a + output htlc_success_tx 0: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220385a5afe75632f50128cbb029ee95c80156b5b4744beddc729ad339c9ca432c802202ba5f48550cad3379ac75b9b4fedb86a35baa6947f16ba5037fb8b11ab3437400147304402205999590b8a79fa346e003a68fd40366397119b2b0cdf37b149968d6bc6fbcc4702202b1e1fb5ab7864931caed4e732c359e0fe3d86a548b557be2246efb1708d579a012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_signature = 304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac + output htlc_timeout_tx 2: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb60100000000000000000124060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207ceb6678d4db33d2401fdc409959e57c16a6cb97a30261d9c61f29b8c58d34b90220084b4a17b4ca0e86f2d798b3698ca52de5621f2ce86f80bed79afa66874511b00147304402207ff03eb0127fc7c6cae49cc29e2a586b98d1e8969cf4a17dfa50b9c2647720b902205e2ecfda2252956c0ca32f175080e75e4e390e433feb1f8ce9f2ba55648a1dac01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877 + output htlc_success_tx 1: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6020000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a401b29a0dff0d18ec903502c13d83e7ec019450113f4a7655a4ce40d1f65ba0220217723a084e727b6ca0cc8b6c69c014a7e4a01fcdcba3e3993f462a3c574d83301483045022100d50d067ca625d54e62df533a8f9291736678d0b86c28a61bb2a80cf42e702d6e02202373dde7e00218eacdafb9415fe0e1071beec1857d1af3c6a201a44cbc47c877012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 3045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb + output htlc_timeout_tx 3: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb6030000000000000000010c0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009b1c987ba599ee3bde1dbca776b85481d70a78b681a8d84206723e2795c7cac002207aac84ad910f8598c4d1c0ea2e3399cf6627a4e3e90131315bc9f038451ce39d01483045022100db9dc65291077a52728c622987e9895b7241d4394d6dcb916d7600a3e8728c22022036ee3ee717ba0bb5c45ee84bc7bbf85c0f90f26ae4e4a25a6b4241afa8a3f1cb01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f + output htlc_success_tx 4: 020000000001018323148ce2419f21ca3d6780053747715832e18ac780931a514b187768882bb604000000000000000001da0d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100cc28030b59f0914f45b84caa983b6f8effa900c952310708c2b5b00781117022022027ba2ccdf94d03c6d48b327f183f6e28c8a214d089b9227f94ac4f85315274f00147304402202d1a3c0d31200265d2a2def2753ead4959ae20b4083e19553acfffa5dfab60bf022020ede134149504e15b88ab261a066de49848411e15e70f9e6a5462aec2949f8f012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with six outputs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 648 + # base commitment transaction fee = 914 + # actual commitment transaction fee = 1914 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6987086 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b057 + # local_signature = 3045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431104e9d6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a2270d5950c89ae0841233f6efea9c951898b301b2e89e0adbd2c687b9f32efa02207943d90f95b9610458e7c65a576e149750ff3accaacad004cd85e70b235e27de01473044022072714e2fbb93cdd1c42eb0828b4f2eff143f717d8f26e79d6ada4f0dcb681bbe02200911be4e5161dd6ebe59ff1c58e1997c4aea804f81db6b698821db6093d7b05701475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 4 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada8 + # signature for output 1 (HTLC 1) + remote_htlc_signature = 3045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d4 + # signature for output 2 (HTLC 3) + remote_htlc_signature = 3045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c2 + # signature for output 3 (HTLC 4) + remote_htlc_signature = 3044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f + # local_signature = 3045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc098 + output htlc_timeout_tx 2: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10000000000000000000123060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022062ef2e77591409d60d7817d9bb1e71d3c4a2931d1a6c7c8307422c84f001a251022022dad9726b0ae3fe92bda745a06f2c00f92342a186d84518588cf65f4dfaada801483045022100a4c574f00411dd2f978ca5cdc1b848c311cd7849c087ad2f21a5bce5e8cc5ae90220090ae39a9bce2fb8bc879d7e9f9022df249f41e25e51f1a9bf6447a9eeffc09801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0 + output htlc_success_tx 1: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd10100000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e968cbbb5f402ed389fdc7f6cd2a80ed650bb42c79aeb2a5678444af94f6c78502204b47a1cb24ab5b0b6fe69fe9cfc7dba07b9dd0d8b95f372c1d9435146a88f8d40147304402207679cf19790bea76a733d2fa0672bd43ab455687a068f815a3d237581f57139a0220683a1a799e102071c206b207735ca80f627ab83d6616b4bcd017c5d79ef3e7d0012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda3 + output htlc_timeout_tx 3: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd1020000000000000000010b0a0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100aa91932e305292cf9969cc23502bbf6cef83a5df39c95ad04a707c4f4fed5c7702207099fc0f3a9bfe1e7683c0e9aa5e76c5432eb20693bf4cb182f04d383dc9c8c20147304402200df76fea718745f3c529bac7fd37923e7309ce38b25c0781e4cf514dd9ef8dc802204172295739dbae9fe0474dcee3608e3433b4b2af3a2e6787108b02f894dcdda301008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6 + output htlc_success_tx 4: 02000000000101579c183eca9e8236a5d7f5dcd79cfec32c497fdc0ec61533cde99ecd436cadd103000000000000000001d90d0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022035cac88040a5bba420b1c4257235d5015309113460bc33f2853cd81ca36e632402202fc94fd3e81e9d34a9d01782a0284f3044370d03d60f3fc041e2da088d2de58f0147304402200daf2eb7afd355b4caf6fb08387b5f031940ea29d1a9f35071288a839c9039e4022067201b562456e7948616c13acb876b386b511599b58ac1d94d127f91c50463a6012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with six outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 2069 + # base commitment transaction fee = 2921 + # actual commitment transaction fee = 3921 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 1 received amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6985079 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb4 + # local_signature = 304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8006d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311077956a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203ca8f31c6a47519f83255dc69f1894d9a6d7476a19f498d31eaf0cd3a85eeb63022026fd92dc752b33905c4c838c528b692a8ad4ced959990b5d5ee2ff940fa90eea01473044022001d55e488b8b035b2dd29d50b65b530923a416d47f377284145bc8767b1b6a75022019bb53ddfe1cefaf156f924777eaaf8fdca1810695a7d0a247ad2afba8232eb401475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 4 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f992 + # signature for output 1 (HTLC 1) + remote_htlc_signature = 3045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f4 + # signature for output 2 (HTLC 3) + remote_htlc_signature = 3045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef18 + # signature for output 3 (HTLC 4) + remote_htlc_signature = 30450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c + # local_signature = 3044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae66402 + output htlc_timeout_tx 2: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0000000000000000000175020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d1cf354de41c1369336cf85b225ed033f1f8982a01be503668df756a7e668b66022001254144fb4d0eecc61908fccc3388891ba17c5d7a1a8c62bdd307e5a513f99201473044022056eb1af429660e45a1b0b66568cb8c4a3aa7e4c9c292d5d6c47f86ebf2c8838f022065c3ac4ebe980ca7a41148569be4ad8751b0a724a41405697ec55035dae6640201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f + output htlc_success_tx 1: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a0100000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d065569dcb94f090345402736385efeb8ea265131804beac06dd84d15dd2d6880220664feb0b4b2eb985fadb6ec7dc58c9334ea88ce599a9be760554a2d4b3b5d9f401483045022100914bb232cd4b2690ee3d6cb8c3713c4ac9c4fb925323068d8b07f67c8541f8d9022057152f5f1615b793d2d45aac7518989ae4fe970f28b9b5c77504799d25433f7f012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000 + # local_signature = 304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f9 + output htlc_timeout_tx 3: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a020000000000000000015d060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d4e69d363de993684eae7b37853c40722a4c1b4a7b588ad7b5d8a9b5006137a102207a069c628170ee34be5612747051bdcc087466dbaa68d5756ea81c10155aef180147304402200e362443f7af830b419771e8e1614fc391db3a4eb799989abfc5ab26d6fcd032022039ab0cad1c14dfbe9446bf847965e56fe016e0cbcf719fd18c1bfbf53ecbd9f901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5 + output htlc_success_tx 4: 02000000000101ca94a9ad516ebc0c4bdd7b6254871babfa978d5accafb554214137d398bfcf6a03000000000000000001f2090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221008ec888e36e4a4b3dc2ed6b823319855b2ae03006ca6ae0d9aa7e24bfc1d6f07102203b0f78885472a67ff4fe5916c0bb669487d659527509516fc3a08e87a2cc0a7c0147304402202c3e14282b84b02705dfd00a6da396c9fe8a8bcb1d3fdb4b20a4feba09440e8b02202b058b39aa9b0c865b22095edcd9ff1f71bbfe20aa4993755e54d042755ed0d5012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with five outputs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 2070 + # base commitment transaction fee = 2566 + # actual commitment transaction fee = 5566 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6985434 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f37526 + # local_signature = 30440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd0 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110da966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220443cb07f650aebbba14b8bc8d81e096712590f524c5991ac0ed3bbc8fd3bd0c7022028a635f548e3ca64b19b69b1ea00f05b22752f91daf0b6dab78e62ba52eb7fd001483045022100f2377f7a67b7fc7f4e2c0c9e3a7de935c32417f5668eda31ea1db401b7dc53030220415fdbc8e91d0f735e70c21952342742e25249b0d062d43efbfc564499f3752601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 3 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 3045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f891 + # signature for output 1 (HTLC 3) + remote_htlc_signature = 3044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf6 + # signature for output 2 (HTLC 4) + remote_htlc_signature = 3045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a4 + # local_signature = 3045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb8 + output htlc_timeout_tx 2: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a2180000000000000000000174020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100eed143b1ee4bed5dc3cde40afa5db3e7354cbf9c44054b5f713f729356f08cf7022077161d171c2bbd9badf3c9934de65a4918de03bbac1450f715275f75b103f89101483045022100a0d043ed533e7fb1911e0553d31a8e2f3e6de19dbc035257f29d747c5e02f1f5022030cd38d8e84282175d49c1ebe0470db3ebd59768cf40780a784e248a43904fb801008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 3045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d + output htlc_timeout_tx 3: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a218010000000000000000015c060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022071e9357619fd8d29a411dc053b326a5224c5d11268070e88ecb981b174747c7a02202b763ae29a9d0732fa8836dd8597439460b50472183f420021b768981b4f7cf601483045022100adb1d679f65f96178b59f23ed37d3b70443118f345224a07ecb043eee2acc157022034d24524fe857144a3bcfff3065a9994d0a6ec5f11c681e49431d573e242612d01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04 + output htlc_success_tx 4: 0200000000010140a83ce364747ff277f4d7595d8d15f708418798922c40bc2b056aca5485a21802000000000000000001f1090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100c9458a4d2cbb741705577deb0a890e5cb90ee141be0400d3162e533727c9cb2102206edcf765c5dc5e5f9b976ea8149bf8607b5a0efb30691138e1231302b640d2a40147304402200831422aa4e1ee6d55e0b894201770a8f8817a189356f2d70be76633ffa6a6f602200dd1b84a4855dc6727dd46c98daae43dfc70889d1ba7ef0087529a57c06e5e04012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with five outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 2194 + # base commitment transaction fee = 2720 + # actual commitment transaction fee = 5720 + # HTLC 2 offered amount 2000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6985280 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec86203953348 + # local_signature = 304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f7061 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005d007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311040966a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b1b010c109c2ecbe7feb2d259b9c4126bd5dc99ee693c422ec0a5781fe161ba0220571fe4e2c649dea9c7aaf7e49b382962f6a3494963c97d80fef9a430ca3f706101483045022100d33c4e541aa1d255d41ea9a3b443b3b822ad8f7f86862638aac1f69f8f760577022007e2a18e6931ce3d3a804b1c78eda1de17dbe1fb7a95488c9a4ec8620395334801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 3 + # signature for output 0 (HTLC 2) + remote_htlc_signature = 30450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf44 + # signature for output 1 (HTLC 3) + remote_htlc_signature = 30440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d + # signature for output 2 (HTLC 4) + remote_htlc_signature = 3045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a + # local_signature = 3044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f + output htlc_timeout_tx 2: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004830450221009ed2f0a67f99e29c3c8cf45c08207b765980697781bb727fe0b1416de0e7622902206052684229bc171419ed290f4b615c943f819c0262414e43c5b91dcf72ddcf4401473044022004ad5f04ae69c71b3b141d4db9d0d4c38d84009fb3cfeeae6efdad414487a9a0022042d3fe1388c1ff517d1da7fb4025663d372c14728ed52dc88608363450ff6a2f01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000 + # local_signature = 304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c915 + output htlc_timeout_tx 3: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a010000000000000000010a060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e05004730440220155d3b90c67c33a8321996a9be5b82431b0c126613be751d400669da9d5c696702204318448bcd48824439d2c6a70be6e5747446be47ff45977cf41672bdc9b6b12d0147304402201707050c870c1f77cc3ed58d6d71bf281de239e9eabd8ef0955bad0d7fe38dcc02204d36d80d0019b3a71e646a08fa4a5607761d341ae8be371946ebe437c289c91501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 3045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0 + output htlc_success_tx 4: 02000000000101fb824d4e4dafc0f567789dee3a6bce8d411fe80f5563d8cdfdcc7d7e4447d43a020000000000000000019a090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a12a9a473ece548584aabdd051779025a5ed4077c4b7aa376ec7a0b1645e5a48022039490b333f53b5b3e2ddde1d809e492cba2b3e5fc3a436cd3ffb4cd3d500fa5a01483045022100ff200bc934ab26ce9a559e998ceb0aee53bc40368e114ab9d3054d9960546e2802202496856ca163ac12c143110b6b3ac9d598df7254f2e17b3b94c3ab5301f4c3b0012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with four outputs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 2195 + # base commitment transaction fee = 2344 + # actual commitment transaction fee = 7344 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6985656 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb7924298 + # local_signature = 304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a5429 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110b8976a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402203b12d44254244b8ff3bb4129b0920fd45120ab42f553d9976394b099d500c99e02205e95bb7a3164852ef0c48f9e0eaf145218f8e2c41251b231f03cbdc4f29a54290147304402205e2f76d4657fb732c0dfc820a18a7301e368f5799e06b7828007633741bda6df0220458009ae59d0c6246065c419359e05eb2a4b4ef4a1b310cc912db44eb792429801475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 2 + # signature for output 0 (HTLC 3) + remote_htlc_signature = 3045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc + # signature for output 1 (HTLC 4) + remote_htlc_signature = 3045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92 + # local_signature = 3045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc704390 + output htlc_timeout_tx 3: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0000000000000000000109060000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100a8a78fa1016a5c5c3704f2e8908715a3cef66723fb95f3132ec4d2d05cd84fb4022025ac49287b0861ec21932405f5600cbce94313dbde0e6c5d5af1b3366d8afbfc01483045022100be6ae1977fd7b630a53623f3f25c542317ccfc2b971782802a4f1ef538eb22b402207edc4d0408f8f38fd3c7365d1cfc26511b7cd2d4fecd8b005fba3cd5bc70439001008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 30440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8 + output htlc_success_tx 4: 020000000001014e16c488fa158431c1a82e8f661240ec0a71ba0ce92f2721a6538c510226ad5c0100000000000000000199090000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e769cb156aa2f7515d126cef7a69968629620ce82afcaa9e210969de6850df4602200b16b3f3486a229a48aadde520dbee31ae340dbadaffae74fbb56681fef27b92014730440220665b9cb4a978c09d1ca8977a534999bc8a49da624d0c5439451dd69cde1a003d022070eae0620f01f3c1bd029cc1488da13fb40fdab76f396ccd335479a11c5276d8012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with four outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 3702 + # base commitment transaction fee = 3953 + # actual commitment transaction fee = 8953 + # HTLC 3 offered amount 3000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6984047 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c1 + # local_signature = 304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b0 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8004b80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431106f916a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402200e930a43c7951162dc15a2b7344f48091c74c70f7024e7116e900d8bcfba861c022066fa6cbda3929e21daa2e7e16a4b948db7e8919ef978402360d1095ffdaff7b001483045022100c1a3b0b60ca092ed5080121f26a74a20cec6bdee3f8e47bae973fcdceb3eda5502207d467a9873c939bf3aa758014ae67295fedbca52412633f7e5b2670fc7c381c101475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 2 + # signature for output 0 (HTLC 3) + remote_htlc_signature = 3045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb + # signature for output 1 (HTLC 4) + remote_htlc_signature = 3045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9 + # local_signature = 304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a + output htlc_timeout_tx 3: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100dfb73b4fe961b31a859b2bb1f4f15cabab9265016dd0272323dc6a9e85885c54022059a7b87c02861ee70662907f25ce11597d7b68d3399443a831ae40e777b76bdb0147304402202765b9c9ece4f127fa5407faf66da4c5ce2719cdbe47cd3175fc7d48b482e43d02205605125925e07bad1e41c618a4b434d72c88a164981c4b8af5eaf4ee9142ec3a01008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000 + # local_signature = 30440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c + output htlc_success_tx 4: 02000000000101b8de11eb51c22498fe39722c7227b6e55ff1a94146cf638458cb9bc6a060d3a30100000000000000000176050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100ea9dc2a7c3c3640334dab733bb4e036e32a3106dc707b24227874fa4f7da746802204d672f7ac0fe765931a8df10b81e53a3242dd32bd9dc9331eb4a596da87954e9014730440220048a41c660c4841693de037d00a407810389f4574b3286afb7bc392a438fa3f802200401d71fa87c64fe621b49ac07e3bf85157ac680acb977124da28652cc7f1a5c012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with three outputs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 3703 + # base commitment transaction fee = 3317 + # actual commitment transaction fee = 11317 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6984683 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 30450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb0 + # local_signature = 3044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110eb936a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022047305531dd44391dce03ae20f8735005c615eb077a974edb0059ea1a311857d602202e0ed6972fbdd1e8cb542b06e0929bc41b2ddf236e04cb75edd56151f4197506014830450221008b7c191dd46893b67b628e618d2dc8e81169d38bade310181ab77d7c94c6675e02203b4dd131fd7c9deb299560983dcdc485545c98f989f7ae8180c28289f9e6bdb001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 1 + # signature for output 0 (HTLC 4) + remote_htlc_signature = 3044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd + # local_signature = 3045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724 + output htlc_success_tx 4: 020000000001011c076aa7fb3d7460d10df69432c904227ea84bbf3134d4ceee5fb0f135ef206d0000000000000000000175050000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500473044022044f65cf833afdcb9d18795ca93f7230005777662539815b8a601eeb3e57129a902206a4bf3e53392affbba52640627defa8dc8af61c958c9e827b2798ab45828abdd01483045022100b94d931a811b32eeb885c28ddcf999ae1981893b21dd1329929543fe87ce793002206370107fdd151c5f2384f9ceb71b3107c69c74c8ed5a28a94a4ab2d27d3b0724012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with three outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 4914 + # base commitment transaction fee = 4402 + # actual commitment transaction fee = 12402 + # HTLC 4 received amount 4000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868 + # to_local amount 6983598 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e + # local_signature = 304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe26 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8003a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110ae8f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e040047304402206a2679efa3c7aaffd2a447fd0df7aba8792858b589750f6a1203f9259173198a022008d52a0e77a99ab533c36206cb15ad7aeb2aa72b93d4b571e728cb5ec2f6fe260147304402206d6cb93969d39177a09d5d45b583f34966195b77c7e585cf47ac5cce0c90cefb022031d71ae4e33a4e80df7f981d696fbdee517337806a3c7138b7491e2cbb077a0e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 1 + # signature for output 0 (HTLC 4) + remote_htlc_signature = 3045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf + # local_signature = 304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68 + output htlc_success_tx 4: 0200000000010110a3fdcbcd5db477cd3ad465e7f501ffa8c437e8301f00a6061138590add757f0000000000000000000122020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100fcb38506bfa11c02874092a843d0cc0a8613c23b639832564a5f69020cb0f6ba02206508b9e91eaa001425c190c68ee5f887e1ad5b1b314002e74db9dbd9e42dbecf0148304502210086e76b460ddd3cea10525fba298405d3fe11383e56966a5091811368362f689a02200f72ee75657915e0ede89c28709acd113ede9e1b7be520e3bc5cda425ecd6e68012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000 + + name: commitment tx with two outputs untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 4915 + # base commitment transaction fee = 3558 + # actual commitment transaction fee = 15558 + # to_local amount 6984442 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f8765552 + # local_signature = 3045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b9 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110fa926a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400483045022100a012691ba6cea2f73fa8bac37750477e66363c6d28813b0bb6da77c8eb3fb0270220365e99c51304b0b1a6ab9ea1c8500db186693e39ec1ad5743ee231b0138384b90147304402200769ba89c7330dfa4feba447b6e322305f12ac7dac70ec6ba997ed7c1b598d0802204fe8d337e7fee781f9b7b1a06e580b22f4f79d740059560191d7db53f876555201475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 0 + + name: commitment tx with two outputs untrimmed (maximum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 9651180 + # base commitment transaction fee = 6987454 + # actual commitment transaction fee = 6999454 + # to_local amount 546 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd + # local_signature = 30440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b9 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b800222020000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80ec0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311004004730440220514f977bf7edc442de8ce43ace9686e5ebdc0f893033f13e40fb46c8b8c6e1f90220188006227d175f5c35da0b092c57bea82537aed89f7778204dc5bacf4f29f2b901473044022037f83ff00c8e5fb18ae1f918ffc24e54581775a20ff1ae719297ef066c71caa9022039c529cccd89ff6c5ed1db799614533844bd6d101da503761c45c713996e3bbd01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 0 + + name: commitment tx with one output untrimmed (minimum feerate) + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 9651181 + # base commitment transaction fee = 6987455 + # actual commitment transaction fee = 7000000 + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e + # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 0 + + name: commitment tx with fee greater than funder amount + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 9651936 + # base commitment transaction fee = 6988001 + # actual commitment transaction fee = 7000000 + # to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b) + remote_signature = 3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e + # local_signature = 3044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b1 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + num_htlcs: 0 + + name: commitment tx with 3 htlc outputs, 2 offered having the same amount and preimage + to_local_msat: 6988000000 + to_remote_msat: 3000000000 + local_feerate_per_kw: 253 + # HTLC 0 received amount 1000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868 + # HTLC 5 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 + # HTLC 6 offered amount 5000000 wscript 76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868 + # HTLC 5 and 6 have CLTV 505 and 506, respectively, and preimage 0505050505050505050505050505050505050505050505050505050505050505 + output commit_tx: 02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8005e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2a8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121b8813000000000000220020305c12e1a0bc21e283c131cea1c66d68857d28b7b2fce0a6fbc40c164852121bc0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110a79f6a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220586f17b3f3f6eec96a0dc7040f1a33174c707e032010421fa1965497cd1c81e602204476e6a4cc17d433e8ad82edff2ffa47c954ecb52aad9cc34382befb66f6e00001473044022069797b84fad1e0e7b02e1edeec80cb6e0b6f01170700bca273788715f55e0c560220260833550768b1db3ed88f8c005cf2560821b1879ec69834cd9765de25ec350601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220 + # local_htlc_signature = 3044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2 + # remote_htlc_signature = 304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed69 + output htlc_success_tx 0: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a67440000000000000000000137030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050048304502210087be93ccb1fd373ebd489c2dbe5f3a95a5cd7b173255f63367d063843f0ed263022027de0683bc4ac9afe1cdff48f16bb1a8e27789328b534e1cc39bf4dd0448ed6901473044022077803ffab08308ac6ceaba6024855029f31d88e1420ece6aac1cf35258efb42702207db2cfe24de7b19a6e0e95082c595643fe18b7e0f0071297b2bb940807e774d2012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000 + # local_htlc_signature = 30440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f39 + # remote_htlc_signature = 3045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f + output htlc_timeout_tx 1: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674401000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100e75ab7170a3cd3266d0abd38566ca8e6f9102949a4394e497a855dd3c0d7158b02201b91a0f3b18c512d8223eb4fdccb563d1a64b828882ca0b183340ee36dff152f014730440220579ac2d6fdbdb0cb7bb07b9d2a52e9cb87aab26156b6290c504c1781f025daa00220310d6a5dc91c4c49cc362c1942b2af84130052c8ec597c1287d9cc18cd349f3901008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868f9010000 + # local_htlc_signature = 3045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca9352 + # remote_htlc_signature = 304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f308 + output htlc_timeout_tx 2: 020000000001012ac263f51690e216ddc176c047106d762592174f52ff72614b037f23124a674402000000000000000001e1120000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206afc9e5ad67a329bbbc63e00f9ce5fb90d07121cf57c4db0b9dd05881f1ee89d02200917f67fff5c5acfcd5e4825e6e651671af6c261eb69b6a44d159edae724f30801483045022100a62f73f9345a7821335391f00c64ebcba11628dd2e5058fc6f9827561381092402202c858823d25922fa2bafdac27f9bcd103d99d940823a581abe70bb0ab8ca935201008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9142002cc93ebefbb1b73f0af055dcc27a0b504ad7688ac6868fa010000 + num_htlcs: 3 \ No newline at end of file diff --git a/eclair-core/src/test/resources/bolt3-tx-test-vectors.txt b/eclair-core/src/test/resources/bolt3-tx-test-vectors-static-remotekey-format.txt similarity index 100% rename from eclair-core/src/test/resources/bolt3-tx-test-vectors.txt rename to eclair-core/src/test/resources/bolt3-tx-test-vectors-static-remotekey-format.txt 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 3ba16195ff..6464c2bdb3 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 @@ -18,6 +18,8 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto, Script, ScriptFlags, Transaction} +import fr.acinq.eclair.channel.ChannelVersion +import fr.acinq.eclair.channel.ChannelVersion.USE_STATIC_REMOTEKEY_BIT import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -29,12 +31,14 @@ import scodec.bits._ import scala.io.Source -class TestVectorsSpec extends AnyFunSuite with Logging { +trait TestVectorsSpec extends AnyFunSuite with Logging { + def filename: String + def channelVersion: ChannelVersion val results = collection.mutable.HashMap.empty[String, Map[String, String]] val current = collection.mutable.HashMap.empty[String, String] var name = "" - Source.fromInputStream(classOf[TestVectorsSpec].getResourceAsStream("/bolt3-tx-test-vectors.txt")).getLines().toArray.map(s => s.dropWhile(_.isWhitespace)).map(line => { + Source.fromInputStream(classOf[TestVectorsSpec].getResourceAsStream(filename)).getLines().toArray.map(s => s.dropWhile(_.isWhitespace)).map(line => { if (line.startsWith("name: ")) { val Array(_, n) = line.split(": ") if (!name.isEmpty) results.put(name, current.toMap) @@ -77,34 +81,15 @@ class TestVectorsSpec extends AnyFunSuite with Logging { val funding_pubkey = funding_privkey.publicKey val per_commitment_point = PublicKey(hex"025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) - val payment_privkey = payment_basepoint_secret + val payment_privkey = channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) match { + case true => payment_basepoint_secret + case false => Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) + } val delayed_payment_privkey = Generators.derivePrivKey(delayed_payment_basepoint_secret, per_commitment_point) val revocation_pubkey = PublicKey(hex"0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19") val feerate_per_kw = 15000 } - /* - - */ - object Remote { val commitTxNumber = 42 val toSelfDelay = CltvExpiryDelta(144) @@ -116,7 +101,10 @@ class TestVectorsSpec extends AnyFunSuite with Logging { val funding_privkey = PrivateKey(hex"1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e1301") val funding_pubkey = funding_privkey.publicKey val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) - val payment_privkey = payment_basepoint_secret + val payment_privkey = channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) match { + case true => payment_basepoint_secret + case false => Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) + } } val coinbaseTx = Transaction.read("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0100f2052a010000001976a9143ca33c2e4446f4a305f23c80df8ad1afdcf652f988ac00000000") @@ -515,3 +503,14 @@ class TestVectorsSpec extends AnyFunSuite with Logging { assert(htlcTxs(2).tx == Transaction.read(results(name)("output htlc_timeout_tx 2"))) } } + +class DefaultCommitmentTestVectorSpec extends TestVectorsSpec { + override def filename: String = "/bolt3-tx-test-vectors-default-commitment-format.txt" + override def channelVersion: ChannelVersion = ChannelVersion.STANDARD +} + + +class StaticRemoteKeyTestVectorSpec extends TestVectorsSpec { + override def filename: String = "/bolt3-tx-test-vectors-static-remotekey-format.txt" + override def channelVersion: ChannelVersion = ChannelVersion.USE_STATIC_REMOTEKEY +} \ No newline at end of file From bb051601b7cb0b2cd0ea416286c49525c9127c78 Mon Sep 17 00:00:00 2001 From: Andrea Date: Thu, 4 Jun 2020 13:38:50 +0200 Subject: [PATCH 12/12] Address PR feedback --- .../scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6464c2bdb3..f9422dc745 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 @@ -83,7 +83,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) val payment_privkey = channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) match { case true => payment_basepoint_secret - case false => Generators.derivePrivKey(payment_basepoint_secret, per_commitment_point) + case false => htlc_privkey } val delayed_payment_privkey = Generators.derivePrivKey(delayed_payment_basepoint_secret, per_commitment_point) val revocation_pubkey = PublicKey(hex"0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19") @@ -103,7 +103,7 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val htlc_privkey = Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) val payment_privkey = channelVersion.isSet(USE_STATIC_REMOTEKEY_BIT) match { case true => payment_basepoint_secret - case false => Generators.derivePrivKey(payment_basepoint_secret, Local.per_commitment_point) + case false => htlc_privkey } }