From 96a96cd994750c82ec826fb0209f635b73e414b5 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 14 Apr 2022 19:06:15 +0200 Subject: [PATCH 1/7] Add localChannelReserve and remoteChannelReserve This is easier to use than having to decide which params we should look into (local or remote). It will also be easier to integrate with dual funding. --- .../fr/acinq/eclair/channel/ChannelData.scala | 4 +-- .../fr/acinq/eclair/channel/Commitments.scala | 34 +++++++++++-------- .../fr/acinq/eclair/channel/Helpers.scala | 10 +++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../channel/fsm/ChannelOpenSingleFunder.scala | 6 ++-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +-- .../ChannelStateTestsHelperMethods.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 2 +- .../internal/channel/ChannelCodecsSpec.scala | 10 +++--- .../channel/version1/ChannelCodecs1Spec.scala | 10 +++--- 12 files changed, 47 insertions(+), 41 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 6a1ffd5bea..89feb5ea7f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -466,7 +466,7 @@ case class LocalParams(nodeId: PublicKey, fundingKeyPath: DeterministicWallet.KeyPath, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve: Satoshi, htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, @@ -481,7 +481,7 @@ case class LocalParams(nodeId: PublicKey, case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - channelReserve: Satoshi, + requestedChannelReserve: Satoshi, htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, 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 1e03c67679..999dbecd4e 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 @@ -211,6 +211,12 @@ case class Commitments(channelId: ByteVector32, val capacity: Satoshi = commitInput.txOut.amount + /** Channel reserve that applies to our funds. */ + val localChannelReserve: Satoshi = remoteParams.requestedChannelReserve + + /** Channel reserve that applies to our peer's funds. */ + val remoteChannelReserve: Satoshi = localParams.requestedChannelReserve + // NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on // top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see // https://github.com/lightningnetwork/lightning-rfc/issues/728 for details). @@ -241,7 +247,7 @@ case class Commitments(channelId: ByteVector32, // we need to base the next current commitment on the last sig we sent, even if we didn't yet receive their revocation val remoteCommit1 = remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit) val reduced = CommitmentSpec.reduce(remoteCommit1.spec, remoteChanges.acked, localChanges.proposed) - val balanceNoFees = (reduced.toRemote - remoteParams.channelReserve).max(0 msat) + val balanceNoFees = (reduced.toRemote - localChannelReserve).max(0 msat) if (localParams.isInitiator) { // The initiator always pays the on-chain fees, so we must subtract that from the amount we can send. val commitFees = commitTxTotalCostMsat(remoteParams.dustLimit, reduced, commitmentFormat) @@ -267,7 +273,7 @@ case class Commitments(channelId: ByteVector32, lazy val availableBalanceForReceive: MilliSatoshi = { val reduced = CommitmentSpec.reduce(localCommit.spec, localChanges.acked, remoteChanges.proposed) - val balanceNoFees = (reduced.toRemote - localParams.channelReserve).max(0 msat) + val balanceNoFees = (reduced.toRemote - remoteChannelReserve).max(0 msat) if (localParams.isInitiator) { // The non-initiator doesn't pay on-chain fees so we don't take those into account when receiving. balanceNoFees @@ -372,15 +378,15 @@ object Commitments { val funderFeeBuffer = commitTxTotalCostMsat(commitments1.remoteParams.dustLimit, reduced.copy(commitTxFeerate = reduced.commitTxFeerate * 2), commitments.commitmentFormat) + htlcOutputFee(reduced.commitTxFeerate * 2, commitments.commitmentFormat) // NB: increasing the feerate can actually remove htlcs from the commit tx (if they fall below the trim threshold) // which may result in a lower commit tx fee; this is why we take the max of the two. - val missingForSender = reduced.toRemote - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) - val missingForReceiver = reduced.toLocal - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForSender = reduced.toRemote - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees.max(funderFeeBuffer.truncateToSatoshi) else 0.sat) + val missingForReceiver = reduced.toLocal - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) if (missingForSender < 0.msat) { - return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) + return Left(InsufficientFunds(commitments.channelId, amount = cmd.amount, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = if (commitments1.localParams.isInitiator) fees else 0.sat)) } else if (missingForReceiver < 0.msat) { if (commitments.localParams.isInitiator) { // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } else { - return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = fees)) } } @@ -441,13 +447,13 @@ object Commitments { val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) // NB: we don't enforce the funderFeeReserve (see sendAdd) because it would confuse a remote initiator that doesn't have this mitigation in place // We could enforce it once we're confident a large portion of the network implements it. - val missingForSender = reduced.toRemote - commitments1.localParams.channelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) - val missingForReceiver = reduced.toLocal - commitments1.remoteParams.channelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) + val missingForSender = reduced.toRemote - commitments1.remoteChannelReserve - (if (commitments1.localParams.isInitiator) 0.sat else fees) + val missingForReceiver = reduced.toLocal - commitments1.localChannelReserve - (if (commitments1.localParams.isInitiator) fees else 0.sat) if (missingForSender < 0.sat) { - return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) + return Left(InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.remoteChannelReserve, fees = if (commitments1.localParams.isInitiator) 0.sat else fees)) } else if (missingForReceiver < 0.sat) { if (commitments.localParams.isInitiator) { - return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.localChannelReserve, fees = fees)) } else { // receiver is not the channel initiator; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment } @@ -558,9 +564,9 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee // we look from remote's point of view, so if local is initiator remote doesn't pay the fees val fees = commitTxTotalCost(commitments1.remoteParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.localChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we avoid sending this fee update @@ -608,9 +614,9 @@ object Commitments { // a node cannot spend pending incoming htlcs, and need to keep funds above the reserve required by the counterparty, after paying the fee val fees = commitTxTotalCost(commitments1.localParams.dustLimit, reduced, commitments.commitmentFormat) - val missing = reduced.toRemote.truncateToSatoshi - commitments1.localParams.channelReserve - fees + val missing = reduced.toRemote.truncateToSatoshi - commitments1.remoteChannelReserve - fees if (missing < 0.sat) { - return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(commitments.channelId, missing = -missing, reserve = commitments1.remoteChannelReserve, fees = fees)) } // if we would overflow our dust exposure with the new feerate, we reject this fee update 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 b6c6f249fe..975fb5ebb3 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 @@ -17,10 +17,10 @@ package fr.acinq.eclair.channel import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw} @@ -254,8 +254,8 @@ object Helpers { } val toRemoteSatoshis = remoteCommit.spec.toRemote.truncateToSatoshi // NB: this is an approximation (we don't take network fees into account) - val result = toRemoteSatoshis > commitments.remoteParams.channelReserve - log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") + val result = toRemoteSatoshis > commitments.localChannelReserve + log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.localChannelReserve} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}") result } @@ -300,9 +300,9 @@ object Helpers { // they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! val toRemoteMsat = remoteSpec.toLocal val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat) - val missing = toRemoteMsat.truncateToSatoshi - localParams.channelReserve - fees + val missing = toRemoteMsat.truncateToSatoshi - localParams.requestedChannelReserve - fees if (missing < Satoshi(0)) { - return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.channelReserve, fees = fees)) + return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = localParams.requestedChannelReserve, fees = fees)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 7b7547a5be..1281c48650 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -223,7 +223,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val pushMsat = pushMsat, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, + channelReserveSatoshis = localParams.requestedChannelReserve, htlcMinimumMsat = localParams.htlcMinimum, feeratePerKw = initialFeeratePerKw, toSelfDelay = localParams.toSelfDelay, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 932f8016a1..b00cfcea2d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -87,7 +87,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.channelReserve, + channelReserveSatoshis = localParams.requestedChannelReserve, minimumDepth = minimumDepth, htlcMinimumMsat = localParams.htlcMinimum, toSelfDelay = localParams.toSelfDelay, @@ -106,7 +106,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = open.dustLimitSatoshis, maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - channelReserve = open.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + requestedChannelReserve = open.channelReserveSatoshis, // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = open.htlcMinimumMsat, toSelfDelay = open.toSelfDelay, maxAcceptedHtlcs = open.maxAcceptedHtlcs, @@ -139,7 +139,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = accept.dustLimitSatoshis, maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - channelReserve = accept.channelReserveSatoshis, // remote requires local to keep this much satoshis as direct payment + requestedChannelReserve = accept.channelReserveSatoshis, // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = accept.htlcMinimumMsat, toSelfDelay = accept.toSelfDelay, maxAcceptedHtlcs = accept.maxAcceptedHtlcs, 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 72735a7b95..354a7de3d0 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 @@ -506,7 +506,7 @@ object Peer { nodeParams.channelKeyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key paths end differently dustLimit = nodeParams.channelConf.dustLimit, maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, - channelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + requestedChannelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit htlcMinimum = nodeParams.channelConf.htlcMinimum, toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, 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 b77d9fa3af..4c99f1b6f7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -208,7 +208,7 @@ object TestConstants { isInitiator = true, fundingSatoshis ).copy( - channelReserve = 10000 sat // Bob will need to keep that much satoshis as direct payment + requestedChannelReserve = 10_000 sat // Bob will need to keep that much satoshis in his balance ) } @@ -346,7 +346,7 @@ object TestConstants { isInitiator = false, fundingSatoshis ).copy( - channelReserve = 20000 sat // Alice will need to keep that much satoshis as direct payment + requestedChannelReserve = 20_000 sat // Alice will need to keep that much satoshis in her balance ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 31444f0cad..576de7975b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -227,7 +227,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.channelReserve).max(0 msat)) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == (pushMsat - aliceParams.requestedChannelReserve).max(0 msat)) // x2 because alice and bob share the same relayer channelUpdateListener.expectMsgType[LocalChannelUpdate] channelUpdateListener.expectMsgType[LocalChannelUpdate] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 21ec61710e..d39673f677 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -106,7 +106,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun import f._ val fees = Transactions.weight2fee(TestConstants.feeratePerKw, Transactions.DefaultCommitmentFormat.commitWeight) val bobParams = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams - val reserve = bobParams.channelReserve + val reserve = bobParams.requestedChannelReserve val missing = 100.sat - fees - reserve val fundingCreated = alice2bob.expectMsgType[FundingCreated] alice2bob.forward(bob) 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 5d3baa9bc3..a209471de2 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 @@ -257,7 +257,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // but this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more val failedAdd = CMD_ADD_HTLC(sender.ref, 11000000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) bob ! failedAdd - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 10000 sat, 22720 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 1360 sat, 20000 sat, 22720 sat) sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 4ea55a1aaa..6194b4f105 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -135,11 +135,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"channelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"channelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -256,7 +256,7 @@ object ChannelCodecsSpec { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - channelReserve = 10000 sat, + requestedChannelReserve = 10000 sat, htlcMinimum = 10000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, @@ -269,7 +269,7 @@ object ChannelCodecsSpec { nodeId = randomKey().publicKey, dustLimit = 546 sat, maxHtlcValueInFlightMsat = UInt64(5000000), - channelReserve = 10000 sat, + requestedChannelReserve = 10000 sat, htlcMinimum = 5000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index 49733b5114..320129a898 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -51,7 +51,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { assert(channelVersionCodec.encode(ChannelVersion.ANCHOR_OUTPUTS) === Attempt.successful(hex"00000007".bits)) } - test("encode/decode localparams") { + test("encode/decode local params") { def roundtrip(localParams: LocalParams, codec: Codec[LocalParams]) = { val encoded = codec.encode(localParams).require val decoded = codec.decode(encoded).require @@ -63,7 +63,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve = Satoshi(Random.nextInt(Int.MaxValue)), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -78,12 +78,12 @@ class ChannelCodecs1Spec extends AnyFunSuite { roundtrip(o, localParamsCodec(ChannelVersion.ANCHOR_OUTPUTS)) } - test("encode/decode remoteparams") { + test("encode/decode remote params") { val o = RemoteParams( nodeId = randomKey().publicKey, dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - channelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve = Satoshi(Random.nextInt(Int.MaxValue)), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -98,7 +98,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { val decoded = remoteParamsCodec.decodeValue(encoded).require assert(o === decoded) - // Backwards-compatibility: decode remoteparams with global features. + // Backwards-compatibility: decode remote params with global features. val withGlobalFeatures = hex"03c70c3b813815a8b79f41622b6f2c343fa24d94fb35fa7110bbb3d4d59cd9612e0000000059844cbc000000001b1524ea000000001503cbac000000006b75d3272e38777e029fa4e94066163024177311de7ba1befec2e48b473c387bbcee1484bf276a54460215e3dfb8e6f262222c5f343f5e38c5c9a43d2594c7f06dd7ac1a4326c665dd050347aba4d56d7007a7dcf03594423dccba9ed700d11e665d261594e1154203df31020d457ee336ba6eeb328d00f1b8bd8bfefb8a4dcd5af6db4c438b7ec5106c7edc0380df17e1beb0f238e51a39122ac4c6fb57f3c4f5b7bc9432f991b1ef4a8af3570002020000018a" val withGlobalFeaturesDecoded = remoteParamsCodec.decode(withGlobalFeatures.bits).require.value assert(withGlobalFeaturesDecoded.initFeatures.toByteVector === hex"028a") From f4804b0cf0d750f8d10ed0702137334e9cccca1b Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 15 Apr 2022 11:25:03 +0200 Subject: [PATCH 2/7] Rename initialFeeratePerKw This name was very confusing. This feerate only applies to the commit tx, so we make that explicit. --- .../fr/acinq/eclair/channel/ChannelData.scala | 8 ++++---- .../fr/acinq/eclair/channel/ChannelEvents.scala | 2 +- .../scala/fr/acinq/eclair/channel/Helpers.scala | 6 +++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 6 +++--- .../channel/fsm/ChannelOpenSingleFunder.scala | 14 +++++++------- .../fr/acinq/eclair/json/JsonSerializers.scala | 4 ++-- .../states/ChannelStateTestsHelperMethods.scala | 6 +++--- .../states/a/WaitForAcceptChannelStateSpec.scala | 4 ++-- .../states/a/WaitForOpenChannelStateSpec.scala | 4 ++-- .../test/scala/fr/acinq/eclair/io/PeerSpec.scala | 8 ++++---- .../scala/fr/acinq/eclair/api/ApiServiceSpec.scala | 2 +- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 89feb5ea7f..06fbf39573 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -78,8 +78,8 @@ case object ERR_INFORMATION_LEAK extends ChannelState case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, - fundingTxFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, + fundingTxFeerate: FeeratePerKw, localParams: LocalParams, remote: ActorRef, remoteInit: Init, @@ -387,7 +387,7 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32 remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, @@ -399,7 +399,7 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, remoteParams: RemoteParams, fundingAmount: Satoshi, pushAmount: MilliSatoshi, - initialFeeratePerKw: FeeratePerKw, + commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: ChannelFlags, channelConfig: ChannelConfig, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 671de2a3f0..236acffead 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate} trait ChannelEvent -case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent +case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isInitiator: Boolean, temporaryChannelId: ByteVector32, commitTxFeerate: FeeratePerKw, fundingTxFeerate: Option[FeeratePerKw]) extends ChannelEvent // This trait can be used by non-standard channels to inject themselves into Register actor and thus make them usable for routing trait AbstractChannelRestored extends ChannelEvent { 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 975fb5ebb3..4f884b0317 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -289,12 +289,12 @@ object Helpers { * * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ - def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { + def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingAmount: Satoshi, pushMsat: MilliSatoshi, commitTxFeerate: FeeratePerKw, fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { val toLocalMsat = if (localParams.isInitiator) fundingAmount.toMilliSatoshi - pushMsat else pushMsat val toRemoteMsat = if (localParams.isInitiator) pushMsat else fundingAmount.toMilliSatoshi - pushMsat - val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toLocalMsat, toRemote = toRemoteMsat) - val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], initialFeeratePerKw, toLocal = toRemoteMsat, toRemote = toLocalMsat) + val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat) + val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat) if (!localParams.isInitiator) { // they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it! diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 1281c48650..d3402066a2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -208,8 +208,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, commitTxFeerate, Some(fundingTxFeerate))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey @@ -225,7 +225,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, channelReserveSatoshis = localParams.requestedChannelReserve, htlcMinimumMsat = localParams.htlcMinimum, - feeratePerKw = initialFeeratePerKw, + feeratePerKw = commitTxFeerate, toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = fundingPubKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index b00cfcea2d..ce0f4219fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -129,7 +129,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, _, remoteInit, _, channelConfig, channelType), open)) => Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) @@ -153,8 +153,8 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { log.debug("remote params: {}", remoteParams) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => @@ -175,9 +175,9 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") @@ -220,9 +220,9 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) - Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, commitTxFeerate, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index b696715fe7..1acbcff540 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -400,8 +400,8 @@ object ChannelEventSerializer extends MinimalSerializer({ JField("remoteNodeId", JString(e.remoteNodeId.toString())), JField("isInitiator", JBool(e.isInitiator)), JField("temporaryChannelId", JString(e.temporaryChannelId.toHex)), - JField("initialFeeratePerKw", JLong(e.initialFeeratePerKw.toLong)), - JField("fundingTxFeeratePerKw", e.fundingTxFeeratePerKw.map(f => JLong(f.toLong)).getOrElse(JNothing)) + JField("commitTxFeeratePerKw", JLong(e.commitTxFeerate.toLong)), + JField("fundingTxFeeratePerKw", e.fundingTxFeerate.map(f => JLong(f.toLong)).getOrElse(JNothing)) ) case e: ChannelStateChanged => JObject( JField("type", JString("channel-state-changed")), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 576de7975b..93bd392c3b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -20,9 +20,9 @@ import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorContext, ActorRef} import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import com.softwaremill.quicklens.ModifyPimp +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, SatoshiLong, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -186,7 +186,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) - val initialFeeratePerKw = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { (TestConstants.fundingSatoshis, 0.msat) } else { @@ -195,7 +195,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) 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 541cfd753a..11f6dc52ba 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 @@ -58,12 +58,12 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 0291fbfc34..84ea97e2ce 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 @@ -52,11 +52,11 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, defaultChannelType) = computeFeatures(setup, test.tags) val channelType = if (test.tags.contains("standard-channel-type")) ChannelTypes.Standard else defaultChannelType - val initialFeeratePerKw = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw + val commitTxFeerate = if (channelType == ChannelTypes.AnchorOutputs || channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) 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 8f998f0a34..f5387b4c77 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 @@ -416,8 +416,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelType === ChannelTypes.AnchorOutputs) assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => @@ -434,8 +434,8 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) assert(init.fundingAmount === 15000.sat) - assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) - assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => 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 be381b571f..286d5537c5 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 @@ -1154,7 +1154,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM wsClient.expectMessage(expectedSerializedPset) val chcr = ChannelCreated(system.deadLetters, system.deadLetters, bobNodeId, isInitiator = true, ByteVector32.One, FeeratePerKw(25 sat), Some(FeeratePerKw(20 sat))) - val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","initialFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" + val expectedSerializedChcr = """{"type":"channel-opened","remoteNodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","isInitiator":true,"temporaryChannelId":"0100000000000000000000000000000000000000000000000000000000000000","commitTxFeeratePerKw":25,"fundingTxFeeratePerKw":20}""" assert(serialization.write(chcr) === expectedSerializedChcr) system.eventStream.publish(chcr) wsClient.expectMessage(expectedSerializedChcr) From 2155c8d5f46bc326a7516b3e458c4ae126f607c6 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 25 Apr 2022 11:51:10 +0200 Subject: [PATCH 3/7] Make channel reserve an Option in params We use a codec trick to make the channelReserve field in LocalParams and RemoteParams an Option[Satoshi]. --- .../fr/acinq/eclair/channel/ChannelData.scala | 16 +++-- .../channel/fsm/ChannelOpenSingleFunder.scala | 4 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 2 +- .../channel/version0/ChannelCodecs0.scala | 7 +-- .../channel/version1/ChannelCodecs1.scala | 6 +- .../channel/version2/ChannelCodecs2.scala | 6 +- .../channel/version3/ChannelCodecs3.scala | 17 ++++-- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../eclair/channel/CommitmentsSpec.scala | 8 +-- .../internal/channel/ChannelCodecsSpec.scala | 10 ++-- .../channel/version1/ChannelCodecs1Spec.scala | 4 +- .../channel/version3/ChannelCodecs3Spec.scala | 59 +++++++++++++++++-- 12 files changed, 101 insertions(+), 42 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 06fbf39573..e7aa74c316 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec @@ -196,7 +196,7 @@ final case class CMD_GET_CHANNEL_INFO(replyTo: ActorRef)extends HasReplyToComman /** response to [[Command]] requests */ sealed trait CommandResponse[+C <: Command] sealed trait CommandSuccess[+C <: Command] extends CommandResponse[C] -sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: Throwable } +sealed trait CommandFailure[+C <: Command, +T <: Throwable] extends CommandResponse[C] { def t: T } /** generic responses */ final case class RES_SUCCESS[+C <: Command](cmd: C, channelId: ByteVector32) extends CommandSuccess[C] @@ -466,14 +466,16 @@ case class LocalParams(nodeId: PublicKey, fundingKeyPath: DeterministicWallet.KeyPath, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - requestedChannelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, isInitiator: Boolean, defaultFinalScriptPubKey: ByteVector, walletStaticPaymentBasepoint: Option[PublicKey], - initFeatures: Features[InitFeature]) + initFeatures: Features[InitFeature]) { + val requestedChannelReserve: Satoshi = requestedChannelReserve_opt.getOrElse(0 sat) +} /** * @param initFeatures see [[LocalParams.initFeatures]] @@ -481,7 +483,7 @@ case class LocalParams(nodeId: PublicKey, case class RemoteParams(nodeId: PublicKey, dustLimit: Satoshi, maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi - requestedChannelReserve: Satoshi, + requestedChannelReserve_opt: Option[Satoshi], htlcMinimum: MilliSatoshi, toSelfDelay: CltvExpiryDelta, maxAcceptedHtlcs: Int, @@ -491,7 +493,9 @@ case class RemoteParams(nodeId: PublicKey, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, initFeatures: Features[InitFeature], - shutdownScript: Option[ByteVector]) + shutdownScript: Option[ByteVector]) { + val requestedChannelReserve: Satoshi = requestedChannelReserve_opt.getOrElse(0 sat) +} case class ChannelFlags(announceChannel: Boolean) { override def toString: String = s"ChannelFlags(announceChannel=$announceChannel)" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index ce0f4219fa..c1f9f50457 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -106,7 +106,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = open.dustLimitSatoshis, maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, - requestedChannelReserve = open.channelReserveSatoshis, // our peer requires us to always have at least that much satoshis in our balance + requestedChannelReserve_opt = Some(open.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = open.htlcMinimumMsat, toSelfDelay = open.toSelfDelay, maxAcceptedHtlcs = open.maxAcceptedHtlcs, @@ -139,7 +139,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { nodeId = remoteNodeId, dustLimit = accept.dustLimitSatoshis, maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, - requestedChannelReserve = accept.channelReserveSatoshis, // our peer requires us to always have at least that much satoshis in our balance + requestedChannelReserve_opt = Some(accept.channelReserveSatoshis), // our peer requires us to always have at least that much satoshis in our balance htlcMinimum = accept.htlcMinimumMsat, toSelfDelay = accept.toSelfDelay, maxAcceptedHtlcs = accept.maxAcceptedHtlcs, 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 354a7de3d0..e6da8cf17e 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 @@ -506,7 +506,7 @@ object Peer { nodeParams.channelKeyManager.newFundingKeyPath(isInitiator), // we make sure that initiator and non-initiator key paths end differently dustLimit = nodeParams.channelConf.dustLimit, maxHtlcValueInFlightMsat = nodeParams.channelConf.maxHtlcValueInFlightMsat, - requestedChannelReserve = (fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit), // BOLT #2: make sure that our reserve is above our dust limit + requestedChannelReserve_opt = Some((fundingAmount * nodeParams.channelConf.reserveToFundingRatio).max(nodeParams.channelConf.dustLimit)), // BOLT #2: make sure that our reserve is above our dust limit htlcMinimum = nodeParams.channelConf.htlcMinimum, toSelfDelay = nodeParams.channelConf.toRemoteDelay, // we choose their delay maxAcceptedHtlcs = nodeParams.channelConf.maxAcceptedHtlcs, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index ee03273f51..7a329c0082 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version0 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.TimestampSecond +import fr.acinq.eclair.{BlockHeight, TimestampSecond} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +27,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.{channelAnnouncementCodec, channelUpdateCodec, combinedFeaturesCodec} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature, TimestampSecond} import scodec.Codec import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -70,7 +69,7 @@ private[channel] object ChannelCodecs0 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -83,7 +82,7 @@ private[channel] object ChannelCodecs0 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index e1fc5e2518..51f74e78bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version1 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +28,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} @@ -56,7 +56,7 @@ private[channel] object ChannelCodecs1 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -69,7 +69,7 @@ private[channel] object ChannelCodecs1 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 155c648267..b1101ec67f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} +import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -27,7 +28,6 @@ import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0.{HtlcTxAndSi import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{BlockHeight, Features, InitFeature} import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} @@ -56,7 +56,7 @@ private[channel] object ChannelCodecs2 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -69,7 +69,7 @@ private[channel] object ChannelCodecs2 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | optional(provide(true), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 423e172fef..39109e7c3f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxOut} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features, InitFeature} +import fr.acinq.eclair.{BlockHeight, FeatureSupport, Features} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -70,12 +70,17 @@ private[channel] object ChannelCodecs3 { (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention ) + private def channelReserveCodec(channelFeatures: ChannelFeatures): Codec[Option[Satoshi]] = satoshi.xmap( + sats => if (channelFeatures.hasFeature(Features.DualFunding)) None else Some(sats), + sats_opt => if (channelFeatures.hasFeature(Features.DualFunding)) Satoshi(0) else sats_opt.getOrElse(Satoshi(0)) + ) + def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | channelReserveCodec(channelFeatures)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -84,11 +89,11 @@ private[channel] object ChannelCodecs3 { ("walletStaticPaymentBasepoint" | optional(provide(channelFeatures.paysDirectlyToWallet), publicKey)) :: ("features" | combinedFeaturesCodec)).as[LocalParams] - val remoteParamsCodec: Codec[RemoteParams] = ( + def remoteParamsCodec(channelFeatures: ChannelFeatures): Codec[RemoteParams] = ( ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | satoshi) :: + ("channelReserve" | channelReserveCodec(channelFeatures)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -269,7 +274,7 @@ private[channel] object ChannelCodecs3 { ("channelConfig" | channelConfigCodec) :: (("channelFeatures" | channelFeaturesCodec) >>:~ { channelFeatures => ("localParams" | localParamsCodec(channelFeatures)) :: - ("remoteParams" | remoteParamsCodec) :: + ("remoteParams" | remoteParamsCodec(channelFeatures)) :: ("channelFlags" | channelflags) :: ("localCommit" | localCommitCodec) :: ("remoteCommit" | remoteCommitCodec) :: 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 4c99f1b6f7..a916f5225f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -208,7 +208,7 @@ object TestConstants { isInitiator = true, fundingSatoshis ).copy( - requestedChannelReserve = 10_000 sat // Bob will need to keep that much satoshis in his balance + requestedChannelReserve_opt = Some(10_000 sat) // Bob will need to keep that much satoshis in his balance ) } @@ -346,7 +346,7 @@ object TestConstants { isInitiator = false, fundingSatoshis ).copy( - requestedChannelReserve = 20_000 sat // Alice will need to keep that much satoshis in her balance + requestedChannelReserve_opt = Some(20_000 sat) // Alice will need to keep that much satoshis in her balance ) } 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 a845529826..1673db2ebe 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 @@ -480,8 +480,8 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with object CommitmentsSpec { def makeCommitments(toLocal: MilliSatoshi, toRemote: MilliSatoshi, feeRatePerKw: FeeratePerKw = FeeratePerKw(0 sat), dustLimit: Satoshi = 0 sat, isInitiator: 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, isInitiator, 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, None) + val localParams = LocalParams(randomKey().publicKey, DeterministicWallet.KeyPath(Seq(42L)), dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(randomKey().publicKey, dustLimit, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), @@ -503,8 +503,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, isInitiator = 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, None) + val localParams = LocalParams(localNodeId, DeterministicWallet.KeyPath(Seq(42L)), 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, isInitiator = true, ByteVector.empty, None, Features.empty) + val remoteParams = RemoteParams(remoteNodeId, 0 sat, UInt64.MaxValue, None, 1 msat, CltvExpiryDelta(144), 50, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(randomBytes32(), 0, (toLocal + toRemote).truncateToSatoshi, randomKey().publicKey, remoteParams.fundingPubKey) Commitments( channelId = randomBytes32(), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index 6194b4f105..1112ea9903 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -135,11 +135,11 @@ class ChannelCodecsSpec extends AnyFunSuite { // this test makes sure that we actually produce the same objects than previous versions of eclair val refs = Map( hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B134000456E4167E3C0EB8C856C79CA31C97C0AA0000000000000222000000012A05F2000000000000028F5C000000000000000102D0001E000BD48A2402E80B723C42EE3E42938866EC6686ABB7ABF64380000000C501A7F2974C5074E9E10DBB3F0D9B8C40932EC63ABC610FAD7EB6B21C6D081A459B000000000000011E80000001EEFFFE5C00000000000147AE00000000000001F403F000F18146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB20131AD64F76FAF90CD7DE26892F1BDAB82FB9E02EF6538D82FF4204B5348F02AE081A5388E9474769D69C4F60A763AE0CCDB5228A06281DE64408871A927297FDFD8818B6383985ABD4F0AC22E73791CF3A4D63C592FA2648242D34B8334B1539E823381BB1F1404C37D9C2318F5FC6B1BF7ECF5E6835B779E3BE09BADCF6DF1F51DCFBC80000000C0808000000000000EFD80000000007F00000000061A0A4880000001EDE5F3C3801203B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E808000000015FFFFFF800000000011001029DFB814F6502A68D6F83B6049E3D2948A2080084083750626532FDB437169C20023A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A95700AD0100000000008083B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E80800000001961B4C001618F8180000000001100102E648BA30998A28C02C2DFD9DDCD0E0BA064DA199C55186485AFAB296B94E704426FFE00000000000B000A67D9B9FAADB91650E0146B1F742E5C16006708890200239822011026A6925C659D006FEB42D639F1E42DD13224EE49AA34E71B612CF96DB66A8CD4011032C22F653C54CC5E41098227427650644266D80DED45B7387AE0FFC10E529C4680A418228110807CB47D9C1A14CB832FB361C398EA672C9542F34A90BAD4288FA6AC5FC9E9845C01101CF71CAE9252D389135D8C606225DCF1E0333CCDF1FAE84B74FC5D3D440C25F880A3A9108146779F781067ED04B4957E14F1C5623AB653039B2B1D49910240848E4E682DB21081B30694071254D8B3B9537320C014B8CB1052E5514F5EFC19CF2EB806308D5CF1A9573D7C531000000000000000000F3180000000007F00000001EDE5F3C380000000061A0A48D64CA627B243AD5915A2E5D0BAD026762028DDF3304992B83A26D6C11735FC5F01ED56D769BDE7F6A068AF1A4BCFDF950321F3A4744B01B1DDC7498677F112AE1A80000000000000000000000000000000000000658000000000000819800040D37301C10C9419287E9A3B704EB6D7F45CC145DD77DCE8A63B0A47C8AB67467D800901DCE3C8B05A891E56F2BAF1B82405ABD8640B759AEEBD939B976D42C311758F40400000000AFFFFFFC00000000008800814EFDC0A7B2815346B7C1DB024F1E94A451040042041BA83132997EDA1B8B4E10011D48840A33BCFBC0833F6825A4ABF0A78E2B11D5B2981CD958EA4C881204247273416D90840D9834A03892A6C59DCA9B990600A5C65882972A8A7AF7E0CE7975C031846AE78D4AB8002000EC0003FFFFFFFF86801076D98A575A4CDFD0E3F44D1BB3CD3BBAF3BD04C38FED439ED90D88DF932A9296801A80007FFFFFFFF4008136A9D5896669E8724C5120FB6B36C241EF3CEF68AE0316161F04A9EE3EAFF36000FC0003FFFFFFFF86780106E4B5CC4155733A2427082907338051A5DA1E7CA6432840A5528ECAFFA3FB628801B80007FFFFFFFF10020CA4E125E9126107745D4354D4187ABCDE323117857A1DCEB7CCF60B2AAFA80C6003A0000FFFFFFFFE1C0080981575FD981A73A848CC0243CB467BF451F6811DAF4D71CAD8CE8B1E96DB190C01000003FFFFFFFF867400814C747E0FD8290BE8A3B8B3F73015A261479A71780CD3A0A9270234E4B394409C00D80003FFFFFFFF90020E1B9C9B10A97F15F5E1BB27FC8AC670DF8DADEAE4EDFAFB23BDD0AC705FDF51600340000FFFFFFFFF0020AD2581F3494A17B0BE3F63516D53F028A204FD3156D8B21AA4E57A8738D2062080007FFFFFFFF0CE83B9C79160B5123CADE575E370480B57B0C816EB35DD7B27372EDA858622EB1E0B8C1E00000B8000FA46CC2C7E9AB4A37C64216CD65C944E6D73998419D1A1AD2827AB6BC85B32280230764E374064EC82A3751E789607E23BEAE93FB0EDDD5E7FA803767079662E80EAEF384E2AFCB68049D9DC246119E77BD2ED4112330760CAB6CD3671CFCE006C584B9C95E0B554261E00154D40806EA694F44751B328A9291BAD124EFD5664280936EC92D27B242737E7E3E83B4704BA367B7DA5108F2F6EDFB1C38EE721A369E77EED71B12090BAEAAAC322C1457E31AB0C4DE5D9351943F10FD747742616A1AABD09F680B37D4105A8872695EE9B97FAB8985FAA9D747D45046229BF265CEEB300A40FE23040C5F335E0515496C58EE47418B72331FCC6F47A31A9B33B8E000008692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002069FCA5D3141D3A78436ECFC366E31024CBB18EAF1843EB5FADAC871B42069166C0726710955E3AD621072FCBDFCB90D79E5B1951A5EE01DB533B72429F84E2562680519DE7DE0419FB412D255F853C71588EAD94C0E6CAC7526440902123939A0B6C806CC1A501C495362CEE54DCC830052E32C414B95453D7BF0673CBAE018C23573C69C694A8F88483050257A7366B838489731E5776B6FA0F02573401176D3E7FAEEF11E95A671420586631255F51A0EC2CF4D4D9F69D587712070FE1FB9316B71868692FFAFF04D2AE211E9461FB39D875D74F32E4109D21D5A03D46612000000002E307800002E0002BA11BBBA0202012000000000000007D0000007D0000000C800000007CFFFF83000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"07738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63c","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[1457788542,1007597768,1455922339,479707306]},"dustLimit":546,"maxHtlcValueInFlightMsat":5000000000,"requestedChannelReserve_opt":167772,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":false,"defaultFinalScriptPubKey":"a9144805d016e47885dc7c852710cdd8cd0d576f57ec87","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","dustLimit":573,"maxHtlcValueInFlightMsat":16609443000,"requestedChannelReserve_opt":167772,"htlcMinimum":1000,"toSelfDelay":2016,"maxAcceptedHtlcs":483,"fundingPubKey":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","revocationBasepoint":"02635ac9eedf5f219afbc4d125e37b5705f73c05deca71b05fe84096a691e055c1","paymentBasepoint":"034a711d28e8ed3ad389ec14ec75c199b6a45140c503bcc88110e3524e52ffbfb1","delayedPaymentBasepoint":"0316c70730b57a9e15845ce6f239e749ac78b25f44c90485a697066962a73d0467","htlcBasepoint":"03763e280986fb384631ebf8d637efd9ebcd06b6ef3c77c1375b9edbe3ea3b9f79","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":7675,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":204739729,"toRemote":16572475271},"commitTxAndRemoteSig":{"commitTx":{"txid":"e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4","tx":"020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620"},"remoteSig":"4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":7779,"spec":{"htlcs":[],"commitTxFeerate":254,"toLocal":16572475271,"toRemote":204739729},"txid":"ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be","remotePerCommitmentPoint":"03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":203,"remoteNextHtlcId":4147,"originChannels":{},"remoteNextCommitInfo":"034dcc0704325064a1fa68edc13adb5fd173051775df73a298ec291f22ad9d19f6","commitInput":{"outPoint":"3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1","amountSatoshis":16777215},"remotePerCommitmentSecrets":null},"shortChannelId":"1513532x23x1","buried":true,"channelAnnouncement":{"nodeSignature1":"d2366163f4d5a51be3210b66b2e4a2736b9ccc20ce8d0d69413d5b5e42d991401183b271ba032764151ba8f3c4b03f11df5749fd876eeaf3fd401bb383cb3174","nodeSignature2":"075779c27157e5b4024ecee12308cf3bde976a0891983b0655b669b38e7e700362c25ce4af05aaa130f000aa6a04037534a7a23a8d99454948dd689277eab321","bitcoinSignature1":"4049b7649693d92139bf3f1f41da3825d1b3dbed2884797b76fd8e1c77390d1b4f3bf76b8d890485d7555619160a2bf18d58626f2ec9a8ca1f887eba3ba130b5","bitcoinSignature2":"0d55e84fb4059bea082d443934af74dcbfd5c4c2fd54eba3ea2823114df932e7759805207f1182062f99af028aa4b62c7723a0c5b9198fe637a3d18d4d99dc70","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","nodeId1":"034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"028cef3ef020cfda09692afc29e38ac4756ca60736563a93220481091c9cd05b64","bitcoinKey2":"03660d280e24a9b16772a6e6418029719620a5caa29ebdf8339e5d700c611ab9e3","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"4e34a547c424182812bd39b35c1c244b98f2bbb5b7d07812b9a008bb69f3fd77788f4ad338a102c331892afa8d076167a6a6cfb4eac3b890387f0fdc98b5b8c3","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1513532x23x1","timestamp":{"iso":"2019-06-18T12:49:33Z","unix":1560862173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":16777215000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"commitTxFeerate":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":{"iso":"2019-06-24T09:39:33Z","unix":1561369173},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isInitiator":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"option_support_large_channel":"optional","gossip_queries_ex":"optional","option_data_loss_protect":"optional","var_onion_optin":"mandatory","option_static_remotekey":"optional","payment_secret":"optional","option_shutdown_anysegwit":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"requestedChannelReserve_opt":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"option_upfront_shutdown_script":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","var_onion_optin":"optional","option_static_remotekey":"mandatory","option_support_large_channel":"optional","option_anchors_zero_fee_htlc_tx":"optional","basic_mpp":"optional","gossip_queries":"optional"},"unknown":[31]}},"channelFlags":{"announceChannel":true},"localCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"commitTxFeerate":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","tlvStream":{"records":[],"unknown":[]}},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":{"iso":"2021-07-08T12:09:56Z","unix":1625746196},"channelFlags":{"isEnabled":true,"isNode1":false},"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"tlvStream":{"records":[],"unknown":[]}}}""" ) refs.foreach { case (oldbin, refjson) => @@ -256,7 +256,7 @@ object ChannelCodecsSpec { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(546), maxHtlcValueInFlightMsat = UInt64(50000000), - requestedChannelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 10000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, @@ -269,7 +269,7 @@ object ChannelCodecsSpec { nodeId = randomKey().publicKey, dustLimit = 546 sat, maxHtlcValueInFlightMsat = UInt64(5000000), - requestedChannelReserve = 10000 sat, + requestedChannelReserve_opt = Some(10000 sat), htlcMinimum = 5000 msat, toSelfDelay = CltvExpiryDelta(144), maxAcceptedHtlcs = 50, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala index 320129a898..6add1bdf43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1Spec.scala @@ -63,7 +63,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { fundingKeyPath = DeterministicWallet.KeyPath(Seq(42L)), dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - requestedChannelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), @@ -83,7 +83,7 @@ class ChannelCodecs1Spec extends AnyFunSuite { nodeId = randomKey().publicKey, dustLimit = Satoshi(Random.nextInt(Int.MaxValue)), maxHtlcValueInFlightMsat = UInt64(Random.nextInt(Int.MaxValue)), - requestedChannelReserve = Satoshi(Random.nextInt(Int.MaxValue)), + requestedChannelReserve_opt = Some(Satoshi(Random.nextInt(Int.MaxValue))), htlcMinimum = MilliSatoshi(Random.nextInt(Int.MaxValue)), toSelfDelay = CltvExpiryDelta(Random.nextInt(Short.MaxValue)), maxAcceptedHtlcs = Random.nextInt(Short.MaxValue), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 36ab4756aa..4f2f3a634d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -15,7 +15,7 @@ */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, Satoshi} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.channel._ @@ -27,6 +27,8 @@ import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, UI import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} +import scala.util.Random + class ChannelCodecs3Spec extends AnyFunSuite { test("basic serialization test (NORMAL)") { @@ -77,11 +79,12 @@ class ChannelCodecs3Spec extends AnyFunSuite { } test("encode/decode optional shutdown script") { + val codec = remoteParamsCodec(ChannelFeatures()) val remoteParams = RemoteParams( randomKey().publicKey, Satoshi(600), UInt64(123456L), - Satoshi(300), + Some(Satoshi(300)), MilliSatoshi(1000), CltvExpiryDelta(42), 42, @@ -92,9 +95,9 @@ class ChannelCodecs3Spec extends AnyFunSuite { randomKey().publicKey, Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), None) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams).require).require === remoteParams) + assert(codec.decodeValue(codec.encode(remoteParams).require).require === remoteParams) val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) - assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams1).require).require === remoteParams1) + assert(codec.decodeValue(codec.encode(remoteParams1).require).require === remoteParams1) val dataWithoutRemoteShutdownScript = normal.copy(commitments = normal.commitments.copy(remoteParams = remoteParams)) assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithoutRemoteShutdownScript).require).require.value === dataWithoutRemoteShutdownScript) @@ -103,6 +106,54 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(DATA_NORMAL_Codec.decode(DATA_NORMAL_Codec.encode(dataWithRemoteShutdownScript).require).require.value === dataWithRemoteShutdownScript) } + test("encode/decode optional channel reserve") { + val localParams = LocalParams( + randomKey().publicKey, + DeterministicWallet.KeyPath(Seq(42L)), + Satoshi(660), + UInt64(500000), + Some(Satoshi(15000)), + MilliSatoshi(1000), + CltvExpiryDelta(36), + 50, + Random.nextBoolean(), + hex"deadbeef", + None, + Features().initFeatures()) + val remoteParams = RemoteParams( + randomKey().publicKey, + Satoshi(500), + UInt64(100000), + Some(Satoshi(30000)), + MilliSatoshi(1500), + CltvExpiryDelta(144), + 10, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + Features(), + None) + + { + val localCodec = localParamsCodec(ChannelFeatures()) + val remoteCodec = remoteParamsCodec(ChannelFeatures()) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams === localParams) + assert(decodedRemoteParams === remoteParams) + } + { + val localCodec = localParamsCodec(ChannelFeatures(Features.DualFunding)) + val remoteCodec = remoteParamsCodec(ChannelFeatures(Features.DualFunding)) + val decodedLocalParams = localCodec.decode(localCodec.encode(localParams).require).require.value + val decodedRemoteParams = remoteCodec.decode(remoteCodec.encode(remoteParams).require).require.value + assert(decodedLocalParams === localParams.copy(requestedChannelReserve_opt = None)) + assert(decodedRemoteParams === remoteParams.copy(requestedChannelReserve_opt = None)) + } + } + test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" val decoded1 = channelDataCodec.decode(oldBin.bits).require.value From b8addaabdf7911c6b15b7192e3496cd5c950c76c Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 25 Apr 2022 13:52:28 +0200 Subject: [PATCH 4/7] fixup! Make channel reserve an Option in params --- .../wire/internal/channel/version0/ChannelCodecs0.scala | 4 ++-- .../wire/internal/channel/version1/ChannelCodecs1.scala | 4 ++-- .../wire/internal/channel/version2/ChannelCodecs2.scala | 4 ++-- .../wire/internal/channel/version3/ChannelCodecs3.scala | 9 ++------- .../fr/acinq/eclair/payment/PaymentPacketSpec.scala | 4 ++-- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 7a329c0082..093080b898 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -69,7 +69,7 @@ private[channel] object ChannelCodecs0 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -82,7 +82,7 @@ private[channel] object ChannelCodecs0 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 51f74e78bd..934b9e9290 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -56,7 +56,7 @@ private[channel] object ChannelCodecs1 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -69,7 +69,7 @@ private[channel] object ChannelCodecs1 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index b1101ec67f..6df6317a8b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -56,7 +56,7 @@ private[channel] object ChannelCodecs2 { ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -69,7 +69,7 @@ private[channel] object ChannelCodecs2 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | optional(provide(true), satoshi)) :: + ("channelReserve" | conditional(included = true, satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 39109e7c3f..2f18d60117 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -70,17 +70,12 @@ private[channel] object ChannelCodecs3 { (cf: ChannelFeatures) => Features(cf.features.map(f => f -> FeatureSupport.Mandatory).toMap).toByteVector // we encode features as mandatory, by convention ) - private def channelReserveCodec(channelFeatures: ChannelFeatures): Codec[Option[Satoshi]] = satoshi.xmap( - sats => if (channelFeatures.hasFeature(Features.DualFunding)) None else Some(sats), - sats_opt => if (channelFeatures.hasFeature(Features.DualFunding)) Satoshi(0) else sats_opt.getOrElse(Satoshi(0)) - ) - def localParamsCodec(channelFeatures: ChannelFeatures): Codec[LocalParams] = ( ("nodeId" | publicKey) :: ("channelPath" | keyPathCodec) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | channelReserveCodec(channelFeatures)) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: @@ -93,7 +88,7 @@ private[channel] object ChannelCodecs3 { ("nodeId" | publicKey) :: ("dustLimit" | satoshi) :: ("maxHtlcValueInFlightMsat" | uint64) :: - ("channelReserve" | channelReserveCodec(channelFeatures)) :: + ("channelReserve" | conditional(!channelFeatures.hasFeature(Features.DualFunding), satoshi)) :: ("htlcMinimum" | millisatoshi) :: ("toSelfDelay" | cltvExpiryDelta) :: ("maxAcceptedHtlcs" | uint16) :: 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 41cf058551..10d6fab65f 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 @@ -368,8 +368,8 @@ object PaymentPacketSpec { } def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { - val params = LocalParams(null, null, null, null, null, null, null, 0, isInitiator = true, null, None, null) - val remoteParams = RemoteParams(randomKey().publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) + val params = LocalParams(null, null, null, null, None, null, null, 0, isInitiator = true, null, None, null) + val remoteParams = RemoteParams(randomKey().publicKey, null, null, None, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null, None) val commitInput = InputInfo(OutPoint(randomBytes32(), 1), TxOut(testCapacity, Nil), Nil) val channelFlags = ChannelFlags.Private new Commitments(channelId, ChannelConfig.standard, ChannelFeatures(), params, remoteParams, channelFlags, null, null, null, null, 0, 0, Map.empty, null, commitInput, null) { From a15ad026748c80f353e2186e461b3df893938594 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 26 Apr 2022 14:09:00 +0200 Subject: [PATCH 5/7] Update dual funding codecs * Use u16 instead of varint for lengths * Revert channel flags extension * Remove support for non-native segwit inputs --- .../eclair/wire/protocol/CommonCodecs.scala | 8 ---- .../protocol/LightningMessageCodecs.scala | 21 +++------ .../wire/protocol/LightningMessageTypes.scala | 2 - .../protocol/LightningMessageCodecsSpec.scala | 45 +++++++------------ 4 files changed, 22 insertions(+), 54 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 7e37e80478..78ad89f00a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -100,9 +100,6 @@ object CommonCodecs { // It is useful in combination with variableSizeBytesLong to encode/decode TLV lengths because those will always be < 2^63. val varintoverflow: Codec[Long] = varint.narrow(l => if (l <= UInt64(Long.MaxValue)) Attempt.successful(l.toBigInt.toLong) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) - // This codec can be safely used for values < 2^32 and will fail otherwise. - val smallvarint: Codec[Int] = varint.narrow(l => if (l <= UInt64(Int.MaxValue)) Attempt.successful(l.toBigInt.toInt) else Attempt.failure(Err(s"overflow for value $l")), l => UInt64(l)) - val bytes32: Codec[ByteVector32] = limitedSizeBytes(32, bytesStrict(32).xmap(d => ByteVector32(d), d => d.bytes)) val bytes64: Codec[ByteVector64] = limitedSizeBytes(64, bytesStrict(64).xmap(d => ByteVector64(d), d => d.bytes)) @@ -115,11 +112,6 @@ object CommonCodecs { val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] - val extendedChannelFlags: Codec[ChannelFlags] = variableSizeBytesLong(varintoverflow, bytes).xmap( - bin => ChannelFlags(bin.lastOption.exists(_ % 2 == 1)), - flags => if (flags.announceChannel) ByteVector(1) else ByteVector(0) - ) - val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) val ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => Attempt.fromTry(Try(Inet6Address.getByAddress(null, b.toArray, null))), a => Attempt.fromTry(Try(ByteVector(a.getAddress)))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 620c91c3ab..770b5c0839 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -115,7 +115,7 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | extendedChannelFlags) :: + ("channelFlags" | channelflags) :: ("tlvStream" | OpenDualFundedChannelTlv.openTlvCodec)).as[OpenDualFundedChannel] val acceptChannelCodec: Codec[AcceptChannel] = ( @@ -150,7 +150,6 @@ object LightningMessageCodecs { ("delayedPaymentBasepoint" | publicKey) :: ("htlcBasepoint" | publicKey) :: ("firstPerCommitmentPoint" | publicKey) :: - ("channelFlags" | extendedChannelFlags) :: ("tlvStream" | AcceptDualFundedChannelTlv.acceptTlvCodec)).as[AcceptDualFundedChannel] val fundingCreatedCodec: Codec[FundingCreated] = ( @@ -170,25 +169,19 @@ object LightningMessageCodecs { ("nextPerCommitmentPoint" | publicKey) :: ("tlvStream" | FundingLockedTlv.fundingLockedTlvCodec)).as[FundingLocked] - private val scriptSigOptCodec: Codec[Option[ByteVector]] = lengthDelimited(bytes).xmap[Option[ByteVector]]( - b => if (b.isEmpty) None else Some(b), - b => b.getOrElse(ByteVector.empty) - ) - val txAddInputCodec: Codec[TxAddInput] = ( ("channelId" | bytes32) :: ("serialId" | uint64) :: - ("previousTx" | lengthDelimited(txCodec)) :: + ("previousTx" | variableSizeBytes(uint16, txCodec)) :: ("previousTxOutput" | uint32) :: ("sequence" | uint32) :: - ("scriptSig" | scriptSigOptCodec) :: ("tlvStream" | TxAddInputTlv.txAddInputTlvCodec)).as[TxAddInput] val txAddOutputCodec: Codec[TxAddOutput] = ( ("channelId" | bytes32) :: ("serialId" | uint64) :: ("amount" | satoshi) :: - ("scriptPubKey" | lengthDelimited(bytes)) :: + ("scriptPubKey" | variableSizeBytes(uint16, bytes)) :: ("tlvStream" | TxAddOutputTlv.txAddOutputTlvCodec)).as[TxAddOutput] val txRemoveInputCodec: Codec[TxRemoveInput] = ( @@ -205,9 +198,9 @@ object LightningMessageCodecs { ("channelId" | bytes32) :: ("tlvStream" | TxCompleteTlv.txCompleteTlvCodec)).as[TxComplete] - private val witnessElementCodec: Codec[ByteVector] = lengthDelimited(bytes) - private val witnessStackCodec: Codec[ScriptWitness] = listOfN(smallvarint, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) - private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(smallvarint, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) + private val witnessElementCodec: Codec[ByteVector] = variableSizeBytes(uint16, bytes) + private val witnessStackCodec: Codec[ScriptWitness] = listOfN(uint16, witnessElementCodec).xmap(s => ScriptWitness(s.toSeq), w => w.stack.toList) + private val witnessesCodec: Codec[Seq[ScriptWitness]] = listOfN(uint16, witnessStackCodec).xmap(l => l.toSeq, l => l.toList) val txSignaturesCodec: Codec[TxSignatures] = ( ("channelId" | bytes32) :: @@ -227,7 +220,7 @@ object LightningMessageCodecs { val txAbortCodec: Codec[TxAbort] = ( ("channelId" | bytes32) :: - ("data" | lengthDelimited(bytes)) :: + ("data" | variableSizeBytes(uint16, bytes)) :: ("tlvStream" | TxAbortTlv.txAbortTlvCodec)).as[TxAbort] val shutdownCodec: Codec[Shutdown] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 6510657c48..ac7683140a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -85,7 +85,6 @@ case class TxAddInput(channelId: ByteVector32, previousTx: Transaction, previousTxOutput: Long, sequence: Long, - scriptSig_opt: Option[ByteVector], tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId case class TxAddOutput(channelId: ByteVector32, @@ -210,7 +209,6 @@ case class AcceptDualFundedChannel(temporaryChannelId: ByteVector32, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, - channelFlags: ChannelFlags, tlvStream: TlvStream[AcceptDualFundedChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScriptTlv].map(_.script) val channelType_opt: Option[ChannelType] = tlvStream.get[ChannelTlv.ChannelTypeTlv].map(_.channelType) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index 317f744309..76b2f9cf4b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -166,23 +166,23 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val txBin2 = hex"0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000" val tx2 = Transaction.read(txBin2.toArray) val testCases = Seq( - TxAddInput(channelId1, UInt64(561), tx1, 1, 5, None) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005 00", - TxAddInput(channelId2, UInt64(0), tx2, 2, 0, None) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 fd0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000 00", - TxAddInput(channelId1, UInt64(561), tx1, 0, 0, Some(hex"00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000 15 00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472", - TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 16 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", + TxAddInput(channelId1, UInt64(561), tx1, 1, 5) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000001 00000005", + TxAddInput(channelId2, UInt64(0), tx2, 2, 0) -> hex"0042 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000000 0100 0200000000010142180a8812fc79a3da7fb2471eff3e22d7faee990604c2ba7f2fc8dfb15b550a0200000000feffffff030f241800000000001976a9146774040642a78ca3b8b395e70f8391b21ec026fc88ac4a155801000000001600148d2e0b57adcb8869e603fd35b5179caf053361253b1d010000000000160014e032f4f4b9f8611df0d30a20648c190c263bbc33024730440220506005aa347f5b698542cafcb4f1a10250aeb52a609d6fd67ef68f9c1a5d954302206b9bb844343f4012bccd9d08a0f5430afb9549555a3252e499be7df97aae477a012103976d6b3eea3de4b056cd88cdfd50a22daf121e0fb5c6e45ba0f40e1effbd275a00000000 00000002 00000000", + TxAddInput(channelId1, UInt64(561), tx1, 0, 0) -> hex"0042 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000231 00f7 020000000001014ade359c5deb7c1cde2e94f401854658f97d7fa31c17ce9a831db253120a0a410100000017160014eb9a5bd79194a23d19d6ec473c768fb74f9ed32cffffffff021ca408000000000017a914946118f24bb7b37d5e9e39579e4a411e70f5b6a08763e703000000000017a9143638b2602d11f934c04abc6adb1494f69d1f14af8702473044022059ddd943b399211e4266a349f26b3289979e29f9b067792c6cfa8cc5ae25f44602204d627a5a5b603d0562e7969011fb3d64908af90a3ec7c876eaa9baf61e1958af012102f5188df1da92ed818581c29778047800ed6635788aa09d9469f7d17628f7323300000000 00000000 00000000", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472") -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472", + TxAddOutput(channelId1, UInt64(1105), 2047 sat, hex"00149357014afd0ccd265658c9ae81efa995e771f472", TlvStream(Nil, Seq(GenericTlv(UInt64(301), hex"2a")))) -> hex"0043 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000451 00000000000007ff 0016 00149357014afd0ccd265658c9ae81efa995e771f472 fd012d012a", TxRemoveInput(channelId2, UInt64(561)) -> hex"0044 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0000000000000231", TxRemoveOutput(channelId1, UInt64(1)) -> hex"0045 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000000000000001", TxComplete(channelId1) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", TxComplete(channelId1, TlvStream(Nil, Seq(GenericTlv(UInt64(231), hex"deadbeef"), GenericTlv(UInt64(507), hex"")))) -> hex"0046 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e704deadbeef fd01fb00", - TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 02 0202dead02beef 04 000401010101000102", - TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 00", + TxSignatures(channelId1, tx2.txid, Seq(ScriptWitness(Seq(hex"dead", hex"beef")), ScriptWitness(Seq(hex"", hex"01010101", hex"", hex"02")))) -> hex"0047 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f169ed4bcb4ca97646845ec063d4deddcbe704f77f1b2c205929195f84a87afc 0002 00020002dead0002beef 0004 00000004010101010000000102", + TxSignatures(channelId2, tx1.txid, Nil) -> hex"0047 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 06f125a8ef64eb5a25826190dc28f15b85dc1adcfc7a178eef393ea325c02e1f 0000", TxInitRbf(channelId1, 8388607, FeeratePerKw(4000 sat)) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 007fffff 00000fa0", TxInitRbf(channelId1, 0, FeeratePerKw(4000 sat), TlvStream(SharedOutputContributionTlv(5000 sat))) -> hex"0048 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00000000 00000fa0 00021388", TxAckRbf(channelId2) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", TxAckRbf(channelId2, TlvStream(SharedOutputContributionTlv(450000 sat))) -> hex"0049 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 000306ddd0", - TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 00", - TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0e 696e7465726e616c206572726f72", + TxAbort(channelId1, hex"") -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 0000", + TxAbort(channelId1, ByteVector.view("internal error".getBytes(Charsets.US_ASCII))) -> hex"004a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 000e 696e7465726e616c206572726f72", ) testCases.foreach { case (message, bin) => val decoded = lightningMessageCodec.decode(bin.bits).require @@ -252,10 +252,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { test("encode/decode open_channel (dual funding)") { val defaultOpen = OpenDualFundedChannel(ByteVector32.Zeroes, ByteVector32.One, FeeratePerKw(5000 sat), FeeratePerKw(4000 sat), 250_000 sat, 500 sat, UInt64(50_000), 15 msat, CltvExpiryDelta(144), 483, 650_000, publicKey(1), publicKey(2), publicKey(3), publicKey(4), publicKey(5), publicKey(6), ChannelFlags(true)) - val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val defaultEncoded = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 01" val testCases = Seq( defaultOpen -> defaultEncoded, - defaultOpen.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"0103401000"), + defaultOpen.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"0103401000"), defaultOpen.copy(tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"), ChannelTypeTlv(ChannelTypes.AnchorOutputsZeroFeeHtlcTx))) -> (defaultEncoded ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283 0103401000"), ) testCases.foreach { case (open, bin) => @@ -271,8 +271,8 @@ class LightningMessageCodecsSpec extends AnyFunSuite { val defaultEncodedWithoutFlags = hex"0040 0000000000000000000000000000000000000000000000000000000000000000 0100000000000000000000000000000000000000000000000000000000000000 00001388 00000fa0 000000000003d090 00000000000001f4 000000000000c350 000000000000000f 0090 01e3 0009eb10 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" val testCases = Seq( defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), + defaultEncodedWithoutFlags ++ hex"a2" -> ChannelFlags(false), + defaultEncodedWithoutFlags ++ hex"ff" -> ChannelFlags(true), ) testCases.foreach { case (bin, flags) => val decoded = lightningMessageCodec.decode(bin.bits).require.value @@ -307,11 +307,10 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } test("encode/decode accept_channel (dual funding)") { - val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) - val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a 0101" + val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6)) + val defaultEncoded = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" val testCases = Seq( defaultAccept -> defaultEncoded, - defaultAccept.copy(channelFlags = ChannelFlags(false), tlvStream = TlvStream(UpfrontShutdownScriptTlv(hex"00143adb2d0445c4d491cc7568b10323bd6615a91283"))) -> (defaultEncoded.dropRight(2) ++ hex"0100" ++ hex"001600143adb2d0445c4d491cc7568b10323bd6615a91283"), defaultAccept.copy(tlvStream = TlvStream(ChannelTypeTlv(ChannelTypes.StaticRemoteKey))) -> (defaultEncoded ++ hex"01021000"), ) testCases.foreach { case (accept, bin) => @@ -322,20 +321,6 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } - test("decode accept_channel with unknown channel flags (dual funding)") { - val defaultAccept = AcceptDualFundedChannel(ByteVector32.One, 50_000 sat, 473 sat, UInt64(100_000_000), 1 msat, 6, CltvExpiryDelta(144), 50, publicKey(1), point(2), point(3), point(4), point(5), point(6), ChannelFlags(true)) - val defaultEncodedWithoutFlags = hex"0041 0100000000000000000000000000000000000000000000000000000000000000 000000000000c350 00000000000001d9 0000000005f5e100 0000000000000001 00000006 0090 0032 031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f 024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d0766 02531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe337 03462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b 0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7 03f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a" - val testCases = Seq( - defaultEncodedWithoutFlags ++ hex"00" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"01a2" -> ChannelFlags(false), - defaultEncodedWithoutFlags ++ hex"037a2a1f" -> ChannelFlags(true), - ) - testCases.foreach { case (bin, flags) => - val decoded = lightningMessageCodec.decode(bin.bits).require.value - assert(decoded === defaultAccept.copy(channelFlags = flags)) - } - } - test("encode/decode closing_signed") { val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") val testCases = Seq( From 902bc7d8274c3c26ad4a17cee703d5994b9f0623 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 27 Apr 2022 15:39:10 +0200 Subject: [PATCH 6/7] Implement first steps of the dual funding flow We implement the first step of the dual funding protocol: exchanging `open_channel2` and `accept_channel2`. We currently stop after exchanging these two messages. Future commits will add the interactive-tx protocol used to build the funding transaction. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 8 +- .../fr/acinq/eclair/channel/ChannelData.scala | 62 ++++-- .../eclair/channel/ChannelFeatures.scala | 2 +- .../fr/acinq/eclair/channel/Helpers.scala | 91 ++++++-- .../fr/acinq/eclair/channel/fsm/Channel.scala | 109 ++++++---- .../channel/fsm/ChannelOpenDualFunded.scala | 200 ++++++++++++++++++ .../channel/fsm/ChannelOpenSingleFunder.scala | 8 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 132 +++++++----- .../fr/acinq/eclair/EclairImplSpec.scala | 4 +- .../scala/fr/acinq/eclair/TestConstants.scala | 3 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 5 +- .../ChannelStateTestsHelperMethods.scala | 19 +- .../a/WaitForAcceptChannelStateSpec.scala | 8 +- ...tForAcceptDualFundedChannelStateSpec.scala | 164 ++++++++++++++ .../a/WaitForOpenChannelStateSpec.scala | 4 +- ...aitForOpenDualFundedChannelStateSpec.scala | 166 +++++++++++++++ .../b/WaitForFundingCreatedStateSpec.scala | 4 +- .../b/WaitForFundingInternalStateSpec.scala | 4 +- .../b/WaitForFundingSignedStateSpec.scala | 4 +- .../c/WaitForFundingConfirmedStateSpec.scala | 6 +- .../c/WaitForFundingLockedStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 4 +- .../eclair/integration/IntegrationSpec.scala | 11 +- .../interop/rustytests/RustyTestsSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 111 +++++++--- 25 files changed, 934 insertions(+), 203 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala 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 0d6be5469e..6ae9b27eeb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -184,11 +184,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) (appKit.switchboard ? Peer.OpenChannel( remoteNodeId = nodeId, - fundingSatoshis = fundingAmount, - pushMsat = pushAmount_opt.getOrElse(0 msat), + fundingAmount = fundingAmount, channelType_opt = channelType_opt, - fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), + pushAmount_opt = pushAmount_opt, + fundingTxFeerate_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index e7aa74c316..78b68e0f7d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -47,6 +47,7 @@ import java.util.UUID */ sealed trait ChannelState case object WAIT_FOR_INIT_INTERNAL extends ChannelState +// Single-funder channel opening: case object WAIT_FOR_OPEN_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState @@ -54,6 +55,11 @@ case object WAIT_FOR_FUNDING_CREATED extends ChannelState case object WAIT_FOR_FUNDING_SIGNED extends ChannelState case object WAIT_FOR_FUNDING_CONFIRMED extends ChannelState case object WAIT_FOR_FUNDING_LOCKED extends ChannelState +// Dual-funded channel opening: +case object WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_DUAL_FUNDING_INTERNAL extends ChannelState +// Channel opened: case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState case object NEGOTIATING extends ChannelState @@ -75,23 +81,27 @@ case object ERR_INFORMATION_LEAK extends ChannelState 8888888888 Y8P 8888888888 888 Y888 888 "Y8888P" */ -case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, - fundingAmount: Satoshi, - pushAmount: MilliSatoshi, - commitTxFeerate: FeeratePerKw, - fundingTxFeerate: FeeratePerKw, - localParams: LocalParams, - remote: ActorRef, - remoteInit: Init, - channelFlags: ChannelFlags, - channelConfig: ChannelConfig, - channelType: SupportedChannelType) -case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, - localParams: LocalParams, - remote: ActorRef, - remoteInit: Init, - channelConfig: ChannelConfig, - channelType: SupportedChannelType) +case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, + fundingAmount: Satoshi, + dualFunded: Boolean, + commitTxFeerate: FeeratePerKw, + fundingTxFeerate: FeeratePerKw, + pushAmount_opt: Option[MilliSatoshi], + localParams: LocalParams, + remote: ActorRef, + remoteInit: Init, + channelFlags: ChannelFlags, + channelConfig: ChannelConfig, + channelType: SupportedChannelType) +case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32, + fundingContribution_opt: Option[Satoshi], + dualFunded: Boolean, + localParams: LocalParams, + remote: ActorRef, + remoteInit: Init, + channelConfig: ChannelConfig, + channelType: SupportedChannelType) + case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close case object INPUT_DISCONNECTED case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init) @@ -376,10 +386,10 @@ sealed trait PersistentChannelData extends ChannelData { def commitments: Commitments } -final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_FUNDEE) extends TransientChannelData { +final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_FUNDER, lastSent: OpenChannel) extends TransientChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32, @@ -425,6 +435,18 @@ final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends PersistentChannelData final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends PersistentChannelData + +final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { + val channelId: ByteVector32 = init.temporaryChannelId +} +final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenDualFundedChannel) extends TransientChannelData { + val channelId: ByteVector32 = lastSent.temporaryChannelId +} +final case class DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId: ByteVector32, + localParams: LocalParams, + remoteParams: RemoteParams, + channelFeatures: ChannelFeatures) extends TransientChannelData + final case class DATA_NORMAL(commitments: Commitments, shortChannelId: ShortChannelId, buried: Boolean, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index 9881aa5869..7a9b9ac0a8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -59,7 +59,7 @@ object ChannelFeatures { def apply(channelType: ChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): ChannelFeatures = { // NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation, // such as option_dataloss_protect or option_shutdown_anysegwit. - val availableFeatures = Seq(Features.Wumbo, Features.UpfrontShutdownScript).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) + val availableFeatures = Seq(Features.Wumbo, Features.UpfrontShutdownScript, Features.DualFunding).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) val allFeatures = channelType.features.toSeq ++ availableFeatures ChannelFeatures(allFeatures: _*) } 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 4f884b0317..0193065ccd 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 @@ -98,9 +98,7 @@ object Helpers { } } - /** - * Called by the fundee - */ + /** Called by the fundee of a singler-funded channel. */ def validateParamsFundee(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], open: OpenChannel, remoteNodeId: PublicKey, remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. @@ -153,22 +151,60 @@ object Helpers { extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } - /** - * Called by the funder - */ - def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { - accept.channelType_opt match { - case Some(theirChannelType) if accept.channelType_opt != open.channelType_opt => + /** Called by the non-initiator of a dual-funded channel. */ + def validateParamsNonInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, open: OpenDualFundedChannel, remoteNodeId: PublicKey, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: + // MUST reject the channel. + if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) + + if (open.fundingAmount < nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel) || open.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), nodeParams.channelConf.maxFundingSatoshis)) + + // BOLT #2: Channel funding limits + if (open.fundingAmount >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Channel.MAX_FUNDING)) + + // BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large. + if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) + + // BOLT #2: The receiving node MUST fail the channel if: max_accepted_htlcs is greater than 483. + if (open.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(open.temporaryChannelId, open.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) + + // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing. + if (isFeeTooSmall(open.commitmentFeerate)) return Left(FeerateTooSmall(open.temporaryChannelId, open.commitmentFeerate)) + + if (open.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(open.temporaryChannelId, open.dustLimit, Channel.MIN_DUST_LIMIT)) + if (open.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, open.dustLimit, nodeParams.channelConf.maxRemoteDustLimit)) + + // BOLT #2: The receiving node MUST fail the channel if: it considers feerate_per_kw too small for timely processing or unreasonably large. + val localFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, open.fundingAmount, None) + if (nodeParams.onChainFeeConf.feerateToleranceFor(remoteNodeId).isFeeDiffTooHigh(channelType, localFeeratePerKw, open.commitmentFeerate)) return Left(FeerateTooDifferent(open.temporaryChannelId, localFeeratePerKw, open.commitmentFeerate)) + + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + extractShutdownScript(open.temporaryChannelId, localFeatures, remoteFeatures, open.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) + } + + private def validateChannelType(channelId: ByteVector32, channelType: SupportedChannelType, openChannelType_opt: Option[ChannelType], acceptChannelType_opt: Option[ChannelType], localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature]): Option[ChannelException] = { + acceptChannelType_opt match { + case Some(theirChannelType) if acceptChannelType_opt != openChannelType_opt => // if channel_type is set, and channel_type was set in open_channel, and they are not equal types: MUST reject the channel. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, theirChannelType)) + Some(InvalidChannelType(channelId, channelType, theirChannelType)) case None if Features.canUseFeature(localFeatures, remoteFeatures, Features.ChannelType) => // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - return Left(MissingChannelType(open.temporaryChannelId)) + Some(MissingChannelType(channelId)) case None if channelType != ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures) => // If we have overridden the default channel type, but they didn't support explicit channel type negotiation, // we need to abort because they expect a different channel type than what we offered. - return Left(InvalidChannelType(open.temporaryChannelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures))) - case _ => // we agree on channel type + Some(InvalidChannelType(channelId, channelType, ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures))) + case _ => + // we agree on channel type + None + } + } + + /** Called by the funder of a single-funder channel. */ + def validateParamsFunder(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenChannel, accept: AcceptChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + validateChannelType(open.temporaryChannelId, channelType, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { + case Some(t) => return Left(t) + case None => // we agree on channel type } if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) @@ -201,6 +237,35 @@ object Helpers { extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) } + /** Called by the initiator of a dual-funded channel. */ + def validateParamsInitiator(nodeParams: NodeParams, channelType: SupportedChannelType, localFeatures: Features[InitFeature], remoteFeatures: Features[InitFeature], open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): Either[ChannelException, (ChannelFeatures, Option[ByteVector])] = { + validateChannelType(open.temporaryChannelId, channelType, open.channelType_opt, accept.channelType_opt, localFeatures, remoteFeatures) match { + case Some(t) => return Left(t) + case None => // we agree on channel type + } + + if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) + + if (accept.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimit, Channel.MIN_DUST_LIMIT)) + if (accept.dustLimit > nodeParams.channelConf.maxRemoteDustLimit) return Left(DustLimitTooLarge(open.temporaryChannelId, accept.dustLimit, nodeParams.channelConf.maxRemoteDustLimit)) + + // if minimum_depth is unreasonably large: + // MAY reject the channel. + if (accept.toSelfDelay > Channel.MAX_TO_SELF_DELAY || accept.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(accept.temporaryChannelId, accept.toSelfDelay, nodeParams.channelConf.maxToLocalDelay)) + + val channelFeatures = ChannelFeatures(channelType, localFeatures, remoteFeatures) + extractShutdownScript(accept.temporaryChannelId, localFeatures, remoteFeatures, accept.upfrontShutdownScript_opt).map(script_opt => (channelFeatures, script_opt)) + } + + /** Compute the channelId of a dual-funded channel. */ + def computeChannelId(open: OpenDualFundedChannel, accept: AcceptDualFundedChannel): ByteVector32 = { + if (LexicographicalOrdering.isLessThan(open.revocationBasepoint.value, accept.revocationBasepoint.value)) { + Crypto.sha256(open.revocationBasepoint.value ++ accept.revocationBasepoint.value) + } else { + Crypto.sha256(accept.revocationBasepoint.value ++ open.revocationBasepoint.value) + } + } + /** * Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by * other nodes. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index d3402066a2..809ea23ee2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -164,6 +164,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val extends FSM[ChannelState, ChannelData] with FSMDiagnosticActorLogging[ChannelState, ChannelData] with ChannelOpenSingleFunder + with ChannelOpenDualFunded with CommonHandlers with FundingHandlers with ErrorHandlers { @@ -208,43 +209,77 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, remote, remoteInit, channelFlags, channelConfig, channelType), Nothing) => - context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, temporaryChannelId, commitTxFeerate, Some(fundingTxFeerate))) - activeConnection = remote - txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) - val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey - val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - val localShutdownScript = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty - val open = OpenChannel(nodeParams.chainHash, - temporaryChannelId = temporaryChannelId, - fundingSatoshis = fundingSatoshis, - pushMsat = pushMsat, - dustLimitSatoshis = localParams.dustLimit, - maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, - channelReserveSatoshis = localParams.requestedChannelReserve, - htlcMinimumMsat = localParams.htlcMinimum, - feeratePerKw = commitTxFeerate, - toSelfDelay = localParams.toSelfDelay, - maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, - fundingPubkey = fundingPubKey, - revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, - paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), - delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, - htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, - firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - channelFlags = channelFlags, - tlvStream = TlvStream( - ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), - ChannelTlv.ChannelTypeTlv(channelType) - )) - goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open - - case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isInitiator => - activeConnection = remote - txPublisher ! SetChannelId(remoteNodeId, inputFundee.temporaryChannelId) - goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(inputFundee) + case Event(input: INPUT_INIT_CHANNEL_INITIATOR, Nothing) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = true, input.temporaryChannelId, input.commitTxFeerate, Some(input.fundingTxFeerate))) + activeConnection = input.remote + txPublisher ! SetChannelId(remoteNodeId, input.temporaryChannelId) + val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) + if (input.dualFunded) { + val tlvs: TlvStream[OpenDualFundedChannelTlv] = if (Features.canUseFeature(input.localParams.initFeatures, input.remoteInit.features, Features.UpfrontShutdownScript)) { + TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(input.localParams.defaultFinalScriptPubKey), ChannelTlv.ChannelTypeTlv(input.channelType)) + } else { + TlvStream(ChannelTlv.ChannelTypeTlv(input.channelType)) + } + val open = OpenDualFundedChannel( + chainHash = nodeParams.chainHash, + temporaryChannelId = input.temporaryChannelId, + fundingFeerate = input.fundingTxFeerate, + commitmentFeerate = input.commitTxFeerate, + fundingAmount = input.fundingAmount, + dustLimit = input.localParams.dustLimit, + maxHtlcValueInFlightMsat = input.localParams.maxHtlcValueInFlightMsat, + htlcMinimum = input.localParams.htlcMinimum, + toSelfDelay = input.localParams.toSelfDelay, + maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, + lockTime = nodeParams.currentBlockHeight.toLong, + fundingPubkey = fundingPubKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + channelFlags = input.channelFlags, + tlvStream = tlvs) + goto(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) using DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(input, open) sending open + } else { + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (Features.canUseFeature(input.localParams.initFeatures, input.remoteInit.features, Features.UpfrontShutdownScript)) input.localParams.defaultFinalScriptPubKey else ByteVector.empty + val open = OpenChannel( + chainHash = nodeParams.chainHash, + temporaryChannelId = input.temporaryChannelId, + fundingSatoshis = input.fundingAmount, + pushMsat = input.pushAmount_opt.getOrElse(0 msat), + dustLimitSatoshis = input.localParams.dustLimit, + maxHtlcValueInFlightMsat = input.localParams.maxHtlcValueInFlightMsat, + channelReserveSatoshis = input.localParams.requestedChannelReserve, + htlcMinimumMsat = input.localParams.htlcMinimum, + feeratePerKw = input.commitTxFeerate, + toSelfDelay = input.localParams.toSelfDelay, + maxAcceptedHtlcs = input.localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubKey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = input.localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + channelFlags = input.channelFlags, + tlvStream = TlvStream( + ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), + ChannelTlv.ChannelTypeTlv(input.channelType) + )) + goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open + } + + case Event(input: INPUT_INIT_CHANNEL_NON_INITIATOR, Nothing) if !input.localParams.isInitiator => + activeConnection = input.remote + txPublisher ! SetChannelId(remoteNodeId, input.temporaryChannelId) + if (input.dualFunded) { + goto(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) using DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(input) + } else { + goto(WAIT_FOR_OPEN_CHANNEL) using DATA_WAIT_FOR_OPEN_CHANNEL(input) + } case Event(INPUT_RESTORED(data), _) => log.debug("restoring channel") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala new file mode 100644 index 0000000000..692c795fa6 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -0,0 +1,200 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.fsm + +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.eclair.Features +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, AcceptDualFundedChannelTlv, ChannelTlv, Error, OpenDualFundedChannel, TlvStream} + +/** + * Created by t-bast on 19/04/2022. + */ + +trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { + + this: Channel => + + /* + INITIATOR NON_INITIATOR + | | + | open_channel2 | WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL + |-------------------------------->| + WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL | | + | accept_channel2 | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_COMPLETE | | WAIT_FOR_DUAL_FUNDING_COMPLETE + | | + | . | + | . | + | . | + | tx_complete | + |-------------------------------->| + | tx_complete | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_SIGNED | | WAIT_FOR_DUAL_FUNDING_SIGNED + | commitment_signed | + |-------------------------------->| + | commitment_signed | + |<--------------------------------| + | tx_signatures | + |<--------------------------------| + | tx_signatures | + |-------------------------------->| + WAIT_FOR_DUAL_FUNDING_LOCKED | | WAIT_FOR_DUAL_FUNDING_LOCKED + | funding_locked funding_locked | + |---------------- ---------------| + | \/ | + | /\ | + |<--------------- -------------->| + NORMAL | | NORMAL + */ + + when(WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL)(handleExceptions { + case Event(open: OpenDualFundedChannel, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => + import d.init.{localParams, remoteInit} + Helpers.validateParamsNonInitiator(nodeParams, d.init.channelType, open, remoteNodeId, localParams.initFeatures, remoteInit.features) match { + case Left(t) => handleLocalError(t, d, Some(open)) + case Right((channelFeatures, remoteShutdownScript)) => + context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isInitiator = false, open.temporaryChannelId, open.commitmentFeerate, Some(open.fundingFeerate))) + val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey + val channelKeyPath = keyManager.keyPath(localParams, d.init.channelConfig) + val totalFundingAmount = open.fundingAmount + d.init.fundingContribution_opt.getOrElse(0 sat) + val minimumDepth = Helpers.minDepthForFunding(nodeParams.channelConf, totalFundingAmount) + val tlvs: TlvStream[AcceptDualFundedChannelTlv] = if (Features.canUseFeature(localParams.initFeatures, remoteInit.features, Features.UpfrontShutdownScript)) { + TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(localParams.defaultFinalScriptPubKey), ChannelTlv.ChannelTypeTlv(d.init.channelType)) + } else { + TlvStream(ChannelTlv.ChannelTypeTlv(d.init.channelType)) + } + val accept = AcceptDualFundedChannel( + temporaryChannelId = open.temporaryChannelId, + fundingAmount = d.init.fundingContribution_opt.getOrElse(0 sat), + dustLimit = localParams.dustLimit, + maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, + htlcMinimum = localParams.htlcMinimum, + minimumDepth = minimumDepth, + toSelfDelay = localParams.toSelfDelay, + maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, + fundingPubkey = fundingPubkey, + revocationBasepoint = keyManager.revocationPoint(channelKeyPath).publicKey, + paymentBasepoint = localParams.walletStaticPaymentBasepoint.getOrElse(keyManager.paymentPoint(channelKeyPath).publicKey), + delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, + htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, + firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), + tlvStream = tlvs) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = open.dustLimit, + maxHtlcValueInFlightMsat = open.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity + htlcMinimum = open.htlcMinimum, + toSelfDelay = open.toSelfDelay, + maxAcceptedHtlcs = open.maxAcceptedHtlcs, + fundingPubKey = open.fundingPubkey, + revocationBasepoint = open.revocationBasepoint, + paymentBasepoint = open.paymentBasepoint, + delayedPaymentBasepoint = open.delayedPaymentBasepoint, + htlcBasepoint = open.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + val channelId = Helpers.computeChannelId(open, accept) + peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) sending accept + } + + case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) => handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => goto(CLOSED) + }) + + when(WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL)(handleExceptions { + case Event(accept: AcceptDualFundedChannel, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + import d.init.{localParams, remoteInit} + Helpers.validateParamsInitiator(nodeParams, d.init.channelType, localParams.initFeatures, remoteInit.features, d.lastSent, accept) match { + case Left(t) => + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(t, d, Some(accept)) + case Right((channelFeatures, remoteShutdownScript)) => + val channelId = Helpers.computeChannelId(d.lastSent, accept) + peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages + txPublisher ! SetChannelId(remoteNodeId, channelId) + context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) + val remoteParams = RemoteParams( + nodeId = remoteNodeId, + dustLimit = accept.dustLimit, + maxHtlcValueInFlightMsat = accept.maxHtlcValueInFlightMsat, + requestedChannelReserve_opt = None, // channel reserve will be computed based on channel capacity + htlcMinimum = accept.htlcMinimum, + toSelfDelay = accept.toSelfDelay, + maxAcceptedHtlcs = accept.maxAcceptedHtlcs, + fundingPubKey = accept.fundingPubkey, + revocationBasepoint = accept.revocationBasepoint, + paymentBasepoint = accept.paymentBasepoint, + delayedPaymentBasepoint = accept.delayedPaymentBasepoint, + htlcBasepoint = accept.htlcBasepoint, + initFeatures = remoteInit.features, + shutdownScript = remoteShutdownScript) + log.debug("remote params: {}", remoteParams) + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_DUAL_FUNDING_INTERNAL)(handleExceptions { + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, _) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index c1f9f50457..9acf67ec23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.TxOwner import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol.{AcceptChannel, AnnouncementSignatures, ChannelTlv, Error, FundingCreated, FundingLocked, FundingSigned, OpenChannel, TlvStream} -import fr.acinq.eclair.{Features, ShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} +import fr.acinq.eclair.{Features, MilliSatoshiLong, ShortChannelId, ToMilliSatoshiConversion, randomKey, toLongId} import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -73,7 +73,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { */ when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { - case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_FUNDEE(_, localParams, _, remoteInit, channelConfig, channelType))) => + case Event(open: OpenChannel, d@DATA_WAIT_FOR_OPEN_CHANNEL(INPUT_INIT_CHANNEL_NON_INITIATOR(_, _, _, localParams, _, remoteInit, channelConfig, channelType))) => Helpers.validateParamsFundee(nodeParams, channelType, localParams.initFeatures, open, remoteNodeId, remoteInit.features) match { case Left(t) => handleLocalError(t, d, Some(open)) case Right((channelFeatures, remoteShutdownScript)) => @@ -129,7 +129,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, commitTxFeerate, fundingTxFeerate, localParams, _, remoteInit, _, channelConfig, channelType), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, fundingSatoshis, _, commitTxFeerate, fundingTxFeerate, pushMsat_opt, localParams, _, remoteInit, _, channelConfig, channelType), open)) => Helpers.validateParamsFunder(nodeParams, channelType, localParams.initFeatures, remoteInit.features, open, accept) match { case Left(t) => channelOpenReplyToUser(Left(LocalError(t))) @@ -154,7 +154,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeerate).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat_opt.getOrElse(0 msat), commitTxFeerate, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => 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 e6da8cf17e..2c3044524a 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 @@ -22,7 +22,7 @@ import akka.event.Logging.MDC import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator @@ -134,61 +134,45 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA stay() case Event(c: Peer.OpenChannel, d: ConnectedData) => - if (c.fundingSatoshis >= Channel.MAX_FUNDING && !d.localFeatures.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)")) + if (c.fundingAmount >= Channel.MAX_FUNDING && !d.localFeatures.hasFeature(Wumbo)) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} 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 && !d.remoteFeatures.hasFeature(Wumbo)) { - sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big, the remote peer doesn't support wumbo")) + } else if (c.fundingAmount >= Channel.MAX_FUNDING && !d.remoteFeatures.hasFeature(Wumbo)) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} is too big, the remote peer doesn't support wumbo")) stay() - } else if (c.fundingSatoshis > nodeParams.channelConf.maxFundingSatoshis) { - sender() ! Status.Failure(new RuntimeException(s"fundingSatoshis=${c.fundingSatoshis} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) + } else if (c.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) { + sender() ! Status.Failure(new RuntimeException(s"fundingAmount=${c.fundingAmount} is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)")) stay() } else { val channelConfig = ChannelConfig.standard + val dualFunded = Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) // If a channel type was provided, we directly use it instead of computing it based on local and remote features. val channelType = c.channelType_opt.getOrElse(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingSatoshis, origin_opt = Some(sender())) + val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = true, c.fundingAmount, origin_opt = Some(sender())) c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) - val temporaryChannelId = randomBytes32() - val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingSatoshis, None) - val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) - log.info(s"requesting a new channel with type=$channelType 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.channelConf.channelFlags), channelConfig, channelType) + val temporaryChannelId = if (dualFunded) { + val channelKeyPath = nodeParams.channelKeyManager.keyPath(localParams, channelConfig) + val revocationBasepoint = nodeParams.channelKeyManager.revocationPoint(channelKeyPath).publicKey + Crypto.sha256(ByteVector.fill(33)(0) ++ revocationBasepoint.value) + } else { + randomBytes32() + } + val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) + val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelType, c.fundingAmount, None) + log.info(s"requesting a new channel with type=$channelType fundingAmount=${c.fundingAmount} dualFunded=$dualFunded pushAmount=${c.pushAmount_opt} fundingFeerate=$fundingTxFeerate temporaryChannelId=$temporaryChannelId localParams=$localParams") + channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, dualFunded, commitTxFeerate, fundingTxFeerate, c.pushAmount_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } - case Event(msg: protocol.OpenChannel, d: ConnectedData) => - d.channels.get(TemporaryChannelId(msg.temporaryChannelId)) match { - case None => - val channelConfig = ChannelConfig.standard - val chosenChannelType: Either[ChannelException, SupportedChannelType] = msg.channelType_opt match { - // remote explicitly specifies a channel type: we check whether we want to allow it - case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { - case Some(acceptedChannelType) => Right(acceptedChannelType) - case None => Left(InvalidChannelType(msg.temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType)) - } - // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` - case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(msg.temporaryChannelId)) - // remote doesn't specify a channel type: we use spec-defined defaults - case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) - } - chosenChannelType match { - case Right(channelType) => - val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = false, fundingAmount = msg.fundingSatoshis, origin_opt = None) - val temporaryChannelId = msg.temporaryChannelId - log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDEE(temporaryChannelId, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) - channel ! msg - stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) - case Left(ex) => - log.warning(s"ignoring open_channel: ${ex.getMessage}") - val err = Error(msg.temporaryChannelId, ex.getMessage) - self ! Peer.OutgoingMessage(err, d.peerConnection) - stay() - } - case Some(_) => - log.warning(s"ignoring open_channel with duplicate temporaryChannelId=${msg.temporaryChannelId}") - stay() + case Event(msg: protocol.OpenChannel, d: ConnectedData) => acceptOrRejectChannel(Left(msg), d) + + case Event(msg: protocol.OpenDualFundedChannel, d: ConnectedData) => + if (Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding)) { + acceptOrRejectChannel(Right(msg), d) + } else { + log.info("rejecting open_channel2: dual funding is not supported") + self ! Peer.OutgoingMessage(Error(msg.temporaryChannelId, "dual funding is not supported"), d.peerConnection) + stay() } case Event(msg: HasChannelId, d: ConnectedData) => @@ -379,6 +363,51 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA self ! Peer.OutgoingMessage(msg, peerConnection) } + def acceptOrRejectChannel(msg: Either[protocol.OpenChannel, protocol.OpenDualFundedChannel], d: ConnectedData): State = { + val temporaryChannelId = msg.fold(_.temporaryChannelId, _.temporaryChannelId) + val msgName = msg.fold(_ => "open_channel", _ => "open_channel2") + d.channels.get(TemporaryChannelId(temporaryChannelId)) match { + case None => + val channelConfig = ChannelConfig.standard + val channelType_opt = msg.fold(_.channelType_opt, _.channelType_opt) + val chosenChannelType: Either[ChannelException, SupportedChannelType] = channelType_opt match { + // remote explicitly specifies a channel type: we check whether we want to allow it + case Some(remoteChannelType) => ChannelTypes.areCompatible(d.localFeatures, remoteChannelType) match { + case Some(acceptedChannelType) => Right(acceptedChannelType) + case None => Left(InvalidChannelType(temporaryChannelId, ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures), remoteChannelType)) + } + // Bolt 2: if `option_channel_type` is negotiated: MUST set `channel_type` + case None if Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.ChannelType) => Left(MissingChannelType(temporaryChannelId)) + // remote doesn't specify a channel type: we use spec-defined defaults + case None => Right(ChannelTypes.defaultFromFeatures(d.localFeatures, d.remoteFeatures)) + } + chosenChannelType match { + case Right(channelType) => + val fundingAmount = msg.fold(_.fundingSatoshis, _.fundingAmount) + val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, channelType, isInitiator = false, fundingAmount, None) + log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") + msg match { + case Left(openSingleFunder) => + channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId, None, dualFunded = false, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! openSingleFunder + case Right(openDualFunded) => + // NB: we don't add a contribution to the funding amount. + channel ! INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId, None, dualFunded = true, localParams, d.peerConnection, d.remoteInit, channelConfig, channelType) + channel ! openDualFunded + } + stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) + case Left(ex) => + log.warning("ignoring {}: {}", msgName, ex.getMessage) + val err = Error(temporaryChannelId, ex.getMessage) + self ! Peer.OutgoingMessage(err, d.peerConnection) + stay() + } + case Some(_) => + log.warning("ignoring {} with duplicate temporaryChannelId={}", msgName, temporaryChannelId) + stay() + } + } + def stopPeer(): State = { log.info("removing peer from db") nodeParams.db.peers.removePeer(remoteNodeId) @@ -464,11 +493,14 @@ object Peer { } case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful - case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { - require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") - require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") - require(pushMsat >= 0.msat, s"pushMsat must be positive") - fundingTxFeeratePerKw_opt.foreach(feeratePerKw => require(feeratePerKw >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feeratePerKw is below minimum ${FeeratePerKw.MinimumFeeratePerKw} rate/kw")) + + case class OpenChannel(remoteNodeId: PublicKey, fundingAmount: Satoshi, channelType_opt: Option[SupportedChannelType], pushAmount_opt: Option[MilliSatoshi], fundingTxFeerate_opt: Option[FeeratePerKw], channelFlags_opt: Option[ChannelFlags], timeout_opt: Option[Timeout]) extends PossiblyHarmful { + require(fundingAmount > 0.sat, s"funding amount must be positive") + pushAmount_opt.foreach(pushAmount => { + require(pushAmount >= 0.msat, s"pushAmount must be positive") + require(pushAmount <= fundingAmount, s"pushAmount must be less than or equal to funding amount") + }) + fundingTxFeerate_opt.foreach(feerate => require(feerate >= FeeratePerKw.MinimumFeeratePerKw, s"fee rate $feerate is below minimum ${FeeratePerKw.MinimumFeeratePerKw}")) } case class GetPeerInfo(replyTo: Option[typed.ActorRef[PeerInfoResponse]]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index f0af4df86f..cb11d8867e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -97,12 +97,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I // standard conversion eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] - assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) + assert(open.fundingTxFeerate_opt === Some(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] - assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) + assert(open1.fundingTxFeerate_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) assert(open1.channelType_opt === Some(ChannelTypes.StaticRemoteKey)) } 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 a916f5225f..5f37170e24 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -44,7 +44,8 @@ import scala.concurrent.duration._ object TestConstants { val defaultBlockHeight = 400000 - val fundingSatoshis: Satoshi = 1000000L sat + val fundingSatoshis: Satoshi = 1000000 sat + val nonInitiatorFundingSatoshis: Satoshi = 500000 sat val pushMsat: MilliSatoshi = 200000000L msat val feeratePerKw: FeeratePerKw = FeeratePerKw(10000 sat) val anchorOutputsFeeratePerKw: FeeratePerKw = FeeratePerKw(2500 sat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index fa4daca90f..cb16f25ea0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -42,7 +42,6 @@ import org.scalatest.{Outcome, Tag} import java.util.UUID import java.util.concurrent.CountDownLatch -import scala.collection.immutable.Nil import scala.concurrent.duration._ import scala.util.Random @@ -80,9 +79,9 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe registerA ! alice registerB ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] pipe ! (alice, bob) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 93bd392c3b..46beb8d4b3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -61,6 +61,8 @@ trait ChannelStateTestsBase extends ChannelStateTestsHelperMethods with FixtureT object ChannelStateTestsTags { /** If set, channels will use option_support_large_channel. */ val Wumbo = "wumbo" + /** If set, channels will use option_dual_fund. */ + val DualFunding = "dual_funding" /** If set, channels will use option_static_remotekey. */ val StaticRemoteKey = "static_remotekey" /** If set, channels will use option_anchor_outputs. */ @@ -73,6 +75,8 @@ object ChannelStateTestsTags { val ChannelsPublic = "channels_public" /** If set, no amount will be pushed when opening a channel (by default we push a small amount). */ val NoPushMsat = "no_push_msat" + /** If set, the non-initiator of a dual-funded channel will contribute some funds. */ + val DualFundingContribution = "dual_funding_contribution" /** If set, max-htlc-value-in-flight will be set to the highest possible value for Alice and Bob. */ val NoMaxHtlcValueInFlight = "no_max_htlc_value_in_flight" /** If set, max-htlc-value-in-flight will be set to a low value for Alice. */ @@ -148,6 +152,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional).updated(Features.DualFunding, FeatureSupport.Optional)) .initFeatures() val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) @@ -157,6 +162,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.UpfrontShutdownScript, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.ChannelType))(_.updated(Features.ChannelType, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(ChannelStateTestsTags.DualFunding))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputsZeroFeeHtlcTx, FeatureSupport.Optional).updated(Features.DualFunding, FeatureSupport.Optional)) .initFeatures() val channelType = ChannelTypes.defaultFromFeatures(aliceInitFeatures, bobInitFeatures) @@ -187,17 +193,16 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { val (aliceParams, bobParams, channelType) = computeFeatures(setup, tags) val channelFlags = ChannelFlags(announceChannel = tags.contains(ChannelStateTestsTags.ChannelsPublic)) val commitTxFeerate = if (tags.contains(ChannelStateTestsTags.AnchorOutputs) || tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - val (fundingSatoshis, pushMsat) = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) { - (TestConstants.fundingSatoshis, 0.msat) - } else { - (TestConstants.fundingSatoshis, TestConstants.pushMsat) - } + val fundingAmount = TestConstants.fundingSatoshis + val pushMsat = if (tags.contains(ChannelStateTestsTags.NoPushMsat)) 0 msat else TestConstants.pushMsat + val nonInitiatorFundingAmount = if (tags.contains(ChannelStateTestsTags.DualFundingContribution)) Some(TestConstants.nonInitiatorFundingSatoshis) else None + val dualFunded = tags.contains(ChannelStateTestsTags.DualFunding) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFundingAmount, dualFunded, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 11f6dc52ba..e2c57046b6 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 @@ -63,8 +63,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(ChannelStateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) @@ -156,8 +156,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Bob advertises support for anchor outputs, but Alice doesn't. val aliceParams = Alice.channelParams val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputs)) alice2bob.forward(bob, open) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala new file mode 100644 index 0000000000..4d4e0bd275 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala @@ -0,0 +1,164 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.a + +import akka.actor.Status +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.TestConstants.Alice +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel} +import fr.acinq.eclair.{Features, TestConstants, TestKitBaseClass} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], open: OpenDualFundedChannel, aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init(wallet = new NoOpOnChainWallet()) + import setup._ + + val channelConfig = ChannelConfig.standard + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + val nonInitiatorContribution = if (test.tags.contains(ChannelStateTestsTags.DualFundingContribution)) Some(TestConstants.nonInitiatorFundingSatoshis) else None + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob, open) + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, open, aliceOrigin, alice2bob, bob2alice))) + } + } + + test("recv AcceptDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + assert(accept.upfrontShutdownScript_opt === None) + assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.fundingAmount === 0.sat) + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelIdAssigned]) + bob2alice.forward(alice, accept) + assert(listener.expectMsgType[ChannelIdAssigned].channelId === Helpers.computeChannelId(open, accept)) + + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + val channelFeatures = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].channelFeatures + assert(channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(channelFeatures.hasFeature(Features.DualFunding)) + aliceOrigin.expectNoMessage() + } + + test("recv AcceptDualFundedChannel (with non-initiator contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.DualFundingContribution)) { f => + import f._ + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + assert(accept.upfrontShutdownScript_opt === None) + assert(accept.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(accept.fundingAmount === TestConstants.nonInitiatorFundingSatoshis) + bob2alice.forward(alice, accept) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + } + + test("recv AcceptDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 + alice ! accept.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (dust limit too low)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val lowDustLimit = Channel.MIN_DUST_LIMIT - 1.sat + alice ! accept.copy(dustLimit = lowDustLimit) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, DustLimitTooSmall(accept.temporaryChannelId, lowDustLimit, Channel.MIN_DUST_LIMIT).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val highDustLimit = Alice.nodeParams.channelConf.maxRemoteDustLimit + 1.sat + alice ! accept.copy(dustLimit = highDustLimit) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, DustLimitTooLarge(accept.temporaryChannelId, highDustLimit, Alice.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv AcceptDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 + alice ! accept.copy(toSelfDelay = delayTooHigh) + val error = alice2bob.expectMsgType[Error] + assert(error === Error(accept.temporaryChannelId, ToSelfDelayTooHigh(accept.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! Error(ByteVector32.Zeroes, "dual funding not supported") + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! TickChannelOpenTimeout + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + +} 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 84ea97e2ce..0e03c1006d 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 @@ -56,8 +56,8 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, commitTxFeerate, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala new file mode 100644 index 0000000000..0beec52237 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenDualFundedChannelStateSpec.scala @@ -0,0 +1,166 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.a + +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, SatoshiLong} +import fr.acinq.eclair.TestConstants.{Alice, Bob} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel} +import fr.acinq.eclair.{Features, TestConstants, TestKitBaseClass, randomBytes32} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, eventListener: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init() + import setup._ + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelCreated]) + system.eventStream.subscribe(listener.ref, classOf[ChannelIdAssigned]) + val channelConfig = ChannelConfig.standard + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + awaitCond(bob.stateName == WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, listener))) + } + } + + test("recv OpenDualFundedChannel", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + assert(open.upfrontShutdownScript_opt === None) + assert(open.channelType_opt === Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx)) + assert(open.fundingFeerate === TestConstants.feeratePerKw) + assert(open.commitmentFeerate === TestConstants.anchorOutputsFeeratePerKw) + assert(open.lockTime === TestConstants.defaultBlockHeight) + + val initiatorEvent = eventListener.expectMsgType[ChannelCreated] + assert(initiatorEvent.isInitiator) + assert(initiatorEvent.temporaryChannelId === ByteVector32.Zeroes) + + alice2bob.forward(bob) + + val nonInitiatorEvent = eventListener.expectMsgType[ChannelCreated] + assert(!nonInitiatorEvent.isInitiator) + assert(nonInitiatorEvent.temporaryChannelId === ByteVector32.Zeroes) + + val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] + val channelIdAssigned = eventListener.expectMsgType[ChannelIdAssigned] + assert(channelIdAssigned.temporaryChannelId === ByteVector32.Zeroes) + assert(channelIdAssigned.channelId === Helpers.computeChannelId(open, accept)) + + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + val channelFeatures = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].channelFeatures + assert(channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(channelFeatures.hasFeature(Features.DualFunding)) + } + + test("recv OpenDualFundedChannel (invalid chain)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val chain = randomBytes32() + bob ! open.copy(chainHash = chain) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidChainHash(open.temporaryChannelId, Block.RegtestGenesisBlock.hash, chain).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (funding too low)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + bob ! open.copy(fundingAmount = 100 sat) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidFundingAmount(open.temporaryChannelId, 100 sat, Bob.nodeParams.channelConf.minFundingSatoshis(false), Bob.nodeParams.channelConf.maxFundingSatoshis).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (invalid max accepted htlcs)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val invalidMaxAcceptedHtlcs = Channel.MAX_ACCEPTED_HTLCS + 1 + bob ! open.copy(maxAcceptedHtlcs = invalidMaxAcceptedHtlcs) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, InvalidMaxAcceptedHtlcs(open.temporaryChannelId, invalidMaxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (to_self_delay too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val delayTooHigh = Alice.nodeParams.channelConf.maxToLocalDelay + 1 + bob ! open.copy(toSelfDelay = delayTooHigh) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, ToSelfDelayTooHigh(open.temporaryChannelId, delayTooHigh, Alice.nodeParams.channelConf.maxToLocalDelay).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (dust limit too high)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val dustLimitTooHigh = Bob.nodeParams.channelConf.maxRemoteDustLimit + 1.sat + bob ! open.copy(dustLimit = dustLimitTooHigh) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, DustLimitTooLarge(open.temporaryChannelId, dustLimitTooHigh, Bob.nodeParams.channelConf.maxRemoteDustLimit).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv OpenDualFundedChannel (dust limit too small)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenDualFundedChannel] + val dustLimitTooSmall = Channel.MIN_DUST_LIMIT - 1.sat + bob ! open.copy(dustLimit = dustLimitTooSmall) + val error = bob2alice.expectMsgType[Error] + assert(error === Error(open.temporaryChannelId, DustLimitTooSmall(open.temporaryChannelId, dustLimitTooSmall, Channel.MIN_DUST_LIMIT).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + bob ! Error(ByteVector32.Zeroes, "dual funding not supported") + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val sender = TestProbe() + val cmd = CMD_CLOSE(sender.ref, None, None) + bob ! cmd + sender.expectMsg(RES_SUCCESS(cmd, ByteVector32.Zeroes)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == CLOSED) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index d39673f677..5ca513df88 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -64,9 +64,9 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index 7fa5845ec6..8b22e15554 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -47,8 +47,8 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) bob2alice.expectMsgType[AcceptChannel] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index b03b8d00e3..8ae744e25e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -63,9 +63,9 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index 8b7a8c7480..d6b99ae9c4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -28,8 +28,8 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.wire.protocol.{AcceptChannel, Error, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.scalatest.{Outcome, Tag} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import scala.concurrent.duration._ @@ -55,9 +55,9 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF within(30 seconds) { val listener = TestProbe() system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index a1bb4b4098..f1c17b8b3d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -52,9 +52,9 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS within(30 seconds) { alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 933cd79311..f3d794570e 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 @@ -69,9 +69,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(TestConstants.pushMsat), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) 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 0b540986b5..b05f48ec21 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 @@ -28,7 +28,6 @@ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.RouteCalculation.ROUTE_MAX_LENGTH import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, SearchBoundaries, NORMAL => _, State => _} -import fr.acinq.eclair.wire.protocol.NodeAddress import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Kit, MilliSatoshi, MilliSatoshiLong, Setup, TestKitBaseClass} import grizzled.slf4j.Logging import org.json4s.{DefaultFormats, Formats} @@ -165,16 +164,16 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit sender.expectMsgType[PeerConnection.ConnectionResult.HasConnection](10 seconds) } - def connect(node1: Kit, node2: Kit, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi): ChannelOpenResponse.ChannelOpened = { + def connect(node1: Kit, node2: Kit, fundingAmount: Satoshi, pushMsat: MilliSatoshi): ChannelOpenResponse.ChannelOpened = { val sender = TestProbe() connect(node1, node2) sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, - fundingSatoshis = fundingSatoshis, - pushMsat = pushMsat, + fundingAmount = fundingAmount, channelType_opt = None, - fundingTxFeeratePerKw_opt = None, - channelFlags = None, + pushAmount_opt = Some(pushMsat), + fundingTxFeerate_opt = None, + channelFlags_opt = None, timeout_opt = None)) sender.expectMsgType[ChannelOpenResponse.ChannelOpened](10 seconds) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 9e36e63243..fb5d8c7b1b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -75,9 +75,9 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Some(1000000000 msat), Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] - bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] pipe ! (alice, bob) within(30 seconds) { 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 f5387b4c77..19ff37e8a6 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 @@ -70,10 +70,11 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.ChannelType))(Features(ChannelType -> Optional)) - .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(Features(StaticRemoteKey -> Optional)) - .modify(_.features).setToIf(test.tags.contains("wumbo"))(Features(Wumbo -> Optional)) - .modify(_.features).setToIf(test.tags.contains("anchor_outputs"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) - .modify(_.features).setToIf(test.tags.contains("anchor_outputs_zero_fee_htlc_tx"))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.StaticRemoteKey))(Features(StaticRemoteKey -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.Wumbo))(Features(Wumbo -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.AnchorOutputs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional)) + .modify(_.features).setToIf(test.tags.contains(ChannelStateTestsTags.DualFunding))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)) .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -275,7 +276,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage() peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].temporaryChannelId === open.temporaryChannelId) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].temporaryChannelId === open.temporaryChannelId) channel.expectMsg(open) // open_channel messages with the same temporary channel id should simply be ignored @@ -294,12 +295,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)") } - test("don't spawn a wumbo channel if remote doesn't support wumbo", Tag("wumbo")) { f => + test("don't spawn a wumbo channel if remote doesn't support wumbo", Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val probe = TestProbe() @@ -308,12 +309,12 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard) // Bob doesn't support wumbo, Alice does assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, the remote peer doesn't support wumbo") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big, the remote peer doesn't support wumbo") } - test("don't spawn a channel if fundingSatoshis is greater than maxFundingSatoshis", Tag("high-max-funding-satoshis"), Tag("wumbo")) { f => + test("don't spawn a channel if fundingSatoshis is greater than maxFundingSatoshis", Tag("high-max-funding-satoshis"), Tag(ChannelStateTestsTags.Wumbo)) { f => import f._ val probe = TestProbe() @@ -322,9 +323,9 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(Wumbo -> Optional))) // Bob supports wumbo assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None)) - assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)") + assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingAmount=$fundingAmountBig is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)") } test("don't spawn a channel if we don't support their channel type") { f => @@ -370,7 +371,40 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle peerConnection.expectMsg(Error(open.temporaryChannelId, "option_channel_type was negotiated but channel_type is missing")) } - test("use their channel type when spawning a channel", Tag("static_remotekey")) { f => + test("don't spawn a dual funded channel if not supported") { f => + import f._ + + connect(remoteNodeId, peer, peerConnection, switchboard) + val open = createOpenDualFundedChannelMessage() + peerConnection.send(peer, open) + peerConnection.expectMsg(Error(open.temporaryChannelId, "dual funding is not supported")) + } + + test("use dual-funding when available", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + // Both peers support option_dual_fund, so it is automatically used. + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) + assert(peer.stateData.channels.isEmpty) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded) + } + + test("accept dual-funded channels when available", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + // Both peers support option_dual_fund, so it is automatically used. + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) + assert(peer.stateData.channels.isEmpty) + val open = createOpenDualFundedChannelMessage() + peerConnection.send(peer, open) + awaitCond(peer.stateData.channels.nonEmpty) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].dualFunded) + channel.expectMsg(open) + } + + test("use their channel type when spawning a channel", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ // We both support option_static_remotekey but they want to open a standard channel. @@ -379,30 +413,32 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val open = createOpenChannelMessage(TlvStream[OpenChannelTlv](ChannelTlv.ChannelTypeTlv(ChannelTypes.Standard))) peerConnection.send(peer, open) awaitCond(peer.stateData.channels.nonEmpty) - assert(channel.expectMsgType[INPUT_INIT_FUNDEE].channelType === ChannelTypes.Standard) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] + assert(init.channelType === ChannelTypes.Standard) + assert(!init.dualFunded) channel.expectMsg(open) } - test("use requested channel type when spawning a channel", Tag("static_remotekey")) { f => + test("use requested channel type when spawning a channel", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.StaticRemoteKey) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType === ChannelTypes.StaticRemoteKey) // We can create channels that don't use the features we have enabled. - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.Standard), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.Standard) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.Standard), None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType === ChannelTypes.Standard) // We can create channels that use features that we haven't enabled. - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, Some(ChannelTypes.AnchorOutputs), None, None, None)) - assert(channel.expectMsgType[INPUT_INIT_FUNDER].channelType === ChannelTypes.AnchorOutputs) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs), None, None, None, None)) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType === ChannelTypes.AnchorOutputs) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ val probe = TestProbe() @@ -412,15 +448,16 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // We ensure the current network feerate is higher than the default anchor output feerate. val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType === ChannelTypes.AnchorOutputs) + assert(!init.dualFunded) assert(init.fundingAmount === 15000.sat) assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } - test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag("anchor_outputs_zero_fee_htlc_tx")) { f => + test("use correct on-chain fee rates when spawning a channel (anchor outputs zero fee htlc)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val probe = TestProbe() @@ -430,22 +467,24 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // We ensure the current network feerate is higher than the default anchor output feerate. val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(mempoolMinFee = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + assert(!init.dualFunded) assert(init.fundingAmount === 15000.sat) assert(init.commitTxFeerate === TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeerate === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } - test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => + test("use correct final script if option_static_remotekey is negotiated", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType === ChannelTypes.StaticRemoteKey) + assert(!init.dualFunded) assert(init.localParams.walletStaticPaymentBasepoint.isDefined) assert(init.localParams.defaultFinalScriptPubKey === Script.write(Script.pay2wpkh(init.localParams.walletStaticPaymentBasepoint.get))) } @@ -462,10 +501,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } val peer = TestFSMRef(new Peer(TestConstants.Alice.nodeParams, remoteNodeId, new DummyOnChainWallet(), channelFactory, switchboard.ref)) connect(remoteNodeId, peer, peerConnection, switchboard) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None)) + val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.fundingAmount === 15000.sat) - assert(init.pushAmount === 100.msat) + assert(init.pushAmount_opt === Some(100.msat)) } test("handle final channelId assigned in state DISCONNECTED") { f => @@ -537,4 +576,8 @@ object PeerSpec { protocol.OpenChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), 25000 sat, 0 msat, 483 sat, UInt64(100), 1000 sat, 1 msat, TestConstants.feeratePerKw, CltvExpiryDelta(144), 10, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private, openTlv) } + def createOpenDualFundedChannelMessage(): protocol.OpenDualFundedChannel = { + protocol.OpenDualFundedChannel(Block.RegtestGenesisBlock.hash, randomBytes32(), TestConstants.feeratePerKw, TestConstants.anchorOutputsFeeratePerKw, 25000 sat, 483 sat, UInt64(100), 1 msat, CltvExpiryDelta(144), 10, 0, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, randomKey().publicKey, ChannelFlags.Private) + } + } \ No newline at end of file From 00f3bf694bc6c8a0f68a518f9fa91ffcce2813c9 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 3 May 2022 14:17:57 +0200 Subject: [PATCH 7/7] Implement interactive-tx construction We build the funding transaction based on the interactive-tx protocol. We currently stop before exchanging signatures. Future commits will add the signature exchange and complete the dual funding flow. --- .../eclair/blockchain/OnChainWallet.scala | 27 +- .../bitcoind/rpc/BitcoinCoreClient.scala | 17 +- .../fr/acinq/eclair/channel/ChannelData.scala | 32 +- .../eclair/channel/ChannelExceptions.scala | 13 + .../acinq/eclair/channel/InteractiveTx.scala | 255 +++++++++++++ .../eclair/channel/InteractiveTxFunder.scala | 200 +++++++++++ .../channel/fsm/ChannelOpenDualFunded.scala | 150 +++++++- .../eclair/channel/fsm/CommonHandlers.scala | 5 + .../eclair/transactions/Transactions.scala | 2 + .../wire/protocol/LightningMessageTypes.scala | 12 +- .../blockchain/DummyOnChainWallet.scala | 31 +- .../bitcoind/BitcoinCoreClientSpec.scala | 2 +- .../blockchain/bitcoind/BitcoindService.scala | 7 +- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 3 +- .../channel/InteractiveTxFunderSpec.scala | 248 +++++++++++++ .../eclair/channel/InteractiveTxSpec.scala | 338 ++++++++++++++++++ .../WaitForDualFundingCreatedStateSpec.scala | 259 ++++++++++++++ .../WaitForDualFundingInternalStateSpec.scala | 146 ++++++++ 18 files changed, 1709 insertions(+), 38 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxFunder.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingInternalStateSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index 3f62f45009..61006822d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Satoshi, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector @@ -30,10 +30,16 @@ import scala.concurrent.{ExecutionContext, Future} /** This trait lets users fund lightning channels. */ trait OnChainChannelFunder { - import OnChainWallet.MakeFundingTxResponse + import OnChainWallet._ - /** Create a channel funding transaction with the provided pubkeyScript. */ - def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] + /** Fund the provided transaction by adding inputs (and a change output if necessary). */ + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] + + /** Sign the wallet inputs of the provided transaction. */ + def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] + + /** Create a fully signed channel funding transaction with the provided pubkeyScript. */ + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] /** * Committing *must* include publishing the transaction on the network. @@ -47,9 +53,10 @@ trait OnChainChannelFunder { */ def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] - /** - * Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos". - */ + /** Return the transaction if it exists, either in the blockchain or in the mempool. */ + def getTransaction(txId: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] + + /** Rollback a transaction that we failed to commit: this probably translates to "release locks on utxos". */ def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] /** @@ -97,4 +104,10 @@ object OnChainWallet { final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) + final case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) { + val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum + } + + final case class SignTransactionResponse(tx: Transaction, complete: Boolean) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 018fd52a03..ef9b9315a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -17,11 +17,11 @@ package fr.acinq.eclair.blockchain.bitcoind.rpc import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.{Bech32, Block} import fr.acinq.bitcoin.scalacompat._ +import fr.acinq.bitcoin.{Bech32, Block} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet -import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw} import fr.acinq.eclair.transactions.Transactions @@ -186,6 +186,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall }) } + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, lockUtxos)) + } + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val partialFundingTx = Transaction( version = 2, @@ -221,11 +225,12 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient) extends OnChainWall def signTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = signTransaction(tx, Nil) + def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = signTransaction(tx, Nil, allowIncomplete) + def signTransaction(tx: Transaction, previousTxs: Seq[PreviousTx], allowIncomplete: Boolean = false)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = { rpcClient.invoke("signrawtransactionwithwallet", tx.toString(), previousTxs).map(json => { val JString(hex) = json \ "hex" val JBool(complete) = json \ "complete" - // TODO: remove allowIncomplete once https://github.com/bitcoin/bitcoin/issues/21151 is fixed if (!complete && !allowIncomplete) { val JArray(errors) = json \ "errors" val message = errors.map(error => { @@ -439,10 +444,6 @@ object BitcoinCoreClient { } } - case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) { - val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum - } - case class PreviousTx(txid: ByteVector32, vout: Long, scriptPubKey: String, redeemScript: String, witnessScript: String, amount: BigDecimal) object PreviousTx { @@ -456,8 +457,6 @@ object BitcoinCoreClient { ) } - case class SignTransactionResponse(tx: Transaction, complete: Boolean) - /** * Information about a transaction currently in the mempool. * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 78b68e0f7d..4a91abb426 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -20,10 +20,11 @@ import akka.actor.{ActorRef, PossiblyHarmful} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.InteractiveTx.{InteractiveTxParams, InteractiveTxSession} import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, CommitSig, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, InteractiveTxMessage, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -59,6 +60,8 @@ case object WAIT_FOR_FUNDING_LOCKED extends ChannelState case object WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL extends ChannelState case object WAIT_FOR_DUAL_FUNDING_INTERNAL extends ChannelState +case object WAIT_FOR_DUAL_FUNDING_CREATED extends ChannelState +case object WAIT_FOR_DUAL_FUNDING_SIGNED extends ChannelState // Channel opened: case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState @@ -445,7 +448,32 @@ final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANN final case class DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, - channelFeatures: ChannelFeatures) extends TransientChannelData + fundingParams: InteractiveTxParams, + commitTxFeerate: FeeratePerKw, + remoteFirstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + channelConfig: ChannelConfig, + channelFeatures: ChannelFeatures, + remoteMessage: Option[InteractiveTxMessage]) extends TransientChannelData +final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32, + localParams: LocalParams, + remoteParams: RemoteParams, + fundingParams: InteractiveTxParams, + txSession: InteractiveTxSession, + commitTxFeerate: FeeratePerKw, + remoteFirstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + channelConfig: ChannelConfig, + channelFeatures: ChannelFeatures) extends TransientChannelData +final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelId: ByteVector32, + localParams: LocalParams, + remoteParams: RemoteParams, + fundingParams: InteractiveTxParams, + commitTxFeerate: FeeratePerKw, + remoteFirstPerCommitmentPoint: PublicKey, + channelFlags: ChannelFlags, + channelConfig: ChannelConfig, + channelFeatures: ChannelFeatures) extends TransientChannelData final case class DATA_NORMAL(commitments: Commitments, shortChannelId: ShortChannelId, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 222726ed80..34a3968e5c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -50,6 +50,19 @@ case class ChannelReserveTooHigh (override val channelId: Byte case class ChannelReserveBelowOurDustLimit (override val channelId: ByteVector32, channelReserve: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"their channelReserve=$channelReserve is below our dustLimit=$dustLimit") case class ChannelReserveNotMet (override val channelId: ByteVector32, toLocal: MilliSatoshi, toRemote: MilliSatoshi, reserve: Satoshi) extends ChannelException(channelId, s"channel reserve is not met toLocal=$toLocal toRemote=$toRemote reserve=$reserve") case class ChannelFundingError (override val channelId: ByteVector32) extends ChannelException(channelId, "channel funding error") +case class InvalidSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"invalid serial_id=${serialId.toByteVector}") +case class DuplicateSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"duplicate serial_id=${serialId.toByteVector}") +case class UnknownSerialId (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"unknown serial_id=${serialId.toByteVector}") +case class DuplicateInput (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"duplicate input $previousTxId:$previousTxOutput (serial_id=${serialId.toByteVector})") +case class InputOutOfBounds (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"invalid input $previousTxId:$previousTxOutput (serial_id=${serialId.toByteVector})") +case class NonSegwitInput (override val channelId: ByteVector32, serialId: UInt64, previousTxId: ByteVector32, previousTxOutput: Long) extends ChannelException(channelId, s"$previousTxId:$previousTxOutput is not a native segwit input (serial_id=${serialId.toByteVector})") +case class OutputBelowDust (override val channelId: ByteVector32, serialId: UInt64, amount: Satoshi, dustLimit: Satoshi) extends ChannelException(channelId, s"invalid output amount=$amount below dust=$dustLimit (serial_id=${serialId.toByteVector})") +case class NonSegwitOutput (override val channelId: ByteVector32, serialId: UInt64) extends ChannelException(channelId, s"output with serial_id=${serialId.toByteVector} is not a native segwit output") +case class InvalidCompleteInteractiveTx (override val channelId: ByteVector32) extends ChannelException(channelId, "the completed interactive tx is invalid") +case class TooManyInteractiveTxRounds (override val channelId: ByteVector32) extends ChannelException(channelId, "too many messages exchanged during interactive tx construction") +case class DualFundingAborted (override val channelId: ByteVector32) extends ChannelException(channelId, "dual funding aborted") +case class InvalidFundingSignature (override val channelId: ByteVector32, tx_opt: Option[Transaction]) extends ChannelException(channelId, s"invalid funding signature: tx=${tx_opt.map(_.toString()).getOrElse("n/a")}") +case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt") case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class NoMoreFeeUpdateClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new update_fee, closing in progress") case class ClosingAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "closing already in progress") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala new file mode 100644 index 0000000000..4bb6de45fc --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala @@ -0,0 +1,255 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel + +import akka.event.LoggingAdapter +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair.blockchain.OnChainChannelFunder +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.wire.protocol._ +import scodec.bits.ByteVector + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Created by t-bast on 27/04/2022. + */ + +/** + * Implementation of the interactive-tx protocol. + * It allows two participants to collaborate to create a shared transaction. + * This is a turn-based protocol: each participant sends one message and then waits for the other participant's response. + */ +object InteractiveTx { + + // Example flow: + // +-------+ +-------+ + // | |---(1)-- tx_add_input ------>| | + // | |<--(2)-- tx_add_input -------| | + // | |---(3)-- tx_add_output ----->| | + // | |<--(4)-- tx_add_output ------| | + // | |---(5)-- tx_add_input ------>| | + // | A |<--(6)-- tx_complete --------| B | + // | |---(7)-- tx_remove_output -->| | + // | |<--(8)-- tx_add_output ------| | + // | |---(9)-- tx_complete ------->| | + // | |<--(10)- tx_complete --------| | + // +-------+ +-------+ + + case class InteractiveTxParams(channelId: ByteVector32, + isInitiator: Boolean, + localAmount: Satoshi, + remoteAmount: Satoshi, + fundingPubkeyScript: ByteVector, + lockTime: Long, + dustLimit: Satoshi, + targetFeerate: FeeratePerKw) { + val fundingAmount: Satoshi = localAmount + remoteAmount + } + + case class InteractiveTxSession(toSend: Seq[Either[TxAddInput, TxAddOutput]], + localInputs: Seq[TxAddInput] = Nil, + remoteInputs: Seq[TxAddInput] = Nil, + localOutputs: Seq[TxAddOutput] = Nil, + remoteOutputs: Seq[TxAddOutput] = Nil, + txCompleteSent: Boolean = false, + txCompleteReceived: Boolean = false, + inputsReceivedCount: Int = 0, + outputsReceivedCount: Int = 0) { + val isComplete: Boolean = txCompleteSent && txCompleteReceived + } + + /** Inputs and outputs we contribute to the funding transaction. */ + case class FundingContributions(inputs: Seq[TxAddInput], outputs: Seq[TxAddOutput]) + + /** Unsigned transaction created collaboratively. */ + case class SharedTransaction(localInputs: Seq[TxAddInput], remoteInputs: Seq[TxAddInput], localOutputs: Seq[TxAddOutput], remoteOutputs: Seq[TxAddOutput], lockTime: Long) { + def buildUnsignedTx(): Transaction = { + val inputs = (localInputs ++ remoteInputs).sortBy(_.serialId).map(i => TxIn(toOutPoint(i), ByteVector.empty, i.sequence)) + val outputs = (localOutputs ++ remoteOutputs).sortBy(_.serialId).map(o => TxOut(o.amount, o.pubkeyScript)) + Transaction(2, inputs, outputs, lockTime) + } + } + + // We restrict the number of inputs / outputs that our peer can send us to ensure the protocol eventually ends. + val MAX_INPUTS_OUTPUTS_RECEIVED = 4096 + + def start(params: InteractiveTxParams, localContributions: FundingContributions): (InteractiveTxSession, Option[InteractiveTxConstructionMessage]) = { + val toSend = localContributions.inputs.map(Left(_)) ++ localContributions.outputs.map(Right(_)) + if (params.isInitiator) { + // The initiator sends the first message. + send(InteractiveTxSession(toSend), params) + } else { + // The non-initiator waits for the initiator to send the first message. + (InteractiveTxSession(toSend), None) + } + } + + private def send(session: InteractiveTxSession, params: InteractiveTxParams): (InteractiveTxSession, Option[InteractiveTxConstructionMessage]) = { + session.toSend.headOption match { + case Some(Left(addInput)) => + val next = session.copy(toSend = session.toSend.tail, localInputs = session.localInputs :+ addInput, txCompleteSent = false) + (next, Some(addInput)) + case Some(Right(addOutput)) => + val next = session.copy(toSend = session.toSend.tail, localOutputs = session.localOutputs :+ addOutput, txCompleteSent = false) + (next, Some(addOutput)) + case None => + val nextState = session.copy(txCompleteSent = true) + (nextState, Some(TxComplete(params.channelId))) + } + } + + def receive(session: InteractiveTxSession, params: InteractiveTxParams, msg: InteractiveTxConstructionMessage): Either[ChannelException, (InteractiveTxSession, Option[InteractiveTxConstructionMessage])] = { + msg match { + case msg: HasSerialId if msg.serialId.toByteVector.bits.last != params.isInitiator => + Left(InvalidSerialId(params.channelId, msg.serialId)) + case addInput: TxAddInput => + if (session.inputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { + Left(TooManyInteractiveTxRounds(params.channelId)) + } else if (session.remoteInputs.exists(_.serialId == addInput.serialId)) { + Left(DuplicateSerialId(params.channelId, addInput.serialId)) + } else if (session.localInputs.exists(i => spendSameOutpoint(i, addInput)) || session.remoteInputs.exists(i => spendSameOutpoint(i, addInput))) { + Left(DuplicateInput(params.channelId, addInput.serialId, addInput.previousTx.txid, addInput.previousTxOutput)) + } else if (addInput.previousTx.txOut.length <= addInput.previousTxOutput) { + Left(InputOutOfBounds(params.channelId, addInput.serialId, addInput.previousTx.txid, addInput.previousTxOutput)) + } else if (!Script.isNativeWitnessScript(addInput.previousTx.txOut(addInput.previousTxOutput.toInt).publicKeyScript)) { + Left(NonSegwitInput(params.channelId, addInput.serialId, addInput.previousTx.txid, addInput.previousTxOutput)) + } else { + val next = session.copy( + remoteInputs = session.remoteInputs :+ addInput, + inputsReceivedCount = session.inputsReceivedCount + 1, + txCompleteReceived = false, + ) + Right(send(next, params)) + } + case addOutput: TxAddOutput => + if (session.outputsReceivedCount + 1 >= MAX_INPUTS_OUTPUTS_RECEIVED) { + Left(TooManyInteractiveTxRounds(params.channelId)) + } else if (session.remoteOutputs.exists(_.serialId == addOutput.serialId)) { + Left(DuplicateSerialId(params.channelId, addOutput.serialId)) + } else if (addOutput.amount < params.dustLimit) { + Left(OutputBelowDust(params.channelId, addOutput.serialId, addOutput.amount, params.dustLimit)) + } else if (!Script.isNativeWitnessScript(addOutput.pubkeyScript)) { + Left(NonSegwitOutput(params.channelId, addOutput.serialId)) + } else { + val next = session.copy( + remoteOutputs = session.remoteOutputs :+ addOutput, + outputsReceivedCount = session.outputsReceivedCount + 1, + txCompleteReceived = false, + ) + Right(send(next, params)) + } + case removeInput: TxRemoveInput => + session.remoteInputs.find(_.serialId == removeInput.serialId) match { + case Some(_) => + val next = session.copy( + remoteInputs = session.remoteInputs.filterNot(_.serialId == removeInput.serialId), + txCompleteReceived = false, + ) + Right(send(next, params)) + case None => + Left(UnknownSerialId(params.channelId, removeInput.serialId)) + } + case removeOutput: TxRemoveOutput => + session.remoteOutputs.find(_.serialId == removeOutput.serialId) match { + case Some(_) => + val next = session.copy( + remoteOutputs = session.remoteOutputs.filterNot(_.serialId == removeOutput.serialId), + txCompleteReceived = false, + ) + Right(send(next, params)) + case None => + Left(UnknownSerialId(params.channelId, removeOutput.serialId)) + } + case _: TxComplete => + val next = session.copy(txCompleteReceived = true) + if (next.isComplete) { + Right(next, None) + } else { + Right(send(next, params)) + } + } + } + + def validateTx(session: InteractiveTxSession, params: InteractiveTxParams)(implicit log: LoggingAdapter): Either[ChannelException, (SharedTransaction, Int)] = { + val sharedTx = SharedTransaction(session.localInputs, session.remoteInputs, session.localOutputs, session.remoteOutputs, params.lockTime) + val tx = sharedTx.buildUnsignedTx() + + if (!session.isComplete) { + log.warning("invalid interactive tx: session isn't complete ({})", session) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + if (tx.txIn.length > 252 || tx.txOut.length > 252) { + log.warning("invalid interactive tx ({} inputs and {} outputs)", tx.txIn.length, tx.txOut.length) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + val sharedOutputs = tx.txOut.zipWithIndex.filter(_._1.publicKeyScript == params.fundingPubkeyScript) + if (sharedOutputs.length != 1) { + log.warning("invalid interactive tx: funding outpoint not included (tx={})", tx) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + val (sharedOutput, sharedOutputIndex) = sharedOutputs.head + if (sharedOutput.amount != params.fundingAmount) { + log.warning("invalid interactive tx: invalid funding amount (expected={}, actual={})", params.fundingAmount, sharedOutput.amount) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + // NB: we have previously verified that the inputs exist in the previous transactions. + val localAmountIn = sharedTx.localInputs.map(i => i.previousTx.txOut(i.previousTxOutput.toInt).amount).sum + val localAmountOut = sharedTx.localOutputs.filter(_.pubkeyScript != params.fundingPubkeyScript).map(_.amount).sum + params.localAmount + val remoteAmountIn = sharedTx.remoteInputs.map(i => i.previousTx.txOut(i.previousTxOutput.toInt).amount).sum + val remoteAmountOut = sharedTx.remoteOutputs.filter(_.pubkeyScript != params.fundingPubkeyScript).map(_.amount).sum + params.remoteAmount + if (localAmountIn < localAmountOut || remoteAmountIn < remoteAmountOut) { + log.warning("invalid interactive tx: input amount is too small (localIn={}, localOut={}, remoteIn={}, remoteOut={})", localAmountIn, localAmountOut, remoteAmountIn, remoteAmountOut) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + // The transaction isn't signed yet, so we estimate its weight knowing that all inputs are using native segwit. + val minimumWitnessWeight = 110 // see Bolt 3 + val minimumWeight = tx.weight() + tx.txIn.length * minimumWitnessWeight + if (minimumWeight > Transactions.MAX_STANDARD_TX_WEIGHT) { + log.warning("invalid interactive tx: exceeds standard weight (weight={})", minimumWeight) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + val minimumFee = Transactions.weight2fee(params.targetFeerate, minimumWeight) + val fee = localAmountIn + remoteAmountIn - tx.txOut.map(_.amount).sum + if (fee < minimumFee) { + log.warning("invalid interactive tx: below the target feerate (target={}, actual={})", params.targetFeerate, Transactions.fee2rate(fee, minimumWeight)) + return Left(InvalidCompleteInteractiveTx(params.channelId)) + } + + Right(sharedTx, sharedOutputIndex) + } + + def rollback(session: InteractiveTxSession, wallet: OnChainChannelFunder)(implicit ec: ExecutionContext): Future[Boolean] = { + val inputs = (session.localInputs ++ session.toSend.collect { case Left(addInput) => addInput }).map(i => TxIn(toOutPoint(i), ByteVector.empty, i.sequence)) + val outputs = (session.localOutputs ++ session.toSend.collect { case Right(addOutput) => addOutput }).map(o => TxOut(o.amount, o.pubkeyScript)) + val dummyTx = Transaction(2, inputs, outputs, 0) + wallet.rollback(dummyTx) + } + + private def spendSameOutpoint(input1: TxAddInput, input2: TxAddInput): Boolean = { + input1.previousTx.txid == input2.previousTx.txid && input1.previousTxOutput == input2.previousTxOutput + } + + def toOutPoint(input: TxAddInput): OutPoint = OutPoint(input.previousTx, input.previousTxOutput.toInt) + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxFunder.scala new file mode 100644 index 0000000000..edbb321ed7 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxFunder.scala @@ -0,0 +1,200 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel + +import akka.actor.typed.scaladsl.{ActorContext, Behaviors} +import akka.actor.typed.{ActorRef, Behavior} +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair.blockchain.OnChainChannelFunder +import fr.acinq.eclair.channel.InteractiveTx.{FundingContributions, InteractiveTxParams, toOutPoint} +import fr.acinq.eclair.wire.protocol.{TxAddInput, TxAddOutput} +import fr.acinq.eclair.{Logs, UInt64, randomBytes} +import scodec.bits.{ByteVector, HexStringSyntax} + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} + +/** + * Created by t-bast on 02/05/2022. + */ + +/** + * This actor adds wallet funds to an interactive-tx session. + * Some inputs cannot be used in this protocol: this actor filters them out and retries funding until it finds a set of + * inputs that the remote peer will accept. + */ +object InteractiveTxFunder { + + // @formatter:off + sealed trait Command + case class Fund(replyTo: ActorRef[Response], previousInputs: Seq[TxAddInput]) extends Command + private case class FundTransactionResult(tx: Transaction) extends Command + private case class InputDetails(usableInputs: Seq[TxAddInput], unusableInputs: Set[OutPoint]) extends Command + private case class WalletFailure(t: Throwable) extends Command + private case object UtxosUnlocked extends Command + + sealed trait Response + case class FundingSucceeded(contributions: FundingContributions) extends Response + case class FundingFailed(t: Throwable) extends Response + // @formatter:on + + def apply(remoteNodeId: PublicKey, params: InteractiveTxParams, wallet: OnChainChannelFunder): Behavior[Command] = { + Behaviors.setup { context => + Behaviors.withMdc(Logs.mdc(remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(params.channelId))) { + Behaviors.receiveMessagePartial { + case Fund(replyTo, previousInputs) => new InteractiveTxFunder(replyTo, params, wallet, context).start(previousInputs) + } + } + } + } + +} + +private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response], + params: InteractiveTxParams, + wallet: OnChainChannelFunder, + context: ActorContext[InteractiveTxFunder.Command])(implicit ec: ExecutionContext = ExecutionContext.Implicits.global) { + + import InteractiveTxFunder._ + + private val log = context.log + + def start(previousInputs: Seq[TxAddInput]): Behavior[Command] = { + val toFund = if (params.isInitiator) { + // If we're the initiator, we need to pay the fees of the common fields of the transaction, even if we don't want + // to contribute to the shared output. + params.localAmount.max(params.dustLimit) + } else { + params.localAmount + } + log.debug("contributing {} to interactive-tx construction", toFund) + if (toFund <= 0.sat) { + // We're not the initiator and we don't want to contribute to the funding transaction. + replyTo ! FundingSucceeded(FundingContributions(Nil, Nil)) + Behaviors.stopped + } else { + // We always double-spend all our previous inputs. + val inputs = previousInputs.map(i => TxIn(toOutPoint(i), ByteVector.empty, i.sequence)) + val dummyTx = Transaction(2, inputs, Seq(TxOut(toFund, params.fundingPubkeyScript)), params.lockTime) + fund(dummyTx, previousInputs, Set.empty) + } + } + + def fund(txNotFunded: Transaction, previousInputs: Seq[TxAddInput], unusableInputs: Set[OutPoint]): Behavior[Command] = { + context.pipeToSelf(wallet.fundTransaction(txNotFunded, params.targetFeerate, replaceable = true, lockUtxos = true)) { + case Failure(t) => WalletFailure(t) + case Success(result) => FundTransactionResult(result.tx) + } + Behaviors.receiveMessagePartial { + case FundTransactionResult(fundedTx) => + filterInputs(fundedTx, previousInputs, unusableInputs) + case WalletFailure(t) => + replyTo ! FundingFailed(t) + val toUnlock = previousInputs.map(i => toOutPoint(i)).toSet ++ unusableInputs + unlockAndStop(toUnlock) + } + } + + def filterInputs(fundedTx: Transaction, previousInputs: Seq[TxAddInput], unusableInputs: Set[OutPoint]): Behavior[Command] = { + context.pipeToSelf(Future.sequence(fundedTx.txIn.map(txIn => getInputDetails(txIn, previousInputs)))) { + case Failure(t) => WalletFailure(t) + case Success(results) => InputDetails(results.collect { case Right(i) => i }, results.collect { case Left(i) => i }.toSet) + } + Behaviors.receiveMessagePartial { + case inputDetails: InputDetails => + if (inputDetails.unusableInputs.isEmpty) { + // This funding iteration did not add any unusable inputs, so we can directly return the results. + val changeOutputs = fundedTx.txOut + .filter(_.publicKeyScript != params.fundingPubkeyScript) + .map(txOut => TxAddOutput(params.channelId, generateSerialId(), txOut.amount, txOut.publicKeyScript)) + val outputs = if (params.isInitiator) { + // If the initiator doesn't want to contribute, we should cancel out the dust amount artificially added previously. + val initiatorChangeOutputs = if (params.localAmount == 0.sat) { + changeOutputs.map(o => o.copy(amount = o.amount + params.dustLimit)) + } else { + changeOutputs + } + // The initiator is responsible for adding the shared output. + TxAddOutput(params.channelId, generateSerialId(), params.fundingAmount, params.fundingPubkeyScript) +: initiatorChangeOutputs + } else { + // The protocol only requires the non-initiator to pay the fees for its inputs and outputs, discounting the + // common fields (shared output, version, nLockTime, etc). However, this is really hard to compute here, + // because we don't know the witness size of our inputs (we let bitcoind handle that). For simplicity's sake, + // we simply accept that we'll slightly overpay the fee (which speeds up channel confirmation). + changeOutputs + } + log.info("added {} inputs and {} outputs to interactive tx", inputDetails.usableInputs.length, outputs.length) + replyTo ! FundingSucceeded(FundingContributions(inputDetails.usableInputs, outputs)) + // We unlock the unusable inputs from previous iterations (if any) as they can be used outside of this protocol. + unlockAndStop(unusableInputs) + } else { + // Some wallet inputs are unusable, so we must fund again to obtain usable inputs instead. + log.info("retrying funding as some utxos cannot be used for interactive-tx construction: {}", inputDetails.unusableInputs.map(o => s"${o.txid}:${o.index}").mkString(",")) + val sanitizedTx = fundedTx.copy( + txIn = fundedTx.txIn.filter(txIn => !inputDetails.unusableInputs.contains(txIn.outPoint)), + // We remove the change output added by this funding iteration. + txOut = fundedTx.txOut.filter(txOut => txOut.publicKeyScript == params.fundingPubkeyScript), + ) + fund(sanitizedTx, inputDetails.usableInputs, unusableInputs ++ inputDetails.unusableInputs) + } + case WalletFailure(t) => + replyTo ! FundingFailed(t) + val toUnlock = fundedTx.txIn.map(_.outPoint).toSet ++ unusableInputs + unlockAndStop(toUnlock) + } + } + + def unlockAndStop(toUnlock: Set[OutPoint]): Behavior[Command] = { + if (toUnlock.isEmpty) { + context.self ! UtxosUnlocked + } else { + val dummyTx = Transaction(2, toUnlock.toSeq.map(o => TxIn(o, Nil, 0)), Nil, 0) + context.pipeToSelf(wallet.rollback(dummyTx))(_ => UtxosUnlocked) + } + Behaviors.receiveMessagePartial { + case UtxosUnlocked => Behaviors.stopped + } + } + + private def getInputDetails(txIn: TxIn, previousInputs: Seq[TxAddInput]): Future[Either[OutPoint, TxAddInput]] = { + previousInputs.find(i => txIn.outPoint == toOutPoint(i)) match { + case Some(previousInput) => Future.successful(Right(previousInput)) + case None => wallet.getTransaction(txIn.outPoint.txid).map(previousTx => { + if (Transaction.write(previousTx).length > 65000) { + // Wallet input transaction is too big to fit inside tx_add_input. + Left(txIn.outPoint) + } else if (!Script.isNativeWitnessScript(previousTx.txOut(txIn.outPoint.index.toInt).publicKeyScript)) { + // Wallet input must be a native segwit input. + Left(txIn.outPoint) + } else { + Right(TxAddInput(params.channelId, generateSerialId(), previousTx, txIn.outPoint.index, txIn.sequence)) + } + }) + } + } + + private def generateSerialId(): UInt64 = { + // The initiator must use even values and the non-initiator odd values. + if (params.isInitiator) { + UInt64(randomBytes(8) & hex"fffffffffffffffe") + } else { + UInt64(randomBytes(8) | hex"0000000000000001") + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 692c795fa6..d974d66687 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -16,13 +16,17 @@ package fr.acinq.eclair.channel.fsm +import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} -import fr.acinq.eclair.Features +import fr.acinq.eclair.channel.Helpers.Funding +import fr.acinq.eclair.channel.InteractiveTx.InteractiveTxParams import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, AcceptDualFundedChannelTlv, ChannelTlv, Error, OpenDualFundedChannel, TlvStream} +import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{Features, MilliSatoshiLong} /** * Created by t-bast on 19/04/2022. @@ -40,7 +44,7 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL | | | accept_channel2 | |<--------------------------------| - WAIT_FOR_DUAL_FUNDING_COMPLETE | | WAIT_FOR_DUAL_FUNDING_COMPLETE + WAIT_FOR_DUAL_FUNDING_CREATED | | WAIT_FOR_DUAL_FUNDING_CREATED | | | . | | . | @@ -58,6 +62,34 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { |<--------------------------------| | tx_signatures | |-------------------------------->| + WAIT_FOR_DUAL_FUNDING_CONFIRMED | | WAIT_FOR_DUAL_FUNDING_CONFIRMED + | tx_init_rbf | + |-------------------------------->| + | tx_ack_rbf | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_RBF_CREATED | | WAIT_FOR_DUAL_FUNDING_RBF_CREATED + | | + | . | + | . | + | . | + | tx_complete | + |-------------------------------->| + | tx_complete | + |<--------------------------------| + WAIT_FOR_DUAL_FUNDING_RBF_SIGNED | | WAIT_FOR_DUAL_FUNDING_RBF_SIGNED + | commitment_signed | + |-------------------------------->| + | commitment_signed | + |<--------------------------------| + | tx_signatures | + |<--------------------------------| + | tx_signatures | + |-------------------------------->| + WAIT_FOR_DUAL_FUNDING_CONFIRMED | | WAIT_FOR_DUAL_FUNDING_CONFIRMED + | | + | . | + | . | + | . | WAIT_FOR_DUAL_FUNDING_LOCKED | | WAIT_FOR_DUAL_FUNDING_LOCKED | funding_locked funding_locked | |---------------- ---------------| @@ -115,11 +147,18 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { initFeatures = remoteInit.features, shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) + // We've exchanged open_channel2 and accept_channel2, we now know the final channelId. val channelId = Helpers.computeChannelId(open, accept) peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages txPublisher ! SetChannelId(remoteNodeId, channelId) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId)) - goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) sending accept + // We start the interactive-tx funding protocol. + val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) + val fundingParams = InteractiveTxParams(channelId, localParams.isInitiator, accept.fundingAmount, open.fundingAmount, fundingPubkeyScript, open.lockTime, open.dustLimit.max(accept.dustLimit), open.fundingFeerate) + val fundingActor = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, wallet)) + fundingActor ! InteractiveTxFunder.Fund(self, Nil) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, fundingParams, open.commitmentFeerate, open.firstPerCommitmentPoint, open.channelFlags, d.init.channelConfig, channelFeatures, None) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) @@ -137,6 +176,7 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { channelOpenReplyToUser(Left(LocalError(t))) handleLocalError(t, d, Some(accept)) case Right((channelFeatures, remoteShutdownScript)) => + // We've exchanged open_channel2 and accept_channel2, we now know the final channelId. val channelId = Helpers.computeChannelId(d.lastSent, accept) peer ! ChannelIdAssigned(self, remoteNodeId, accept.temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages txPublisher ! SetChannelId(remoteNodeId, channelId) @@ -157,9 +197,13 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { initFeatures = remoteInit.features, shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) + // We start the interactive-tx funding protocol. val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) - goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, channelFeatures) + val fundingParams = InteractiveTxParams(channelId, localParams.isInitiator, d.lastSent.fundingAmount, accept.fundingAmount, fundingPubkeyScript, d.lastSent.lockTime, d.lastSent.dustLimit.max(accept.dustLimit), d.lastSent.fundingFeerate) + val fundingActor = context.spawnAnonymous(InteractiveTxFunder(remoteNodeId, fundingParams, wallet)) + fundingActor ! InteractiveTxFunder.Fund(self, Nil) + goto(WAIT_FOR_DUAL_FUNDING_INTERNAL) using DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL(channelId, localParams, remoteParams, fundingParams, d.lastSent.commitmentFeerate, accept.firstPerCommitmentPoint, d.lastSent.channelFlags, d.init.channelConfig, channelFeatures, None) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL) => @@ -180,6 +224,24 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_DUAL_FUNDING_INTERNAL)(handleExceptions { + case Event(InteractiveTxFunder.FundingSucceeded(localContributions), d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + val (txSession, msg_opt) = InteractiveTx.start(d.fundingParams, localContributions) + d.remoteMessage.foreach(self ! _) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CREATED(d.channelId, d.localParams, d.remoteParams, d.fundingParams, txSession, d.commitTxFeerate, d.remoteFirstPerCommitmentPoint, d.channelFlags, d.channelConfig, d.channelFeatures) + msg_opt match { + case Some(msg) => goto(WAIT_FOR_DUAL_FUNDING_CREATED) using nextData sending msg + case None => goto(WAIT_FOR_DUAL_FUNDING_CREATED) using nextData + } + + case Event(InteractiveTxFunder.FundingFailed(t), d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + log.error(t, s"could not fund dual-funded channel: ") + channelOpenReplyToUser(Left(LocalError(t))) + handleLocalError(ChannelFundingError(d.channelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(msg: InteractiveTxMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => + // When we're not the initiator, we may receive their first interactive-tx message while we're funding our contribution. + stay() using d.copy(remoteMessage = Some(msg)) + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) handleFastClose(c, d.channelId) @@ -197,4 +259,82 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { goto(CLOSED) }) + when(WAIT_FOR_DUAL_FUNDING_CREATED)(handleExceptions { + case Event(msg: InteractiveTxMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + msg match { + case msg: InteractiveTxConstructionMessage => + InteractiveTx.receive(d.txSession, d.fundingParams, msg) match { + case Right((txSession1, outgoingMsg_opt)) => + if (txSession1.isComplete) { + InteractiveTx.validateTx(txSession1, d.fundingParams) match { + case Right((completeTx, fundingOutputIndex)) => + val fundingTx = completeTx.buildUnsignedTx() + Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.fundingAmount, 0 msat, d.commitTxFeerate, fundingTx.hash, fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { + case Right((_, localCommitTx, _, remoteCommitTx)) => + require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(d.localParams.fundingKeyPath), TxOwner.Remote, d.channelFeatures.commitmentFormat) + val commitSig = CommitSig(d.channelId, localSigOfRemoteTx, Nil) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelId, d.localParams, d.remoteParams, d.fundingParams, d.commitTxFeerate, d.remoteFirstPerCommitmentPoint, d.channelFlags, d.channelConfig, d.channelFeatures) + goto(WAIT_FOR_DUAL_FUNDING_SIGNED) using nextData sending Seq(outgoingMsg_opt, Some(commitSig)).flatten + case Left(cause) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(cause, d, None) + } + case Left(cause) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(cause, d, Some(msg)) + } + } else { + stay() using d.copy(txSession = txSession1) sending outgoingMsg_opt + } + case Left(cause) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(cause, d, Some(msg)) + } + case _: TxAbort => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(DualFundingAborted(d.channelId)))) + handleLocalError(DualFundingAborted(d.channelId), d, Some(msg)) + case _: TxSignatures => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(InvalidFundingSignature(d.channelId, None)))) + handleLocalError(InvalidFundingSignature(d.channelId, None), d, Some(msg)) + case _: TxInitRbf => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(InvalidRbfAttempt(d.channelId)))) + handleLocalError(InvalidRbfAttempt(d.channelId), d, Some(msg)) + case _: TxAckRbf => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(InvalidRbfAttempt(d.channelId)))) + handleLocalError(InvalidRbfAttempt(d.channelId), d, Some(msg)) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + InteractiveTx.rollback(d.txSession, wallet) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_DUAL_FUNDING_SIGNED)(handleExceptions { + case Event(msg, d) => ??? + }) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 3fd98cd9a3..5feb253b6d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -70,6 +70,11 @@ trait CommonHandlers { state } + def sending(msg_opt: Option[LightningMessage]): FSM.State[ChannelState, ChannelData] = { + msg_opt.foreach(msg => send(msg)) + state + } + /** * This method allows performing actions during the transition, e.g. after a call to [[MyState.storing]]. This is * particularly useful to publish transactions only after we are sure that the state has been persisted. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 3256c731f7..1deb5395ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -37,6 +37,8 @@ import scala.util.Try */ object Transactions { + val MAX_STANDARD_TX_WEIGHT = 400_000 + sealed trait CommitmentFormat { // @formatter:off def commitWeight: Int diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index ac7683140a..65ec641c98 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -38,6 +38,7 @@ sealed trait LightningMessage extends Serializable sealed trait SetupMessage extends LightningMessage sealed trait ChannelMessage extends LightningMessage sealed trait InteractiveTxMessage extends LightningMessage +sealed trait InteractiveTxConstructionMessage extends InteractiveTxMessage // <- not in the spec sealed trait HtlcMessage extends LightningMessage sealed trait RoutingMessage extends LightningMessage sealed trait AnnouncementMessage extends RoutingMessage // <- not in the spec @@ -45,6 +46,7 @@ sealed trait HasTimestamp extends LightningMessage { def timestamp: TimestampSec sealed trait HasTemporaryChannelId extends LightningMessage { def temporaryChannelId: ByteVector32 } // <- not in the spec sealed trait HasChannelId extends LightningMessage { def channelId: ByteVector32 } // <- not in the spec sealed trait HasChainHash extends LightningMessage { def chainHash: ByteVector32 } // <- not in the spec +sealed trait HasSerialId extends LightningMessage { def serialId: UInt64 } // <- not in the spec sealed trait UpdateMessage extends HtlcMessage // <- not in the spec sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <- not in the spec // @formatter:on @@ -85,24 +87,24 @@ case class TxAddInput(channelId: ByteVector32, previousTx: Transaction, previousTxOutput: Long, sequence: Long, - tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + tlvStream: TlvStream[TxAddInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxAddOutput(channelId: ByteVector32, serialId: UInt64, amount: Satoshi, pubkeyScript: ByteVector, - tlvStream: TlvStream[TxAddOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + tlvStream: TlvStream[TxAddOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxRemoveInput(channelId: ByteVector32, serialId: UInt64, - tlvStream: TlvStream[TxRemoveInputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + tlvStream: TlvStream[TxRemoveInputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxRemoveOutput(channelId: ByteVector32, serialId: UInt64, - tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + tlvStream: TlvStream[TxRemoveOutputTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId with HasSerialId case class TxComplete(channelId: ByteVector32, - tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId + tlvStream: TlvStream[TxCompleteTlv] = TlvStream.empty) extends InteractiveTxConstructionMessage with HasChannelId case class TxSignatures(channelId: ByteVector32, txId: ByteVector32, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 9cbebadc3a..93bdfaf3e4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -16,10 +16,10 @@ package fr.acinq.eclair.blockchain +import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} -import fr.acinq.bitcoin.TxIn.SEQUENCE_FINAL -import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits._ @@ -40,11 +40,17 @@ class DummyOnChainWallet extends OnChainWallet { override def getReceivePubkey(receiveAddress: Option[String] = None)(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = ??? + + override def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ??? + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Future.successful(DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount)) override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) + override def getTransaction(txId: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = ??? + override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = { rolledback = rolledback + tx Future.successful(true) @@ -58,17 +64,28 @@ class NoOpOnChainWallet extends OnChainWallet { import DummyOnChainWallet._ + var rolledback = Set.empty[Transaction] + override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) override def getReceiveAddress(label: String)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) override def getReceivePubkey(receiveAddress: Option[String] = None)(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise[MakeFundingTxResponse]().future // will never be completed + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed + + override def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = Promise().future // will never be completed + + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) - override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) + override def getTransaction(txId: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = Promise().future // will never be completed + + override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = { + rolledback = rolledback + tx + Future.successful(true) + } override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false) @@ -80,10 +97,12 @@ object DummyOnChainWallet { val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { - val fundingTx = Transaction(version = 2, + val fundingTx = Transaction( + version = 2, txIn = TxIn(OutPoint(ByteVector32(ByteVector.fill(32)(1)), 42), signatureScript = Nil, sequence = SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, - lockTime = 0) + lockTime = 0 + ) MakeFundingTxResponse(fundingTx, 0, 420 sat) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 1a664fb395..1170027ed7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -22,7 +22,7 @@ import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, MilliBtcDouble, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxIn, TxOut} import fr.acinq.bitcoin -import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, OnChainBalance} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinJsonRPCAuthMethod.UserPassword diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index d51266e49c..9a03d3caf0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -173,8 +173,11 @@ trait BitcoindService extends Logging { new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(walletName)) } - def getNewAddress(sender: TestProbe = TestProbe(), rpcClient: BitcoinJsonRPCClient = bitcoinrpcclient): String = { - rpcClient.invoke("getnewaddress").pipeTo(sender.ref) + def getNewAddress(sender: TestProbe = TestProbe(), rpcClient: BitcoinJsonRPCClient = bitcoinrpcclient, addressType_opt: Option[String] = None): String = { + addressType_opt match { + case Some(addressType) => rpcClient.invoke("getnewaddress", "", addressType).pipeTo(sender.ref) + case None => rpcClient.invoke("getnewaddress").pipeTo(sender.ref) + } val JString(address) = sender.expectMsgType[JValue] address } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index ff42206b52..92f50d1d88 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -22,10 +22,11 @@ import akka.actor.{ActorRef, Props, typed} import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, SignTransactionResponse} import fr.acinq.eclair.blockchain.WatcherSpec._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{FundTransactionOptions, FundTransactionResponse, SignTransactionResponse} +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.FundTransactionOptions import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NewTransaction} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala new file mode 100644 index 0000000000..91aa57b24a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala @@ -0,0 +1,248 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel + +import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} +import akka.pattern.pipe +import akka.testkit.TestProbe +import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, SignTransactionResponse} +import fr.acinq.eclair.blockchain.bitcoind.BitcoindService +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient +import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{MempoolTx, Utxo} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.InteractiveTx.{InteractiveTxParams, SharedTransaction, toOutPoint} +import fr.acinq.eclair.channel.InteractiveTxFunder.{Fund, FundingFailed, FundingSucceeded} +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.{TestKitBaseClass, randomBytes32, randomKey} +import org.scalatest.BeforeAndAfterAll +import org.scalatest.funsuite.AnyFunSuiteLike + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt + +class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with AnyFunSuiteLike with BeforeAndAfterAll { + + import InteractiveTxSpec._ + + override def beforeAll(): Unit = { + startBitcoind() + waitForBitcoindReady() + } + + override def afterAll(): Unit = { + stopBitcoind() + } + + private def addUtxo(wallet: BitcoinCoreClient, amount: Satoshi, probe: TestProbe): Unit = { + wallet.getReceiveAddress().pipeTo(probe.ref) + val walletAddress = probe.expectMsgType[String] + sendToAddress(walletAddress, amount, probe) + } + + test("fund transaction") { + // Initialize wallets with a few confirmed utxos. + val probe = TestProbe() + val initiatorRpcClient = createWallet("basic-funding-initiator") + val initiatorWallet = new BitcoinCoreClient(initiatorRpcClient) + val nonInitiatorRpcClient = createWallet("basic-funding-non-initiator") + val nonInitiatorWallet = new BitcoinCoreClient(nonInitiatorRpcClient) + Seq(50_000 sat, 35_000 sat, 60_000 sat).foreach(amount => addUtxo(initiatorWallet, amount, probe)) + Seq(100_000 sat, 75_000 sat).foreach(amount => addUtxo(nonInitiatorWallet, amount, probe)) + generateBlocks(1) + + // Each participant funds part of the shared transaction. + val channelId = randomBytes32() + val sharedOutputScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) + val lockTime = 42 + val targetFeerate = FeeratePerKw(5000 sat) + val initiatorParams = InteractiveTxParams(channelId, isInitiator = true, 120_000 sat, 40_000 sat, sharedOutputScript, lockTime, 660 sat, targetFeerate) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, initiatorParams, initiatorWallet)) ! Fund(probe.ref, Nil) + val initiatorContributions = probe.expectMsgType[FundingSucceeded].contributions + val nonInitiatorParams = InteractiveTxParams(channelId, isInitiator = false, 40_000 sat, 120_000 sat, sharedOutputScript, lockTime, 660 sat, targetFeerate) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, nonInitiatorParams, nonInitiatorWallet)) ! Fund(probe.ref, Nil) + val nonInitiatorContributions = probe.expectMsgType[FundingSucceeded].contributions + + // The initiator is responsible for adding the shared output. + assert(initiatorContributions.inputs.length === 3) + assert(initiatorContributions.outputs.length === 2) + assert(initiatorContributions.outputs.count(_.pubkeyScript == sharedOutputScript) === 1) + assert(initiatorContributions.outputs.exists(o => o.pubkeyScript == sharedOutputScript && o.amount == 160_000.sat)) + assert(nonInitiatorContributions.inputs.length === 1) + assert(nonInitiatorContributions.outputs.length === 1) + assert(nonInitiatorContributions.outputs.count(_.pubkeyScript == sharedOutputScript) === 0) + + // Utxos are locked for the duration of the protocol + val initiatorLocks = getLocks(probe, initiatorRpcClient) + assert(initiatorLocks.size === 3) + assert(initiatorLocks === initiatorContributions.inputs.map(toOutPoint).toSet) + val nonInitiatorLocks = getLocks(probe, nonInitiatorRpcClient) + assert(nonInitiatorLocks.size === 1) + assert(nonInitiatorLocks === nonInitiatorContributions.inputs.map(toOutPoint).toSet) + + // The resulting transaction is valid and has the right feerate. + val sharedTx = SharedTransaction(initiatorContributions.inputs, nonInitiatorContributions.inputs, initiatorContributions.outputs, nonInitiatorContributions.outputs, initiatorParams.lockTime) + val unsignedTx = sharedTx.buildUnsignedTx() + initiatorWallet.signTransaction(unsignedTx, allowIncomplete = true).pipeTo(probe.ref) + val partiallySignedTx = probe.expectMsgType[SignTransactionResponse].tx + nonInitiatorWallet.signTransaction(partiallySignedTx).pipeTo(probe.ref) + val signedTx = probe.expectMsgType[SignTransactionResponse].tx + assert(signedTx.lockTime === lockTime) + initiatorWallet.publishTransaction(signedTx).pipeTo(probe.ref) + probe.expectMsg(signedTx.txid) + initiatorWallet.getMempoolTx(signedTx.txid).pipeTo(probe.ref) + val mempoolTx = probe.expectMsgType[MempoolTx] + assert(mempoolTx.fees === computeFees(sharedTx)) + val feerate = Transactions.fee2rate(mempoolTx.fees, signedTx.weight()) + assert(targetFeerate <= feerate && feerate <= targetFeerate * 1.25, s"unexpected feerate (target=$targetFeerate actual=$feerate)") + } + + test("fund transaction without contributing (initiator)") { + // Initialize wallet with a few confirmed utxos. + val probe = TestProbe() + val wallet = new BitcoinCoreClient(createWallet("non-contributing-initiator")) + addUtxo(wallet, 100_000 sat, probe) + generateBlocks(1) + + // Even though the initiator isn't contributing, they're paying the fees for the common parts of the transaction. + val params = InteractiveTxParams(randomBytes32(), isInitiator = true, 0 sat, 50_000 sat, Script.write(Script.pay2wpkh(randomKey().publicKey)), 0, 330 sat, FeeratePerKw(4000 sat)) + assert(params.fundingAmount === 50_000.sat) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params, wallet)) ! Fund(probe.ref, Nil) + val fundingContributions = probe.expectMsgType[FundingSucceeded].contributions + assert(fundingContributions.inputs.length === 1) + assert(fundingContributions.outputs.length === 2) + assert(fundingContributions.outputs.exists(o => o.pubkeyScript == params.fundingPubkeyScript && o.amount === params.fundingAmount)) + + // But the initiator doesn't pay the funding amount, that will be the non-initiator's responsibility. + val initiatorFees = computeFees(fundingContributions.inputs, fundingContributions.outputs) + params.fundingAmount + assert(initiatorFees > 0.sat) + val partialTx = SharedTransaction(fundingContributions.inputs, Nil, fundingContributions.outputs, Nil, params.lockTime).buildUnsignedTx() + wallet.signTransaction(partialTx, allowIncomplete = true).pipeTo(probe.ref) + val signedTx = probe.expectMsgType[SignTransactionResponse].tx + val feerate = Transactions.fee2rate(initiatorFees, signedTx.weight()) + assert(params.targetFeerate <= feerate && feerate <= params.targetFeerate * 1.25, s"unexpected feerate (target=${params.targetFeerate} actual=$feerate)") + } + + test("fund transaction without contributing (non-initiator)") { + // Initialize empty wallet. + val probe = TestProbe() + val wallet = new BitcoinCoreClient(createWallet("non-contributing-non-initiator")) + + // When the non-initiator isn't contributing, they don't need to do anything. + val params = InteractiveTxParams(randomBytes32(), isInitiator = false, 0 sat, 150_000 sat, Script.write(Script.pay2wpkh(randomKey().publicKey)), 0, 330 sat, FeeratePerKw(4000 sat)) + assert(params.fundingAmount === 150_000.sat) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params, wallet)) ! Fund(probe.ref, Nil) + val fundingContributions = probe.expectMsgType[FundingSucceeded].contributions + assert(fundingContributions.inputs.isEmpty) + assert(fundingContributions.outputs.isEmpty) + } + + test("fund transaction with previous inputs") { + // Initialize wallet with a few confirmed utxos. + val probe = TestProbe() + val wallet = new BitcoinCoreClient(createWallet("funding-previous-inputs")) + Seq(55_000 sat, 60_000 sat, 57_000 sat, 52_000 sat, 75_000 sat).foreach(amount => addUtxo(wallet, amount, probe)) + generateBlocks(1) + + // We fund the transaction a first time. + val feerate1 = FeeratePerKw(3000 sat) + val params1 = InteractiveTxParams(randomBytes32(), isInitiator = true, 100_000 sat, 50_000 sat, Script.write(Script.pay2wpkh(randomKey().publicKey)), 0, 330 sat, feerate1) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params1, wallet)) ! Fund(probe.ref, Nil) + val contributions1 = probe.expectMsgType[FundingSucceeded].contributions + assert(contributions1.inputs.length === 2) + assert(contributions1.outputs.length <= 2) + assert(contributions1.outputs.exists(o => o.pubkeyScript == params1.fundingPubkeyScript && o.amount == params1.fundingAmount)) + val fee1 = computeFees(contributions1.inputs, contributions1.outputs) + + // We fund if a second time, re-using the same inputs and adding new ones if necessary. + val feerate2 = FeeratePerKw(7500 sat) + val params2 = params1.copy(targetFeerate = feerate2) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params2, wallet)) ! Fund(probe.ref, contributions1.inputs) + val contributions2 = probe.expectMsgType[FundingSucceeded].contributions + contributions1.inputs.foreach(i => assert(contributions2.inputs.contains(i))) + assert(contributions2.outputs.length <= 2) + assert(contributions2.outputs.exists(o => o.pubkeyScript == params1.fundingPubkeyScript && o.amount == params1.fundingAmount)) + val fee2 = computeFees(contributions2.inputs, contributions2.outputs) + assert(fee2 > fee1) + } + + test("skip unusable utxos when funding transaction") { + // Initialize wallet with a few confirmed utxos, including some unusable utxos. + val probe = TestProbe() + val walletRpcClient = createWallet("funding-unusable-utxos") + val wallet = new BitcoinCoreClient(walletRpcClient) + Seq(75_000 sat, 60_000 sat).foreach(amount => addUtxo(wallet, amount, probe)) + // Dual funding disallows non-segwit inputs. + val legacyTxId = { + val legacyAddress = getNewAddress(probe, walletRpcClient, Some("legacy")) + sendToAddress(legacyAddress, 100_000 sat, probe).txid + } + // Dual funding cannot use transactions that exceed 65k bytes. + val bigTxId = { + wallet.getReceivePubkey().pipeTo(probe.ref) + val publicKey = probe.expectMsgType[PublicKey] + val tx = Transaction(2, Nil, TxOut(100_000 sat, Script.pay2wpkh(publicKey)) +: (1 to 2500).map(_ => TxOut(5000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) + val minerWallet = new BitcoinCoreClient(bitcoinrpcclient) + minerWallet.fundTransaction(tx, FeeratePerKw(500 sat), replaceable = true, lockUtxos = false).pipeTo(probe.ref) + val unsignedTx = probe.expectMsgType[FundTransactionResponse].tx + minerWallet.signTransaction(unsignedTx).pipeTo(probe.ref) + val signedTx = probe.expectMsgType[SignTransactionResponse].tx + assert(Transaction.write(signedTx).length >= 65_000) + minerWallet.publishTransaction(signedTx).pipeTo(probe.ref) + probe.expectMsgType[ByteVector32] + } + generateBlocks(1) + + // We verify that all utxos are correctly included in our wallet. + wallet.listUnspent().pipeTo(probe.ref) + val utxos = probe.expectMsgType[Seq[Utxo]] + assert(utxos.length === 4) + assert(utxos.exists(_.txid == bigTxId)) + assert(utxos.exists(_.txid == legacyTxId)) + + // We can't use some of our utxos, so we don't have enough to fund our channel. + val params = InteractiveTxParams(randomBytes32(), isInitiator = true, 140_000 sat, 0 sat, Script.write(Script.pay2wpkh(randomKey().publicKey)), 0, 660 sat, FeeratePerKw(5000 sat)) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params, wallet)) ! Fund(probe.ref, Nil) + val failure = probe.expectMsgType[FundingFailed] + assert(failure.t.getMessage.contains("Insufficient funds")) + // Utxos shouldn't be locked after a failure. + awaitCond(getLocks(probe, walletRpcClient).isEmpty, max = 10 seconds, interval = 100 millis) + + // We add more usable utxos to unblock funding. + Seq(80_000 sat, 50_000 sat).foreach(amount => addUtxo(wallet, amount, probe)) + generateBlocks(1) + system.spawnAnonymous(InteractiveTxFunder(randomKey().publicKey, params, wallet)) ! Fund(probe.ref, Nil) + val contributions = probe.expectMsgType[FundingSucceeded].contributions + assert(!contributions.inputs.exists(_.previousTx.txid == legacyTxId)) + assert(!contributions.inputs.exists(_.previousTx.txid == bigTxId)) + val locks = getLocks(probe, walletRpcClient) + assert(locks === contributions.inputs.map(toOutPoint).toSet) + + val sharedTx = SharedTransaction(contributions.inputs, Nil, contributions.outputs, Nil, params.lockTime) + wallet.signTransaction(sharedTx.buildUnsignedTx()).pipeTo(probe.ref) + val signedTx = probe.expectMsgType[SignTransactionResponse].tx + wallet.publishTransaction(signedTx).pipeTo(probe.ref) + probe.expectMsg(signedTx.txid) + wallet.getMempoolTx(signedTx.txid).pipeTo(probe.ref) + val mempoolTx = probe.expectMsgType[MempoolTx] + assert(mempoolTx.fees === computeFees(sharedTx)) + val feerate = Transactions.fee2rate(mempoolTx.fees, signedTx.weight()) + assert(params.targetFeerate <= feerate && feerate <= params.targetFeerate * 1.25, s"unexpected feerate (target=${params.targetFeerate} actual=$feerate)") + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala new file mode 100644 index 0000000000..37c5e29c55 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala @@ -0,0 +1,338 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel + +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.InteractiveTx._ +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{UInt64, randomBytes32, randomKey} +import org.scalatest.funsuite.AnyFunSuiteLike +import scodec.bits.ByteVector + +class InteractiveTxSpec extends AnyFunSuiteLike { + + import InteractiveTxSpec._ + + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + + test("initiator only") { + // +-------+ +-------+ + // | |--(1)- tx_add_input -->| | + // | |<-(2)- tx_complete ----| | + // | |--(3)- tx_add_input -->| | + // | A |<-(4)- tx_complete ----| B | + // | |--(5)- tx_add_output ->| | + // | |<-(6)- tx_complete ----| | + // | |--(7)- tx_complete --->| | + // +-------+ +-------+ + + val channelId = randomBytes32() + val fundingScript = createFundingScript() + val initiatorParams = InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 0 sat, fundingScript, 0, 660 sat, FeeratePerKw(2500 sat)) + val initiatorContributions = FundingContributions( + Seq(createInput(channelId, UInt64(0), 60_000 sat), createInput(channelId, UInt64(2), 45_000 sat)), + Seq(TxAddOutput(channelId, UInt64(0), initiatorParams.fundingAmount, initiatorParams.fundingPubkeyScript)), + ) + val nonInitiatorParams = InteractiveTxParams(channelId, isInitiator = false, 0 sat, 100_000 sat, fundingScript, 0, 660 sat, FeeratePerKw(2500 sat)) + val nonInitiatorContributions = FundingContributions(Nil, Nil) + + // A --- tx_add_input --> B + val (initiatorSession1, Some(msg1)) = InteractiveTx.start(initiatorParams, initiatorContributions) + assert(msg1 === initiatorContributions.inputs.head) + // A <--- tx_complete --- B + val (nonInitiatorSession1, None) = InteractiveTx.start(nonInitiatorParams, nonInitiatorContributions) + val Right((nonInitiatorSession2, Some(msg2))) = InteractiveTx.receive(nonInitiatorSession1, nonInitiatorParams, msg1) + assert(msg2 === TxComplete(channelId)) + // A --- tx_add_input --> B + val Right((initiatorSession2, Some(msg3))) = InteractiveTx.receive(initiatorSession1, initiatorParams, msg2) + assert(msg3 === initiatorContributions.inputs.last) + // A <--- tx_complete --- B + val Right((nonInitiatorSession3, Some(msg4))) = InteractiveTx.receive(nonInitiatorSession2, nonInitiatorParams, msg3) + assert(msg4 === TxComplete(channelId)) + // A --- tx_add_output --> B + val Right((initiatorSession3, Some(msg5))) = InteractiveTx.receive(initiatorSession2, initiatorParams, msg4) + assert(msg5 === initiatorContributions.outputs.head) + assert(!initiatorSession3.isComplete) + // A <--- tx_complete --- B + val Right((nonInitiatorSession4, Some(msg6))) = InteractiveTx.receive(nonInitiatorSession3, nonInitiatorParams, msg5) + assert(msg6 === TxComplete(channelId)) + assert(!nonInitiatorSession4.isComplete) + // A --- tx_complete ---> B + val Right((initiatorSession4, Some(msg7))) = InteractiveTx.receive(initiatorSession3, initiatorParams, msg6) + assert(msg7 === TxComplete(channelId)) + assert(initiatorSession4.isComplete) + val Right((nonInitiatorSession5, None)) = InteractiveTx.receive(nonInitiatorSession4, nonInitiatorParams, msg7) + assert(nonInitiatorSession5.isComplete) + + val Right((initiatorTx, initiatorIndex)) = InteractiveTx.validateTx(initiatorSession4, initiatorParams) + val Right((nonInitiatorTx, nonInitiatorIndex)) = InteractiveTx.validateTx(nonInitiatorSession5, nonInitiatorParams) + assert(initiatorIndex === nonInitiatorIndex) + assert(initiatorIndex === 0) + assert(initiatorTx.buildUnsignedTx() === nonInitiatorTx.buildUnsignedTx()) + val tx = initiatorTx.buildUnsignedTx() + assert(tx.txIn.length === 2) + assert(tx.txOut.length === 1) + assert(tx.txOut.head === TxOut(initiatorParams.fundingAmount, fundingScript)) + } + + test("initiator and non-initiator") { + // +-------+ +-------+ + // | |--(1)- tx_add_input -->| | + // | |<-(2)- tx_add_input ---| | + // | |--(3)- tx_add_input -->| | + // | A |<-(4)- tx_add_output --| B | + // | |--(5)- tx_add_output ->| | + // | |<-(6)- tx_complete ----| | + // | |--(7)- tx_add_output ->| | + // | |<-(8)- tx_complete ----| | + // | |--(9)- tx_complete --->| | + // +-------+ +-------+ + + val channelId = randomBytes32() + val fundingScript = createFundingScript() + val initiatorParams = InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 50_000 sat, fundingScript, 0, 660 sat, FeeratePerKw(2500 sat)) + val initiatorContributions = FundingContributions( + Seq(createInput(channelId, UInt64(0), 60_000 sat), createInput(channelId, UInt64(2), 45_000 sat)), + Seq(TxAddOutput(channelId, UInt64(0), initiatorParams.fundingAmount, initiatorParams.fundingPubkeyScript), TxAddOutput(channelId, UInt64(2), 2500 sat, createChangeScript())), + ) + val nonInitiatorParams = InteractiveTxParams(channelId, isInitiator = false, 50_000 sat, 100_000 sat, fundingScript, 0, 660 sat, FeeratePerKw(2_500 sat)) + val nonInitiatorContributions = FundingContributions( + Seq(createInput(channelId, UInt64(1), 58_000 sat)), + Seq(TxAddOutput(channelId, UInt64(1), 7_000 sat, createChangeScript())) + ) + + // A --- tx_add_input --> B + val (initiatorSession1, Some(msg1)) = InteractiveTx.start(initiatorParams, initiatorContributions) + assert(msg1 === initiatorContributions.inputs.head) + // A <-- tx_add_input --- B + val (nonInitiatorSession1, None) = InteractiveTx.start(nonInitiatorParams, nonInitiatorContributions) + val Right((nonInitiatorSession2, Some(msg2))) = InteractiveTx.receive(nonInitiatorSession1, nonInitiatorParams, msg1) + assert(msg2 === nonInitiatorContributions.inputs.head) + // A --- tx_add_input --> B + val Right((initiatorSession2, Some(msg3))) = InteractiveTx.receive(initiatorSession1, initiatorParams, msg2) + assert(msg3 === initiatorContributions.inputs.last) + // A <-- tx_add_output --- B + val Right((nonInitiatorSession3, Some(msg4))) = InteractiveTx.receive(nonInitiatorSession2, nonInitiatorParams, msg3) + assert(msg4 === nonInitiatorContributions.outputs.head) + // A --- tx_add_output --> B + val Right((initiatorSession3, Some(msg5))) = InteractiveTx.receive(initiatorSession2, initiatorParams, msg4) + assert(msg5 === initiatorContributions.outputs.head) + // A <-- tx_complete --- B + val Right((nonInitiatorSession4, Some(msg6))) = InteractiveTx.receive(nonInitiatorSession3, nonInitiatorParams, msg5) + assert(msg6 === TxComplete(channelId)) + // A --- tx_add_output --> B + val Right((initiatorSession4, Some(msg7))) = InteractiveTx.receive(initiatorSession3, initiatorParams, msg6) + assert(msg7 === initiatorContributions.outputs.last) + assert(!initiatorSession4.isComplete) + // A <-- tx_complete --- B + val Right((nonInitiatorSession5, Some(msg8))) = InteractiveTx.receive(nonInitiatorSession4, nonInitiatorParams, msg7) + assert(msg8 === TxComplete(channelId)) + assert(!nonInitiatorSession5.isComplete) + // A --- tx_complete --> B + val Right((initiatorSession5, Some(msg9))) = InteractiveTx.receive(initiatorSession4, initiatorParams, msg8) + assert(initiatorSession5.isComplete) + assert(msg9 === TxComplete(channelId)) + val Right((nonInitiatorSession6, None)) = InteractiveTx.receive(nonInitiatorSession5, nonInitiatorParams, msg9) + assert(nonInitiatorSession6.isComplete) + + val Right((initiatorTx, initiatorIndex)) = InteractiveTx.validateTx(initiatorSession5, initiatorParams) + val Right((nonInitiatorTx, nonInitiatorIndex)) = InteractiveTx.validateTx(nonInitiatorSession6, nonInitiatorParams) + assert(initiatorIndex === nonInitiatorIndex) + assert(initiatorIndex === 0) + assert(initiatorTx.buildUnsignedTx() === nonInitiatorTx.buildUnsignedTx()) + val tx = initiatorTx.buildUnsignedTx() + assert(tx.txIn.length === 3) + assert(tx.txOut.length === 3) + assert(tx.txOut.head === TxOut(initiatorParams.fundingAmount, fundingScript)) + } + + test("invalid input") { + val params = InteractiveTxParams(randomBytes32(), isInitiator = true, 0 sat, 100_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(2_500 sat)) + val mixedOutputs = Seq( + TxOut(2500 sat, Script.pay2wpkh(randomKey().publicKey)), + TxOut(2500 sat, Script.pay2pkh(randomKey().publicKey)), + ) + val mixedTx = Transaction(2, Nil, mixedOutputs, 0) + val session = { + val (session1, Some(msg)) = InteractiveTx.start(params, FundingContributions(Nil, Nil)) + assert(msg === TxComplete(params.channelId)) + assert(!session1.isComplete) + val Right((session2, _)) = InteractiveTx.receive(session1, params, TxAddInput(params.channelId, UInt64(7), mixedTx, 0, 0)) + session2 + } + assert(InteractiveTx.receive(session, params, createInput(params.channelId, UInt64(0), 15_000 sat)) === Left(InvalidSerialId(params.channelId, UInt64(0)))) + assert(InteractiveTx.receive(session, params, TxAddInput(params.channelId, UInt64(1), mixedTx, 2, 0)) === Left(InputOutOfBounds(params.channelId, UInt64(1), mixedTx.txid, 2))) + assert(InteractiveTx.receive(session, params, createInput(params.channelId, UInt64(7), 15_000 sat)) === Left(DuplicateSerialId(params.channelId, UInt64(7)))) + assert(InteractiveTx.receive(session, params, TxAddInput(params.channelId, UInt64(13), mixedTx, 0, 0)) === Left(DuplicateInput(params.channelId, UInt64(13), mixedTx.txid, 0))) + assert(InteractiveTx.receive(session, params, TxAddInput(params.channelId, UInt64(17), mixedTx, 1, 0)) === Left(NonSegwitInput(params.channelId, UInt64(17), mixedTx.txid, 1))) + } + + test("invalid output") { + val params = InteractiveTxParams(randomBytes32(), isInitiator = false, 0 sat, 100_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(2_500 sat)) + val session = { + val (session1, None) = InteractiveTx.start(params, FundingContributions(Nil, Nil)) + assert(!session1.isComplete) + val Right((session2, _)) = InteractiveTx.receive(session1, params, TxAddOutput(params.channelId, UInt64(4), 45_000 sat, createChangeScript())) + session2 + } + assert(InteractiveTx.receive(session, params, TxAddOutput(params.channelId, UInt64(3), 15_000 sat, createChangeScript())) === Left(InvalidSerialId(params.channelId, UInt64(3)))) + assert(InteractiveTx.receive(session, params, TxAddOutput(params.channelId, UInt64(4), 15_000 sat, createChangeScript())) === Left(DuplicateSerialId(params.channelId, UInt64(4)))) + assert(InteractiveTx.receive(session, params, TxAddOutput(params.channelId, UInt64(6), 659 sat, createChangeScript())) === Left(OutputBelowDust(params.channelId, UInt64(6), 659 sat, 660 sat))) + assert(InteractiveTx.receive(session, params, TxAddOutput(params.channelId, UInt64(8), 15_000 sat, Script.write(Script.pay2pkh(randomKey().publicKey)))) === Left(NonSegwitOutput(params.channelId, UInt64(8)))) + } + + test("too many protocol rounds") { + val params = InteractiveTxParams(randomBytes32(), isInitiator = false, 0 sat, 100_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(2_500 sat)) + var session = InteractiveTx.start(params, FundingContributions(Nil, Nil))._1 + (1 until InteractiveTx.MAX_INPUTS_OUTPUTS_RECEIVED).foreach(i => { + val Right((nextSession, _)) = InteractiveTx.receive(session, params, createInput(params.channelId, UInt64(2 * i), 2500 sat)) + session = nextSession + }) + val Left(f) = InteractiveTx.receive(session, params, createInput(params.channelId, UInt64(15000), 1561 sat)) + assert(f === TooManyInteractiveTxRounds(params.channelId)) + } + + test("remove input/output") { + val params = InteractiveTxParams(randomBytes32(), isInitiator = false, 0 sat, 100_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(2_500 sat)) + var session = InteractiveTx.start(params, FundingContributions(Nil, Nil))._1 + // A --- tx_add_input --> B + val input = createInput(params.channelId, UInt64(0), 150_000 sat) + session = InteractiveTx.receive(session, params, input).toOption.get._1 + // A --- tx_add_input --> B + session = InteractiveTx.receive(session, params, createInput(params.channelId, UInt64(2), 10_000 sat)).toOption.get._1 + // A --- tx_add_output --> B + session = InteractiveTx.receive(session, params, TxAddOutput(params.channelId, UInt64(0), 25_000 sat, createChangeScript())).toOption.get._1 + // A --- tx_add_output --> B + val output = TxAddOutput(params.channelId, UInt64(4), params.fundingAmount, params.fundingPubkeyScript) + session = InteractiveTx.receive(session, params, output).toOption.get._1 + assert(InteractiveTx.receive(session, params, TxRemoveInput(params.channelId, UInt64(4))) === Left(UnknownSerialId(params.channelId, UInt64(4)))) + assert(InteractiveTx.receive(session, params, TxRemoveOutput(params.channelId, UInt64(2))) === Left(UnknownSerialId(params.channelId, UInt64(2)))) + // A --- tx_remove_input --> B + session = InteractiveTx.receive(session, params, TxRemoveInput(params.channelId, UInt64(2))).toOption.get._1 + // A --- tx_remove_output --> B + session = InteractiveTx.receive(session, params, TxRemoveOutput(params.channelId, UInt64(0))).toOption.get._1 + // A --- tx_complete --> B + session = InteractiveTx.receive(session, params, TxComplete(params.channelId)).toOption.get._1 + assert(session.isComplete) + val tx = InteractiveTx.validateTx(session, params).toOption.get._1.buildUnsignedTx() + assert(tx.txIn.length === 1) + assert(tx.txOut.length === 1) + assert(tx.txIn.head.outPoint === toOutPoint(input)) + assert(tx.txOut.head === TxOut(output.amount, output.pubkeyScript)) + } + + test("validate transaction") { + val params = InteractiveTxParams(randomBytes32(), isInitiator = true, 100_000 sat, 50_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(5000 sat)) + val validSession = InteractiveTxSession( + toSend = Nil, + localInputs = Seq(createInput(params.channelId, UInt64(0), 150_000 sat)), + remoteInputs = Seq(createInput(params.channelId, UInt64(1), 75_000 sat)), + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript), TxAddOutput(params.channelId, UInt64(2), 40_000 sat, createChangeScript())), + remoteOutputs = Seq(TxAddOutput(params.channelId, UInt64(1), 20_000 sat, createChangeScript())), + txCompleteSent = true, + txCompleteReceived = true, + ) + assert(validSession.isComplete) + assert(InteractiveTx.validateTx(validSession, params).isRight) + + val incompleteSession = validSession.copy(txCompleteSent = false) + assert(InteractiveTx.validateTx(incompleteSession, params) === Left(InvalidCompleteInteractiveTx(params.channelId))) + + val invalidSessions = Seq( + // Too many inputs. + validSession.copy( + localInputs = (1 to 53).map(i => createInput(params.channelId, UInt64(2 * i), 2000 sat)), + remoteInputs = (1 to 200).map(i => createInput(params.channelId, UInt64(2 * i + 1), 1000 sat)), + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript)), + remoteOutputs = Seq(TxAddOutput(params.channelId, UInt64(1), 140_000 sat, createChangeScript())), + ), + // Too many outputs. + validSession.copy( + localInputs = Seq(createInput(params.channelId, UInt64(0), 210_000 sat)), + remoteInputs = Seq(createInput(params.channelId, UInt64(1), 210_000 sat)), + localOutputs = TxAddOutput(params.channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript) +: (1 to 52).map(i => TxAddOutput(params.channelId, UInt64(2 * i), 1000 sat, createChangeScript())), + remoteOutputs = (1 to 200).map(i => TxAddOutput(params.channelId, UInt64(2 * i + 1), 1000 sat, createChangeScript())), + ), + // Funding output is missing. + validSession.copy( + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(2), 140_000 sat, createChangeScript())) + ), + // Multiple funding outputs. + validSession.copy( + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript), TxAddOutput(params.channelId, UInt64(2), 40_000 sat, params.fundingPubkeyScript)) + ), + // Invalid funding amount. + validSession.copy( + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount - 1.sat, params.fundingPubkeyScript), TxAddOutput(params.channelId, UInt64(2), 40_000 sat, createChangeScript())), + ), + validSession.copy( + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount + 1.sat, params.fundingPubkeyScript), TxAddOutput(params.channelId, UInt64(2), 40_000 sat, createChangeScript())), + ), + // Local amount insufficient. + validSession.copy( + localOutputs = Seq(TxAddOutput(params.channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript), TxAddOutput(params.channelId, UInt64(2), 60_000 sat, createChangeScript())), + ), + // Remote amount insufficient. + validSession.copy( + remoteOutputs = Seq(TxAddOutput(params.channelId, UInt64(1), 30_000 sat, createChangeScript())), + ), + // Feerate too low. + validSession.copy( + localInputs = Seq(createInput(params.channelId, UInt64(0), 140_001 sat)), + remoteInputs = Seq(createInput(params.channelId, UInt64(1), 70_001 sat)), + ), + ) + for (session <- invalidSessions) { + assert(session.isComplete) + assert(InteractiveTx.validateTx(session, params) === Left(InvalidCompleteInteractiveTx(params.channelId))) + } + } + +} + +object InteractiveTxSpec { + + def createFundingScript(): ByteVector = { + Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) + } + + def createChangeScript(): ByteVector = { + Script.write(Script.pay2wpkh(randomKey().publicKey)) + } + + def createInput(channelId: ByteVector32, serialId: UInt64, amount: Satoshi): TxAddInput = { + val previousTx = Transaction(2, Nil, Seq(createTxOut(amount), createTxOut(amount), createTxOut(amount)), 0) + TxAddInput(channelId, serialId, previousTx, 1, 0) + } + + def createTxOut(amount: Satoshi): TxOut = { + TxOut(amount, createChangeScript()) + } + + def computeFees(sharedTx: SharedTransaction): Satoshi = { + computeFees(sharedTx.localInputs ++ sharedTx.remoteInputs, sharedTx.localOutputs ++ sharedTx.remoteOutputs) + } + + def computeFees(inputs: Seq[TxAddInput], outputs: Seq[TxAddOutput]): Satoshi = { + val amountIn = inputs.map(i => i.previousTx.txOut(i.previousTxOutput.toInt).amount).sum + val amountOut = outputs.map(_.amount).sum + amountIn - amountOut + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala new file mode 100644 index 0000000000..5c1a4c114a --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingCreatedStateSpec.scala @@ -0,0 +1,259 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.b + +import akka.actor.Status +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.fee.FeeratePerKw +import fr.acinq.eclair.channel.InteractiveTx.FundingContributions +import fr.acinq.eclair.channel.InteractiveTxSpec.{createChangeScript, createInput} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel, TxAbort, TxAckRbf, TxAddInput, TxAddOutput, TxComplete, TxInitRbf, TxSignatures} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64, randomBytes32} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} +import scodec.bits.HexStringSyntax + +import scala.concurrent.duration.DurationInt + +class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, wallet: NoOpOnChainWallet) + + override def withFixture(test: OneArgTest): Outcome = { + val wallet = new NoOpOnChainWallet() + val setup = init(wallet = wallet) + import setup._ + val channelConfig = ChannelConfig.standard + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + + val cid = channelId(bob) + val fundingScript = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams.fundingPubkeyScript + val aliceFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(createInput(cid, UInt64(0), TestConstants.fundingSatoshis + 55_000.sat)), + Seq(TxAddOutput(cid, UInt64(0), TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis, fundingScript), TxAddOutput(cid, UInt64(2), 45_000 sat, createChangeScript())), + )) + val bobFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(createInput(cid, UInt64(1), TestConstants.nonInitiatorFundingSatoshis + 25_000.sat)), + Seq(TxAddOutput(cid, UInt64(3), 20_000 sat, createChangeScript())), + )) + + if (test.tags.contains("message-before-funding")) { + alice ! aliceFunding + val firstMsg = alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob, firstMsg) + bob ! bobFunding + } else { + alice ! aliceFunding + bob ! bobFunding + } + + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOrigin, alice2bob, bob2alice, wallet))) + } + } + + test("complete interactive-tx protocol", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + // The initiator sends the first interactive-tx message. + alice2bob.expectMsgType[TxAddInput] + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddOutput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + } + + test("complete interactive-tx protocol (first message before funding)", Tag(ChannelStateTestsTags.DualFunding), Tag("message-before-funding")) { f => + import f._ + + // The initiator has already sent the first interactive-tx message. + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddOutput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + } + + test("recv invalid interactive-tx message", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + // Invalid serial_id and below dust. + bob2alice.forward(alice, createInput(channelId(alice), UInt64(0), 330 sat)) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.nonEmpty) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TxAbort", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob, TxAbort(channelId(alice), hex"deadbeef")) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(bob.stateName == CLOSED) + + bob2alice.forward(alice, TxAbort(channelId(bob), hex"deadbeef")) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TxSignatures", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob, TxSignatures(channelId(alice), randomBytes32(), Nil)) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(bob.stateName == CLOSED) + + bob2alice.forward(alice, TxSignatures(channelId(bob), randomBytes32(), Nil)) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TxInitRbf", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob, TxInitRbf(channelId(alice), 0, FeeratePerKw(15_000 sat))) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(bob.stateName == CLOSED) + + bob2alice.forward(alice, TxInitRbf(channelId(bob), 0, FeeratePerKw(15_000 sat))) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv TxAckRbf", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob, TxAckRbf(channelId(alice))) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(bob.stateName == CLOSED) + + bob2alice.forward(alice, TxAckRbf(channelId(bob))) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + alice ! Error(finalChannelId, "oops") + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! Error(finalChannelId, "oops") + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + + alice ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + + bob ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice ! INPUT_DISCONNECTED + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! INPUT_DISCONNECTED + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! TickChannelOpenTimeout + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingInternalStateSpec.scala new file mode 100644 index 0000000000..94dcfb41b7 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingInternalStateSpec.scala @@ -0,0 +1,146 @@ +/* + * Copyright 2022 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.b + +import akka.actor.Status +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.channel.InteractiveTx.FundingContributions +import fr.acinq.eclair.channel.InteractiveTxFunder.{FundingFailed, FundingSucceeded} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, Error, Init, OpenDualFundedChannel, TxAddInput, TxAddOutput} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} +import scodec.bits.HexStringSyntax + +import scala.concurrent.duration.DurationInt + +class WaitForDualFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init(wallet = new NoOpOnChainWallet()) + import setup._ + val channelConfig = ChannelConfig.standard + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + within(30 seconds) { + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOrigin, alice2bob, bob2alice))) + } + } + + test("recv FundingContributions", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + val aliceFundingParams = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams + val bobFundingParams = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams + assert(aliceFundingParams.isInitiator) + assert(!bobFundingParams.isInitiator) + assert(aliceFundingParams.fundingAmount === TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis) + assert(aliceFundingParams.fundingAmount === bobFundingParams.fundingAmount) + assert(aliceFundingParams.fundingPubkeyScript === bobFundingParams.fundingPubkeyScript) + + val inputs = Seq(TxAddInput(finalChannelId, UInt64(0), Transaction(2, Nil, Nil, 0), 0, 0)) + val outputs = Seq(TxAddOutput(finalChannelId, UInt64(1), 25000 sat, hex"deadbeef")) + alice ! FundingSucceeded(FundingContributions(inputs, outputs)) + alice2bob.expectMsgType[TxAddInput] // the initiator starts the interactive-tx protocol + alice2bob.expectNoMessage(100 millis) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) + + bob ! FundingSucceeded(FundingContributions(inputs, Nil)) + bob2alice.expectNoMessage(100 millis) // the non-initiator waits for the initiator's first message + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) + } + + test("recv Status.Failure (wallet error)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + alice ! FundingFailed(new RuntimeException("insufficient funds")) + alice2bob.expectMsg(Error(finalChannelId, ChannelFundingError(finalChannelId).getMessage)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! FundingFailed(new RuntimeException("insufficient funds")) + bob2alice.expectMsg(Error(finalChannelId, ChannelFundingError(finalChannelId).getMessage)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + alice ! Error(finalChannelId, "oops") + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! Error(finalChannelId, "oops") + awaitCond(bob.stateName == CLOSED) + } + + test("recv CMD_CLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val finalChannelId = channelId(alice) + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + + alice ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + + bob ! c + sender.expectMsg(RES_SUCCESS(c, finalChannelId)) + awaitCond(bob.stateName == CLOSED) + } + + test("recv INPUT_DISCONNECTED", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == CLOSED) + } + + test("recv TickChannelOpenTimeout", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! TickChannelOpenTimeout + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + +}