From 96a96cd994750c82ec826fb0209f635b73e414b5 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 14 Apr 2022 19:06:15 +0200 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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 6968978d097cadc331eebce724a9a11a3b970a45 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 3 May 2022 14:17:57 +0200 Subject: [PATCH 7/9] 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 | 14 + .../fr/acinq/eclair/channel/Helpers.scala | 13 +- .../acinq/eclair/channel/InteractiveTx.scala | 252 +++++++++++++ .../eclair/channel/InteractiveTxFunder.scala | 200 +++++++++++ .../channel/fsm/ChannelOpenDualFunded.scala | 129 ++++++- .../channel/fsm/ChannelOpenSingleFunder.scala | 4 +- .../eclair/channel/fsm/CommonHandlers.scala | 5 + .../eclair/channel/fsm/ErrorHandlers.scala | 8 +- .../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 | 251 +++++++++++++ .../eclair/channel/InteractiveTxSpec.scala | 338 ++++++++++++++++++ ...tForAcceptDualFundedChannelStateSpec.scala | 2 +- ...aitForOpenDualFundedChannelStateSpec.scala | 4 +- .../WaitForDualFundingCreatedStateSpec.scala | 259 ++++++++++++++ .../WaitForDualFundingInternalStateSpec.scala | 146 ++++++++ 23 files changed, 1710 insertions(+), 48 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..c7b1319889 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,20 @@ 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 UnexpectedFundingSignatures (override val channelId: ByteVector32) extends ChannelException(channelId, "unexpected funding signatures (tx_signatures)") +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/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 0193065ccd..97b8683e08 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 @@ -354,9 +354,14 @@ 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, 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 + def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32, + localParams: LocalParams, remoteParams: RemoteParams, + localFundingAmount: Satoshi, remoteFundingAmount: Satoshi, pushMsat: MilliSatoshi, + commitTxFeerate: FeeratePerKw, + fundingTxHash: ByteVector32, fundingTxOutputIndex: Int, + remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = { + val toLocalMsat = if (localParams.isInitiator) localFundingAmount.toMilliSatoshi - pushMsat else localFundingAmount.toMilliSatoshi + pushMsat + val toRemoteMsat = if (localParams.isInitiator) remoteFundingAmount.toMilliSatoshi + pushMsat else remoteFundingAmount.toMilliSatoshi - pushMsat val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat) val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat) @@ -373,7 +378,7 @@ object Helpers { val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val channelKeyPath = keyManager.keyPath(localParams, channelConfig) - val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey) + val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, localFundingAmount + remoteFundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey) val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0) val (localCommitTx, _) = Commitments.makeLocalTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec) val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec) 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..555ea5a586 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala @@ -0,0 +1,252 @@ +/* + * 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.fee.FeeratePerKw +import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.wire.protocol._ +import scodec.bits.ByteVector + +/** + * 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) + } + + /** Return a dummy transaction containing all local contributions. */ + def dummyLocalTx(session: InteractiveTxSession): Transaction = { + 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)) + Transaction(2, inputs, outputs, 0) + } + + 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..819db969fe 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,61 @@ 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 Left(cause) => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), cause, d, Some(msg)) + case Right((txSession1, outgoingMsg_opt)) => + if (txSession1.isComplete) { + InteractiveTx.validateTx(txSession1, d.fundingParams) match { + case Left(cause) => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), cause, d, Some(msg)) + case Right((completeTx, fundingOutputIndex)) => + val fundingTx = completeTx.buildUnsignedTx() + Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.localAmount, d.fundingParams.remoteAmount, 0 msat, d.commitTxFeerate, fundingTx.hash, fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { + case Left(cause) => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), cause, d, Some(msg)) + 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 + } + } + } else { + stay() using d.copy(txSession = txSession1) sending outgoingMsg_opt + } + } + case _: TxAbort => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), DualFundingAborted(d.channelId), d, Some(msg)) + case _: TxSignatures => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), UnexpectedFundingSignatures(d.channelId), d, Some(msg)) + case _: TxInitRbf => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), InvalidRbfAttempt(d.channelId), d, Some(msg)) + case _: TxAckRbf => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), InvalidRbfAttempt(d.channelId), d, Some(msg)) + } + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + wallet.rollback(InteractiveTx.dummyLocalTx(d.txSession)) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + wallet.rollback(InteractiveTx.dummyLocalTx(d.txSession)) + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + wallet.rollback(InteractiveTx.dummyLocalTx(d.txSession)) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_DUAL_FUNDING_CREATED) => + wallet.rollback(InteractiveTx.dummyLocalTx(d.txSession)) + 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/ChannelOpenSingleFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunder.scala index 9acf67ec23..643d858fc6 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 @@ -177,7 +177,7 @@ 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, 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, commitTxFeerate, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, 0 sat, 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!") @@ -222,7 +222,7 @@ 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, 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, commitTxFeerate, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, 0 sat, 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/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/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 096f3bd610..30d8735e02 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.ClosingTx -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, LightningMessage, OpenChannel} import java.sql.SQLException @@ -65,6 +65,12 @@ trait ErrorHandlers extends CommonHandlers { blockchain ! WatchTxConfirmed(self, closingTx.tx.txid, nodeParams.channelConf.minDepthBlocks) } + def handleInteractiveTxError(sharedTx: Transaction, cause: Throwable, d: ChannelData, msg_opt: Option[LightningMessage]) = { + wallet.rollback(sharedTx) + channelOpenReplyToUser(Left(LocalError(cause))) + handleLocalError(cause, d, msg_opt) + } + def handleLocalError(cause: Throwable, d: ChannelData, msg: Option[Any]) = { cause match { case _: ForcedLocalCommit => 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..6d2936595f --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala @@ -0,0 +1,251 @@ +/* + * 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)) + // Only used utxos should be locked. + awaitCond({ + val locks = getLocks(probe, walletRpcClient) + locks === contributions.inputs.map(toOutPoint).toSet + }, max = 10 seconds, interval = 100 millis) + + 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/a/WaitForAcceptDualFundedChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptDualFundedChannelStateSpec.scala index 4d4e0bd275..a51d8f1059 100644 --- 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 @@ -69,7 +69,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt 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 + val channelFeatures = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].channelFeatures assert(channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) assert(channelFeatures.hasFeature(Features.DualFunding)) aliceOrigin.expectNoMessage() 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 index 0beec52237..ac321f70a0 100644 --- 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 @@ -77,8 +77,8 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur 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 + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CREATED) + val channelFeatures = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CREATED].channelFeatures assert(channelFeatures.channelType === ChannelTypes.AnchorOutputsZeroFeeHtlcTx) assert(channelFeatures.hasFeature(Features.DualFunding)) } 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] + } + +} From 24bc08e50624abdf28bc83fbf1d969bde546fdeb Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 6 May 2022 11:54:37 +0200 Subject: [PATCH 8/9] Exchange dual funding signatures We exchange commit and funding signatures for a dual-funded channel. Once we've signed the funding transaction, we must start to remember this channel as our peer may publish it. --- .../eclair/blockchain/OnChainWallet.scala | 6 + .../fr/acinq/eclair/channel/ChannelData.scala | 22 +- .../eclair/channel/ChannelExceptions.scala | 1 + .../fr/acinq/eclair/channel/Helpers.scala | 1 + .../acinq/eclair/channel/InteractiveTx.scala | 114 ++++++- .../fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../channel/fsm/ChannelOpenDualFunded.scala | 118 +++++++- .../channel/fsm/ChannelOpenSingleFunder.scala | 19 +- .../eclair/channel/fsm/CommonHandlers.scala | 14 +- .../channel/fsm/DualFundingHandlers.scala | 52 ++++ ...lers.scala => SingleFundingHandlers.scala} | 30 +- .../channel/version3/ChannelCodecs3.scala | 46 ++- .../eclair/wire/protocol/CommonCodecs.scala | 2 + .../scala/fr/acinq/eclair/TestDatabases.scala | 2 +- .../blockchain/DummyOnChainWallet.scala | 14 +- .../channel/InteractiveTxFunderSpec.scala | 55 ++-- .../eclair/channel/InteractiveTxSpec.scala | 78 ++++- .../ChannelStateTestsHelperMethods.scala | 2 + .../b/WaitForDualFundingSignedStateSpec.scala | 283 ++++++++++++++++++ 19 files changed, 763 insertions(+), 98 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala rename eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/{FundingHandlers.scala => SingleFundingHandlers.scala} (82%) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.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 61006822d0..f3ada93f87 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 @@ -38,6 +38,12 @@ trait OnChainChannelFunder { /** Sign the wallet inputs of the provided transaction. */ def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] + /** + * Publish a transaction on the bitcoin network. + * This method must be idempotent: if the tx was already published, it must return a success. + */ + def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32] + /** Create a fully signed channel funding transaction with the provided pubkeyScript. */ def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] 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 4a91abb426..e013a6aa49 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,11 +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.channel.InteractiveTx.{InteractiveTxParams, InteractiveTxSession, SharedTransaction, SignedSharedTransaction} 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, CommitSig, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, InteractiveTxMessage, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, InteractiveTxMessage, OnionRoutingPacket, OpenChannel, OpenDualFundedChannel, Shutdown, TxSignatures, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -62,6 +62,7 @@ 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 +case object WAIT_FOR_DUAL_FUNDING_CONFIRMED extends ChannelState // Channel opened: case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState @@ -374,6 +375,9 @@ case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Opti } } +/** Once a dual funding tx has been signed, we must remember the associated commitments. */ +case class DualFundingTx(fundingTx: SignedSharedTransaction, commitments: Commitments) + sealed trait ChannelData extends PossiblyHarmful { def channelId: ByteVector32 } @@ -469,11 +473,23 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelId: ByteVector32, localParams: LocalParams, remoteParams: RemoteParams, fundingParams: InteractiveTxParams, + sharedTx: SharedTransaction, + fundingOutputIndex: Int, commitTxFeerate: FeeratePerKw, remoteFirstPerCommitmentPoint: PublicKey, channelFlags: ChannelFlags, channelConfig: ChannelConfig, - channelFeatures: ChannelFeatures) extends TransientChannelData + channelFeatures: ChannelFeatures, + signingInProgress: Boolean, + commitments_opt: Option[Commitments], + remoteSigs_opt: Option[TxSignatures]) extends TransientChannelData { + val fundingTx: Transaction = sharedTx.buildUnsignedTx() +} +final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, + fundingTx: SignedSharedTransaction, + previousFundingTxs: Seq[DualFundingTx], + waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm + deferred: Option[FundingLocked]) extends PersistentChannelData 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 c7b1319889..0496d2ed0c 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 @@ -62,6 +62,7 @@ case class InvalidCompleteInteractiveTx (override val channelId: Byte 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 UnexpectedFundingSignatures (override val channelId: ByteVector32) extends ChannelException(channelId, "unexpected funding signatures (tx_signatures)") +case class InvalidFundingFeerate (override val channelId: ByteVector32, targetFeerate: FeeratePerKw, actualFeerate: FeeratePerKw) extends ChannelException(channelId, s"invalid funding feerate: target=$targetFeerate actual=$actualFeerate") 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") 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 97b8683e08..f86e7b9364 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 @@ -56,6 +56,7 @@ object Helpers { remoteParams = data.commitments.remoteParams.copy(initFeatures = remoteInit.features)) data match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.copy(commitments = commitments1) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) case d: DATA_NORMAL => d.copy(commitments = commitments1) case d: DATA_SHUTDOWN => d.copy(commitments = commitments1) 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 index 555ea5a586..1b00043bba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala @@ -17,12 +17,19 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.eclair.UInt64 +import fr.acinq.eclair.blockchain.OnChainChannelFunder +import fr.acinq.eclair.blockchain.OnChainWallet.SignTransactionResponse 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} +import scala.util.{Failure, Success, Try} + /** * Created by t-bast on 27/04/2022. */ @@ -74,14 +81,59 @@ object InteractiveTx { /** Inputs and outputs we contribute to the funding transaction. */ case class FundingContributions(inputs: Seq[TxAddInput], outputs: Seq[TxAddOutput]) + /** A lighter version of our peer's TxAddInput that avoids storing potentially large messages in our DB. */ + case class RemoteTxAddInput(serialId: UInt64, outPoint: OutPoint, txOut: TxOut, sequence: Long) + + object RemoteTxAddInput { + def apply(i: TxAddInput): RemoteTxAddInput = RemoteTxAddInput(i.serialId, toOutPoint(i), i.previousTx.txOut(i.previousTxOutput.toInt), i.sequence) + } + + /** A lighter version of our peer's TxAddOutput that avoids storing potentially large messages in our DB. */ + case class RemoteTxAddOutput(serialId: UInt64, amount: Satoshi, pubkeyScript: ByteVector) + + object RemoteTxAddOutput { + def apply(o: TxAddOutput): RemoteTxAddOutput = RemoteTxAddOutput(o.serialId, o.amount, o.pubkeyScript) + } + /** Unsigned transaction created collaboratively. */ - case class SharedTransaction(localInputs: Seq[TxAddInput], remoteInputs: Seq[TxAddInput], localOutputs: Seq[TxAddOutput], remoteOutputs: Seq[TxAddOutput], lockTime: Long) { + case class SharedTransaction(localInputs: Seq[TxAddInput], remoteInputs: Seq[RemoteTxAddInput], localOutputs: Seq[TxAddOutput], remoteOutputs: Seq[RemoteTxAddOutput], lockTime: Long) { + val localAmountIn: Satoshi = localInputs.map(i => i.previousTx.txOut(i.previousTxOutput.toInt).amount).sum + val remoteAmountIn: Satoshi = remoteInputs.map(_.txOut.amount).sum + val totalAmountIn: Satoshi = localAmountIn + remoteAmountIn + val fees: Satoshi = totalAmountIn - localOutputs.map(_.amount).sum - remoteOutputs.map(_.amount).sum + 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)) + val localTxIn = localInputs.map(i => (i.serialId, TxIn(toOutPoint(i), ByteVector.empty, i.sequence))) + val remoteTxIn = remoteInputs.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))) + val inputs = (localTxIn ++ remoteTxIn).sortBy(_._1).map(_._2) + val localTxOut = localOutputs.map(o => (o.serialId, TxOut(o.amount, o.pubkeyScript))) + val remoteTxOut = remoteOutputs.map(o => (o.serialId, TxOut(o.amount, o.pubkeyScript))) + val outputs = (localTxOut ++ remoteTxOut).sortBy(_._1).map(_._2) + Transaction(2, inputs, outputs, lockTime) + } + } + + // @formatter:off + sealed trait SignedSharedTransaction { + def tx: SharedTransaction + } + case class PartiallySignedSharedTransaction(tx: SharedTransaction, localSigs: TxSignatures) extends SignedSharedTransaction + case class FullySignedSharedTransaction(tx: SharedTransaction, localSigs: TxSignatures, remoteSigs: TxSignatures) extends SignedSharedTransaction { + val signedTx: Transaction = { + import tx._ + require(localSigs.witnesses.length == localInputs.length, "the number of local signatures does not match the number of local inputs") + require(remoteSigs.witnesses.length == remoteInputs.length, "the number of remote signatures does not match the number of remote inputs") + val signedLocalInputs = localInputs.sortBy(_.serialId).zip(localSigs.witnesses).map { case (i, w) => (i.serialId, TxIn(toOutPoint(i), ByteVector.empty, i.sequence, w)) } + val signedRemoteInputs = remoteInputs.sortBy(_.serialId).zip(remoteSigs.witnesses).map { case (i, w) => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence, w)) } + val inputs = (signedLocalInputs ++ signedRemoteInputs).sortBy(_._1).map(_._2) + val localTxOut = localOutputs.map(o => (o.serialId, TxOut(o.amount, o.pubkeyScript))) + val remoteTxOut = remoteOutputs.map(o => (o.serialId, TxOut(o.amount, o.pubkeyScript))) + val outputs = (localTxOut ++ remoteTxOut).sortBy(_._1).map(_._2) Transaction(2, inputs, outputs, lockTime) } + val feerate: FeeratePerKw = Transactions.fee2rate(tx.fees, signedTx.weight()) } + // @formatter:on // 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 @@ -184,7 +236,7 @@ object InteractiveTx { } 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 sharedTx = SharedTransaction(session.localInputs, session.remoteInputs.map(i => RemoteTxAddInput(i)), session.localOutputs, session.remoteOutputs.map(o => RemoteTxAddOutput(o)), params.lockTime) val tx = sharedTx.buildUnsignedTx() if (!session.isComplete) { @@ -208,13 +260,10 @@ object InteractiveTx { 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) + if (sharedTx.localAmountIn < localAmountOut || sharedTx.remoteAmountIn < remoteAmountOut) { + log.warning("invalid interactive tx: input amount is too small (localIn={}, localOut={}, remoteIn={}, remoteOut={})", sharedTx.localAmountIn, localAmountOut, sharedTx.remoteAmountIn, remoteAmountOut) return Left(InvalidCompleteInteractiveTx(params.channelId)) } @@ -227,15 +276,54 @@ object InteractiveTx { } 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)) + if (sharedTx.fees < minimumFee) { + log.warning("invalid interactive tx: below the target feerate (target={}, actual={})", params.targetFeerate, Transactions.fee2rate(sharedTx.fees, minimumWeight)) return Left(InvalidCompleteInteractiveTx(params.channelId)) } Right(sharedTx, sharedOutputIndex) } + def signTx(channelId: ByteVector32, unsignedTx: SharedTransaction, wallet: OnChainChannelFunder)(implicit ec: ExecutionContext): Future[PartiallySignedSharedTransaction] = { + val tx = unsignedTx.buildUnsignedTx() + if (unsignedTx.localInputs.isEmpty) { + Future.successful(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(channelId, tx.txid, Nil))) + } else { + wallet.signTransaction(tx, allowIncomplete = true).map { + case SignTransactionResponse(signedTx, _) => + val localOutpoints = unsignedTx.localInputs.map(toOutPoint).toSet + val sigs = signedTx.txIn.filter(txIn => localOutpoints.contains(txIn.outPoint)).map(_.witness) + PartiallySignedSharedTransaction(unsignedTx, TxSignatures(channelId, tx.txid, sigs)) + } + } + } + + def addRemoteSigs(params: InteractiveTxParams, partiallySignedTx: PartiallySignedSharedTransaction, remoteSigs: TxSignatures): Either[ChannelException, FullySignedSharedTransaction] = { + if (partiallySignedTx.tx.localInputs.length != partiallySignedTx.localSigs.witnesses.length) { + return Left(InvalidFundingSignature(params.channelId, Some(partiallySignedTx.tx.buildUnsignedTx()))) + } + if (partiallySignedTx.tx.remoteInputs.length != remoteSigs.witnesses.length) { + return Left(InvalidFundingSignature(params.channelId, Some(partiallySignedTx.tx.buildUnsignedTx()))) + } + val txWithSigs = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, remoteSigs) + if (remoteSigs.txId != txWithSigs.signedTx.txid) { + return Left(InvalidFundingSignature(params.channelId, Some(partiallySignedTx.tx.buildUnsignedTx()))) + } + // We allow a 5% error margin since witness size prediction could be inaccurate. + if (params.localAmount > 0.sat && txWithSigs.feerate < params.targetFeerate * 0.95) { + return Left(InvalidFundingFeerate(params.channelId, params.targetFeerate, txWithSigs.feerate)) + } + val previousOutputs = { + val localOutputs = txWithSigs.tx.localInputs.map(i => toOutPoint(i) -> i.previousTx.txOut(i.previousTxOutput.toInt)).toMap + val remoteOutputs = txWithSigs.tx.remoteInputs.map(i => i.outPoint -> i.txOut).toMap + localOutputs ++ remoteOutputs + } + Try(Transaction.correctlySpends(txWithSigs.signedTx, previousOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + case Failure(_) => Left(InvalidFundingSignature(params.channelId, Some(partiallySignedTx.tx.buildUnsignedTx()))) // NB: we don't send our signatures to our peer. + case Success(_) => Right(txWithSigs) + } + } + /** Return a dummy transaction containing all local contributions. */ def dummyLocalTx(session: InteractiveTxSession): Transaction = { val inputs = (session.localInputs ++ session.toSend.collect { case Left(addInput) => addInput }).map(i => TxIn(toOutPoint(i), ByteVector.empty, i.sequence)) 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 809ea23ee2..2b35cc6e44 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 @@ -166,7 +166,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val with ChannelOpenSingleFunder with ChannelOpenDualFunded with CommonHandlers - with FundingHandlers + with SingleFundingHandlers with ErrorHandlers { import 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 index 819db969fe..397bd04828 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,23 +16,29 @@ package fr.acinq.eclair.channel.fsm +import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} +import akka.pattern.pipe import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmed import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.channel.InteractiveTx.InteractiveTxParams +import fr.acinq.eclair.channel.InteractiveTx.{InteractiveTxParams, PartiallySignedSharedTransaction} 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.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions.TxOwner +import fr.acinq.eclair.transactions.{Scripts, Transactions} import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Features, MilliSatoshiLong} +import fr.acinq.eclair.{Features, MilliSatoshiLong, randomKey} + +import scala.util.{Failure, Success} /** * Created by t-bast on 19/04/2022. */ -trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { +trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { this: Channel => @@ -60,9 +66,10 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { |<--------------------------------| | tx_signatures | |<--------------------------------| + | | WAIT_FOR_DUAL_FUNDING_CONFIRMED | tx_signatures | |-------------------------------->| - WAIT_FOR_DUAL_FUNDING_CONFIRMED | | WAIT_FOR_DUAL_FUNDING_CONFIRMED + WAIT_FOR_DUAL_FUNDING_CONFIRMED | | | tx_init_rbf | |-------------------------------->| | tx_ack_rbf | @@ -274,10 +281,10 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.localAmount, d.fundingParams.remoteAmount, 0 msat, d.commitTxFeerate, fundingTx.hash, fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { case Left(cause) => handleInteractiveTxError(InteractiveTx.dummyLocalTx(d.txSession), cause, d, Some(msg)) case Right((_, localCommitTx, _, remoteCommitTx)) => - require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") + require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "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) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(d.channelId, d.localParams, d.remoteParams, d.fundingParams, completeTx, fundingOutputIndex, d.commitTxFeerate, d.remoteFirstPerCommitmentPoint, d.channelFlags, d.channelConfig, d.channelFeatures, signingInProgress = false, None, None) goto(WAIT_FOR_DUAL_FUNDING_SIGNED) using nextData sending Seq(outgoingMsg_opt, Some(commitSig)).flatten } } @@ -313,6 +320,103 @@ trait ChannelOpenDualFunded extends FundingHandlers with ErrorHandlers { }) when(WAIT_FOR_DUAL_FUNDING_SIGNED)(handleExceptions { + case Event(commit: CommitSig, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.fundingAmount, 0 msat, d.commitTxFeerate, d.fundingTx.hash, d.fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { + case Left(cause) => handleInteractiveTxError(d.fundingTx, cause, d, Some(commit)) + case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => + val fundingPubKey = keyManager.fundingPublicKey(d.localParams.fundingKeyPath) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, d.channelFeatures.commitmentFormat) + val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, d.remoteParams.fundingPubKey, localSigOfLocalTx, commit.signature) + Transactions.checkSpendable(signedLocalCommitTx) match { + case Failure(_) => handleInteractiveTxError(d.fundingTx, InvalidCommitmentSignature(d.channelId, signedLocalCommitTx.tx), d, Some(commit)) + case Success(_) => + val commitments = Commitments( + d.channelId, d.channelConfig, d.channelFeatures, + d.localParams, d.remoteParams, d.channelFlags, + LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, commit.signature), htlcTxsAndRemoteSigs = Nil), + RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, d.remoteFirstPerCommitmentPoint), + LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil), + localNextHtlcId = 0L, remoteNextHtlcId = 0L, + originChannels = Map.empty, + remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array, + localCommitTx.input, + ShaChain.init) + context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) + // The peer with the lowest total of input amount must transmit its `tx_signatures` first. + if (d.fundingParams.localAmount <= d.fundingParams.remoteAmount) { + InteractiveTx.signTx(d.channelId, d.sharedTx, wallet).pipeTo(self) + stay() using d.copy(commitments_opt = Some(commitments), signingInProgress = true) + } else { + stay() using d.copy(commitments_opt = Some(commitments)) + } + } + } + + case Event(txSigs: TxSignatures, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + d.commitments_opt match { + case Some(_) if d.signingInProgress => + stay() using d.copy(remoteSigs_opt = Some(txSigs)) + case Some(_) => + InteractiveTx.signTx(d.channelId, d.sharedTx, wallet).pipeTo(self) + stay() using d.copy(remoteSigs_opt = Some(txSigs), signingInProgress = true) + case None => + log.error("received funding tx signatures before commitment signatures, aborting") + handleInteractiveTxError(d.fundingTx, ChannelFundingError(d.channelId), d, Some(txSigs)) + } + + case Event(partiallySignedTx: PartiallySignedSharedTransaction, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + val fundingMinDepth = Helpers.minDepthForFunding(nodeParams.channelConf, d.fundingParams.fundingAmount) + d.commitments_opt match { + case Some(commitments) => d.remoteSigs_opt match { + case Some(remoteSigs) => InteractiveTx.addRemoteSigs(d.fundingParams, partiallySignedTx, remoteSigs) match { + case Left(cause) => handleInteractiveTxError(d.fundingTx, cause, d, Some(remoteSigs)) + case Right(signedTx) => + log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, d.fundingTx.txid) + blockchain ! WatchFundingConfirmed(self, d.fundingTx.txid, fundingMinDepth) + // NB: we publish the funding tx only *after* the channel state has been written to disk because we want + // to make sure we first persist the commitment that returns back the funds to us in case of problem + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, signedTx, Nil, nodeParams.currentBlockHeight, None) + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using nextData storing() sending partiallySignedTx.localSigs calling publishFundingTx(nextData) + } + case None => + // We send our `tx_signatures` first, we must remember the channel even though we don't have the fully + // signed funding transaction yet. Our peer may publish the funding transaction without explicitly sending + // us their signatures. + blockchain ! WatchFundingConfirmed(self, d.fundingTx.txid, fundingMinDepth) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, partiallySignedTx, Nil, nodeParams.currentBlockHeight, None) + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using nextData storing() sending partiallySignedTx.localSigs + } + case None => + log.error("funding tx signed before commitment was signed, aborting") + handleInteractiveTxError(d.fundingTx, ChannelFundingError(d.channelId), d, None) + } + + case Event(f: Status.Failure, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + log.error(f.cause, "could not sign dual-funded transaction: ") + handleInteractiveTxError(d.fundingTx, ChannelFundingError(d.channelId), d, None) + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelClosed(d.channelId))) + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(RemoteError(e))) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("disconnected")))) + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => + wallet.rollback(d.fundingTx) + channelOpenReplyToUser(Left(LocalError(new RuntimeException("open channel cancelled, took too long")))) + goto(CLOSED) + }) + + when(WAIT_FOR_DUAL_FUNDING_CONFIRMED)(handleExceptions { case Event(msg, d) => ??? }) 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 643d858fc6..f02107623f 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 @@ -45,7 +45,7 @@ import scala.util.{Failure, Success, Try} /** * This trait contains the state machine for the single-funder channel funding flow. */ -trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { +trait ChannelOpenSingleFunder extends SingleFundingHandlers with ErrorHandlers { this: Channel => @@ -296,24 +296,9 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers { watchFundingTx(commitments) blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) log.info(s"committing txid=${fundingTx.txid}") - // we will publish the funding tx only after the channel state has been written to disk because we want to // make sure we first persist the commitment that returns back the funds to us in case of problem - def publishFundingTx(): Unit = { - wallet.commit(fundingTx).onComplete { - case Success(true) => - context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) - channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(channelId))) - case Success(false) => - channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) - self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published - case Failure(t) => - channelOpenReplyToUser(Left(LocalError(t))) - log.error(t, s"error while committing funding tx: ") // tx may still have been published, can't fail-fast - } - } - - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx() + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx(commitments, fundingTx, fundingTxFee) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => 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 5feb253b6d..e8b2515dde 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 @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel.fsm -import akka.actor.FSM +import akka.actor.{FSM, Status} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.PendingCommandsDb @@ -36,6 +36,18 @@ trait CommonHandlers { this: Channel => + /** + * This function is used to return feedback to user at channel opening + */ + def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { + val m = message match { + case Left(LocalError(t)) => Status.Failure(t) + case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) + case Right(s) => s + } + origin_opt.foreach(_ ! m) + } + def send(msg: LightningMessage): Unit = { peer ! Peer.OutgoingMessage(msg, activeConnection) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala new file mode 100644 index 0000000000..ddf22c8b49 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -0,0 +1,52 @@ +/* + * 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.eclair.channel._ + +import scala.util.{Failure, Success} + +/** + * Created by t-bast on 06/05/2022. + */ + +/** + * This trait contains handlers related to dual-funding channel transactions. + */ +trait DualFundingHandlers extends CommonHandlers { + + this: Channel => + + def publishFundingTx(d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED): Unit = { + d.fundingTx match { + case _: InteractiveTx.PartiallySignedSharedTransaction => + log.info("we haven't received remote funding signatures yet: we cannot publish the funding transaction but our peer should publish it") + case fundingTx: InteractiveTx.FullySignedSharedTransaction => + // Note that we don't use wallet.commit because we don't want to rollback on failure, since our peer may be able + // to publish and we may be able to RBF. + wallet.publishTransaction(fundingTx.signedTx).onComplete { + case Success(_) => + context.system.eventStream.publish(TransactionPublished(d.commitments.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.fees, "funding")) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(d.commitments.channelId))) + case Failure(t) => + channelOpenReplyToUser(Left(LocalError(t))) + log.error(t, "error while committing funding tx: ") // tx may be published by our peer, we can't fail-fast + } + } + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala similarity index 82% rename from eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala rename to eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala index f478859c2f..9ba758e4ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/FundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/SingleFundingHandlers.scala @@ -16,9 +16,8 @@ package fr.acinq.eclair.channel.fsm -import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{TypedActorRefOps, actorRefAdapter} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair.BlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMeta, GetTxWithMetaResponse, WatchFundingSpent} import fr.acinq.eclair.channel._ @@ -34,30 +33,29 @@ import scala.util.{Failure, Success} */ /** - * This trait contains handlers related to funding channel transactions. + * This trait contains handlers related to single-funder channel transactions. */ -trait FundingHandlers extends CommonHandlers { +trait SingleFundingHandlers extends CommonHandlers { this: Channel => - /** - * This function is used to return feedback to user at channel opening - */ - def channelOpenReplyToUser(message: Either[ChannelOpenError, ChannelOpenResponse]): Unit = { - val m = message match { - case Left(LocalError(t)) => Status.Failure(t) - case Left(RemoteError(e)) => Status.Failure(new RuntimeException(s"peer sent error: ascii='${e.toAscii}' bin=${e.data.toHex}")) - case Right(s) => s + def publishFundingTx(commitments: Commitments, fundingTx: Transaction, fundingTxFee: Satoshi): Unit = { + wallet.commit(fundingTx).onComplete { + case Success(true) => + context.system.eventStream.publish(TransactionPublished(commitments.channelId, remoteNodeId, fundingTx, fundingTxFee, "funding")) + channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(commitments.channelId))) + case Success(false) => + channelOpenReplyToUser(Left(LocalError(new RuntimeException("couldn't publish funding tx")))) + self ! BITCOIN_FUNDING_PUBLISH_FAILED // fail-fast: this should be returned only when we are really sure the tx has *not* been published + case Failure(t) => + channelOpenReplyToUser(Left(LocalError(t))) + log.error(t, "error while committing funding tx: ") // tx may still have been published, can't fail-fast } - origin_opt.foreach(_ ! m) } def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { - // TODO: should we wait for an acknowledgment from the watcher? val knownSpendingTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, knownSpendingTxs) - // TODO: implement this? (not needed if we use a reasonable min_depth) - //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks, BITCOIN_FUNDING_LOST) } /** 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 2f18d60117..e7c31876ba 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,8 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.{ExtendedPrivateKey, KeyPath} -import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction, TxOut} +import fr.acinq.eclair.channel.InteractiveTx._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ @@ -328,6 +329,48 @@ private[channel] object ChannelCodecs3 { ("shortChannelId" | shortchannelid) :: ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + private val remoteTxAddInputCodec: Codec[RemoteTxAddInput] = ( + ("serialId" | uint64) :: + ("outPoint" | outPointCodec) :: + ("txOut" | txOutCodec) :: + ("sequence" | uint32)).as[RemoteTxAddInput] + + private val remoteTxAddOutputCodec: Codec[RemoteTxAddOutput] = ( + ("serialId" | uint64) :: + ("amount" | satoshi) :: + ("scriptPubKey" | variableSizeBytes(uint16, bytes))).as[RemoteTxAddOutput] + + private val sharedTransactionCodec: Codec[SharedTransaction] = ( + ("localInputs" | seqOfN(uint16, lengthDelimited(txAddInputCodec))) :: + ("remoteInputs" | seqOfN(uint16, remoteTxAddInputCodec)) :: + ("localOutputs" | seqOfN(uint16, lengthDelimited(txAddOutputCodec))) :: + ("remoteOutputs" | seqOfN(uint16, remoteTxAddOutputCodec)) :: + ("lockTime" | uint32)).as[SharedTransaction] + + private val partiallySignedSharedTransactionCodec: Codec[PartiallySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec))).as[PartiallySignedSharedTransaction] + + private val fullySignedSharedTransactionCodec: Codec[FullySignedSharedTransaction] = ( + ("sharedTx" | sharedTransactionCodec) :: + ("localSigs" | lengthDelimited(txSignaturesCodec)) :: + ("remoteSigs" | lengthDelimited(txSignaturesCodec))).as[FullySignedSharedTransaction] + + private val signedSharedTransactionCodec: Codec[SignedSharedTransaction] = discriminated[SignedSharedTransaction].by(uint16) + .typecase(0x01, partiallySignedSharedTransactionCodec) + .typecase(0x02, fullySignedSharedTransactionCodec) + + private val dualFundingTxCodec: Codec[DualFundingTx] = ( + ("fundingTx" | signedSharedTransactionCodec) :: + ("commitments" | commitmentsCodec)).as[DualFundingTx] + + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( + ("commitments" | commitmentsCodec) :: + ("fundingTx" | signedSharedTransactionCodec) :: + ("previousFundingTxs" | seqOfN(uint16, dualFundingTxCodec)) :: + ("waitingSince" | blockHeight) :: + ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: @@ -387,6 +430,7 @@ private[channel] object ChannelCodecs3 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x09, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec) .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x07, Codecs.DATA_NORMAL_Codec) .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) 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 78ad89f00a..1a2ee7593d 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 @@ -110,6 +110,8 @@ object CommonCodecs { val listofsignatures: Codec[List[ByteVector64]] = listOfN(uint16, bytes64) + def seqOfN[A](countCodec: Codec[Int], valueCodec: Codec[A]): Codec[Seq[A]] = listOfN(countCodec, valueCodec).xmap(_.toSeq, _.toList) + val channelflags: Codec[ChannelFlags] = (ignore(7) dropLeft bool).as[ChannelFlags] val ipv4address: Codec[Inet4Address] = bytes(4).xmap(b => InetAddress.getByAddress(b.toArray).asInstanceOf[Inet4Address], a => ByteVector(a.getAddress)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 7b813fee70..1212fdbd50 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -45,7 +45,6 @@ object TestDatabases { dbs.copy(channels = new SqliteChannelsDbWithValidation(dbs.channels)) } - /** * ChannelsDb instance that wraps around an actual db instance and does additional checks * This can be thought of as fuzzing and fills a gap between codec unit tests and database tests, by checking that channel state can be written and read consistently @@ -71,6 +70,7 @@ object TestDatabases { def freeze3(input: PersistentChannelData): PersistentChannelData = input match { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) 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 93bdfaf3e4..2af77eca29 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 @@ -40,16 +40,18 @@ 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 fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, lockUtxos: Boolean)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Future.successful(FundTransactionResponse(tx, 0 sat, None)) - override def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = ??? + override def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = Future.successful(SignTransactionResponse(tx, complete = true)) + + override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32] = Future.successful(tx.txid) 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 getTransaction(txId: ByteVector32)(implicit ec: ExecutionContext): Future[Transaction] = Future.failed(new RuntimeException("transaction not found")) override def rollback(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = { rolledback = rolledback + tx @@ -64,7 +66,7 @@ class NoOpOnChainWallet extends OnChainWallet { import DummyOnChainWallet._ - var rolledback = Set.empty[Transaction] + var rolledback = Seq.empty[Transaction] override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) @@ -76,6 +78,8 @@ class NoOpOnChainWallet extends OnChainWallet { override def signTransaction(tx: Transaction, allowIncomplete: Boolean)(implicit ec: ExecutionContext): Future[SignTransactionResponse] = Promise().future // will never be completed + override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[ByteVector32] = Future.successful(tx.txid) + 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) @@ -83,7 +87,7 @@ class NoOpOnChainWallet extends OnChainWallet { 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 + rolledback = rolledback :+ tx Future.successful(true) } 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 index 6d2936595f..729aec19ff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxFunderSpec.scala @@ -26,9 +26,10 @@ 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.InteractiveTx._ import fr.acinq.eclair.channel.InteractiveTxFunder.{Fund, FundingFailed, FundingSucceeded} import fr.acinq.eclair.transactions.Transactions +import fr.acinq.eclair.wire.protocol.TxSignatures import fr.acinq.eclair.{TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -96,20 +97,23 @@ class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with 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 + val sharedInitiatorTx = SharedTransaction(initiatorContributions.inputs, nonInitiatorContributions.inputs.map(i => RemoteTxAddInput(i)), initiatorContributions.outputs, nonInitiatorContributions.outputs.map(o => RemoteTxAddOutput(o)), initiatorParams.lockTime) + InteractiveTx.signTx(channelId, sharedInitiatorTx, initiatorWallet).pipeTo(probe.ref) + val initiatorSignedTx = probe.expectMsgType[PartiallySignedSharedTransaction] + val sharedNonInitiatorTx = SharedTransaction(nonInitiatorContributions.inputs, initiatorContributions.inputs.map(i => RemoteTxAddInput(i)), nonInitiatorContributions.outputs, initiatorContributions.outputs.map(o => RemoteTxAddOutput(o)), nonInitiatorParams.lockTime) + InteractiveTx.signTx(channelId, sharedNonInitiatorTx, nonInitiatorWallet).pipeTo(probe.ref) + val nonInitiatorSignedTx = probe.expectMsgType[PartiallySignedSharedTransaction] + val Right(initiatorTx) = InteractiveTx.addRemoteSigs(initiatorParams, initiatorSignedTx, nonInitiatorSignedTx.localSigs) + val Right(nonInitiatorTx) = InteractiveTx.addRemoteSigs(nonInitiatorParams, nonInitiatorSignedTx, initiatorSignedTx.localSigs) + assert(initiatorTx.signedTx === nonInitiatorTx.signedTx) + val signedTx = initiatorTx.signedTx 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)") + assert(mempoolTx.fees === sharedInitiatorTx.fees) + assert(targetFeerate <= initiatorTx.feerate && initiatorTx.feerate <= targetFeerate * 1.25, s"unexpected feerate (target=$targetFeerate actual=${initiatorTx.feerate})") } test("fund transaction without contributing (initiator)") { @@ -129,12 +133,13 @@ class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with 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 + val initiatorFees = computeFees(fundingContributions) + 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()) + val partialTx = SharedTransaction(fundingContributions.inputs, Nil, fundingContributions.outputs, Nil, params.lockTime) + InteractiveTx.signTx(params.channelId, partialTx, wallet).pipeTo(probe.ref) + val partiallySignedTx = probe.expectMsgType[PartiallySignedSharedTransaction] + val initiatorTx = FullySignedSharedTransaction(partiallySignedTx.tx, partiallySignedTx.localSigs, TxSignatures(params.channelId, partialTx.buildUnsignedTx().txid, Nil)) + val feerate = Transactions.fee2rate(initiatorFees, initiatorTx.signedTx.weight()) assert(params.targetFeerate <= feerate && feerate <= params.targetFeerate * 1.25, s"unexpected feerate (target=${params.targetFeerate} actual=$feerate)") } @@ -167,7 +172,7 @@ class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with 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) + val fee1 = computeFees(contributions1) // We fund if a second time, re-using the same inputs and adding new ones if necessary. val feerate2 = FeeratePerKw(7500 sat) @@ -177,7 +182,7 @@ class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with 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) + val fee2 = computeFees(contributions2) assert(fee2 > fee1) } @@ -237,15 +242,15 @@ class InteractiveTxFunderSpec extends TestKitBaseClass with BitcoindService with }, max = 10 seconds, interval = 100 millis) 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) + InteractiveTx.signTx(params.channelId, sharedTx, wallet).pipeTo(probe.ref) + val partiallySignedTx = probe.expectMsgType[PartiallySignedSharedTransaction] + val Right(initiatorTx) = InteractiveTx.addRemoteSigs(params, partiallySignedTx, TxSignatures(params.channelId, sharedTx.buildUnsignedTx().txid, Nil)) + wallet.publishTransaction(initiatorTx.signedTx).pipeTo(probe.ref) + probe.expectMsg(initiatorTx.signedTx.txid) + wallet.getMempoolTx(initiatorTx.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)") + assert(mempoolTx.fees === sharedTx.fees) + assert(params.targetFeerate <= initiatorTx.feerate && initiatorTx.feerate <= params.targetFeerate * 1.25, s"unexpected feerate (target=${params.targetFeerate} actual=${initiatorTx.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 index 37c5e29c55..04c1e1ba9f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala @@ -16,7 +16,8 @@ package fr.acinq.eclair.channel -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.{SigHash, SigVersion} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.InteractiveTx._ import fr.acinq.eclair.transactions.Scripts @@ -304,6 +305,71 @@ class InteractiveTxSpec extends AnyFunSuiteLike { } } + test("validate remote signatures") { + val channelId = randomBytes32() + val params = InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 50_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(5000 sat)) + val initiatorKey = randomKey() + val nonInitiatorKey = randomKey() + val sharedTx = SharedTransaction( + localInputs = Seq( + TxAddInput(channelId, UInt64(0), Transaction(2, Nil, Seq(TxOut(120_000 sat, Script.pay2wpkh(initiatorKey.publicKey))), 0), 0, 0) + ), + remoteInputs = Seq( + RemoteTxAddInput(UInt64(1), OutPoint(randomBytes32(), 0), TxOut(40_000 sat, Script.pay2wpkh(nonInitiatorKey.publicKey)), 0), + RemoteTxAddInput(UInt64(3), OutPoint(randomBytes32(), 0), TxOut(20_000 sat, Script.pay2wpkh(nonInitiatorKey.publicKey)), 0), + ), + localOutputs = Seq( + TxAddOutput(channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript), + TxAddOutput(channelId, UInt64(2), 15_000 sat, Script.write(Script.pay2wpkh(initiatorKey.publicKey))), + ), + remoteOutputs = Seq( + RemoteTxAddOutput(UInt64(1), 7_500 sat, Script.write(Script.pay2wpkh(nonInitiatorKey.publicKey))), + ), + 0 + ) + val unsignedTx = sharedTx.buildUnsignedTx() + val initiatorSigs = Seq( + Script.witnessPay2wpkh(initiatorKey.publicKey, Transaction.signInput(unsignedTx, 0, Script.pay2pkh(initiatorKey.publicKey), SigHash.SIGHASH_ALL, 120_000 sat, SigVersion.SIGVERSION_WITNESS_V0, initiatorKey)) + ) + val nonInitiatorSigs = Seq( + Script.witnessPay2wpkh(nonInitiatorKey.publicKey, Transaction.signInput(unsignedTx, 1, Script.pay2pkh(nonInitiatorKey.publicKey), SigHash.SIGHASH_ALL, 40_000 sat, SigVersion.SIGVERSION_WITNESS_V0, nonInitiatorKey)), + Script.witnessPay2wpkh(nonInitiatorKey.publicKey, Transaction.signInput(unsignedTx, 2, Script.pay2pkh(nonInitiatorKey.publicKey), SigHash.SIGHASH_ALL, 20_000 sat, SigVersion.SIGVERSION_WITNESS_V0, nonInitiatorKey)), + ) + // Valid signed transaction. + assert(InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, initiatorSigs)), TxSignatures(channelId, unsignedTx.txid, nonInitiatorSigs)).isRight) + // Invalid number of local signatures. + assert(InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, Nil)), TxSignatures(channelId, unsignedTx.txid, nonInitiatorSigs)) === Left(InvalidFundingSignature(channelId, Some(unsignedTx)))) + // Invalid number of remote signatures. + assert(InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, initiatorSigs)), TxSignatures(channelId, unsignedTx.txid, nonInitiatorSigs.tail)) === Left(InvalidFundingSignature(channelId, Some(unsignedTx)))) + // Invalid txid. + assert(InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, initiatorSigs)), TxSignatures(channelId, unsignedTx.hash, nonInitiatorSigs)) === Left(InvalidFundingSignature(channelId, Some(unsignedTx)))) + // Invalid signatures. + assert(InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, initiatorSigs)), TxSignatures(channelId, unsignedTx.txid, nonInitiatorSigs.reverse)) === Left(InvalidFundingSignature(channelId, Some(unsignedTx)))) + } + + test("validate final feerate") { + val channelId = randomBytes32() + val params = InteractiveTxParams(channelId, isInitiator = true, 75_000 sat, 50_000 sat, createFundingScript(), 0, 660 sat, FeeratePerKw(5000 sat)) + val initiatorKey = randomKey() + val nonInitiatorKey = randomKey() + val sharedTx = SharedTransaction( + localInputs = Seq(TxAddInput(channelId, UInt64(0), Transaction(2, Nil, Seq(TxOut(75_000 sat, Script.pay2wpkh(initiatorKey.publicKey))), 0), 0, 0)), + remoteInputs = Seq(RemoteTxAddInput(UInt64(1), OutPoint(randomBytes32(), 1), TxOut(50_000 sat, Script.pay2wpkh(nonInitiatorKey.publicKey)), 0)), + localOutputs = Seq(TxAddOutput(channelId, UInt64(0), params.fundingAmount, params.fundingPubkeyScript)), + remoteOutputs = Nil, + 0 + ) + val unsignedTx = sharedTx.buildUnsignedTx() + val initiatorSigs = Seq( + Script.witnessPay2wpkh(initiatorKey.publicKey, Transaction.signInput(unsignedTx, 0, Script.pay2pkh(initiatorKey.publicKey), SigHash.SIGHASH_ALL, 75_000 sat, SigVersion.SIGVERSION_WITNESS_V0, initiatorKey)) + ) + val nonInitiatorSigs = Seq( + Script.witnessPay2wpkh(nonInitiatorKey.publicKey, Transaction.signInput(unsignedTx, 1, Script.pay2pkh(nonInitiatorKey.publicKey), SigHash.SIGHASH_ALL, 50_000 sat, SigVersion.SIGVERSION_WITNESS_V0, nonInitiatorKey)), + ) + val Left(failure) = InteractiveTx.addRemoteSigs(params, PartiallySignedSharedTransaction(sharedTx, TxSignatures(channelId, unsignedTx.txid, initiatorSigs)), TxSignatures(channelId, unsignedTx.txid, nonInitiatorSigs)) + assert(failure === InvalidFundingFeerate(channelId, params.targetFeerate, FeeratePerKw(0 sat))) + } + } object InteractiveTxSpec { @@ -325,13 +391,9 @@ object InteractiveTxSpec { 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 + def computeFees(contributions: FundingContributions): Satoshi = { + val amountIn = contributions.inputs.map(i => i.previousTx.txOut(i.previousTxOutput.toInt).amount).sum + val amountOut = contributions.outputs.map(_.amount).sum amountIn - amountOut } 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 46beb8d4b3..ba9aa1f45c 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 @@ -175,12 +175,14 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight))(UInt64(150000000)) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(5000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(1000 sat) + .modify(_.requestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) val bobParams = Bob.channelParams .modify(_.initFeatures).setTo(bobInitFeatures) .modify(_.walletStaticPaymentBasepoint).setToIf(channelType.paysDirectlyToWallet)(Some(Helpers.getWalletPaymentBasepoint(wallet))) .modify(_.maxHtlcValueInFlightMsat).setToIf(tags.contains(ChannelStateTestsTags.NoMaxHtlcValueInFlight))(UInt64.MaxValue) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(1000 sat) .modify(_.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) + .modify(_.requestedChannelReserve_opt).setToIf(tags.contains(ChannelStateTestsTags.DualFunding))(None) (aliceParams, bobParams, channelType) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala new file mode 100644 index 0000000000..b76f8843cf --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -0,0 +1,283 @@ +/* + * 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, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{SigHash, SigVersion} +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmed +import fr.acinq.eclair.channel.InteractiveTx.{FundingContributions, PartiallySignedSharedTransaction} +import fr.acinq.eclair.channel.InteractiveTxSpec.createChangeScript +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.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.transactions.Transactions.{PlaceHolderPubKey, PlaceHolderSig} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, CommitSig, Error, Init, OpenDualFundedChannel, TxAddInput, TxAddOutput, TxComplete, TxSignatures} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64, randomBytes32, randomKey} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} +import scodec.bits.ByteVector + +import scala.concurrent.duration.DurationInt + +class WaitForDualFundingSignedStateSpec 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, alice2blockchain: TestProbe, bob2blockchain: TestProbe, aliceSigs: TxSignatures, bobSigs: TxSignatures, 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) + alice2blockchain.expectMsgType[SetChannelId] // temporary channel id + bob2blockchain.expectMsgType[SetChannelId] // temporary channel id + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[SetChannelId] // final channel id + bob2blockchain.expectMsgType[SetChannelId] // final channel id + + // Alice and Bob both contribute one input and one output. + val cid = channelId(bob) + val privKey = randomKey() + val fundingScript = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams.fundingPubkeyScript + val aliceInputAmount = TestConstants.fundingSatoshis + 75_000.sat + val aliceFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(TxAddInput(cid, UInt64(0), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 2), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(aliceInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(cid, UInt64(0), TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis, fundingScript), TxAddOutput(cid, UInt64(2), 60_000 sat, createChangeScript())), + )) + alice ! aliceFunding + val bobInputAmount = TestConstants.nonInitiatorFundingSatoshis + 25_000.sat + val bobFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(TxAddInput(cid, UInt64(1), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(bobInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(cid, UInt64(1), 20_000 sat, createChangeScript())), + )) + bob ! bobFunding + + // Alice and Bob go through the interactive tx construction. + alice2bob.expectMsgType[TxAddInput] + 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) + + val unsignedTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].fundingTx + val aliceSigs = { + val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, aliceInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) + TxSignatures(cid, unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) + } + val bobSigs = { + val sig = Transaction.signInput(unsignedTx, 1, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, bobInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) + TxSignatures(cid, unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) + } + withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceSigs, bobSigs, wallet))) + } + } + + test("recv CommitSig", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val aliceCommit = alice2bob.expectMsgType[CommitSig] + val bobCommit = bob2alice.expectMsgType[CommitSig] + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[ChannelSignatureReceived]) + + alice2bob.forward(bob, aliceCommit) + listener.expectMsgType[ChannelSignatureReceived] + alice2bob.expectNoMessage(100 millis) + bob2alice.forward(alice, bobCommit) + listener.expectMsgType[ChannelSignatureReceived] + bob2alice.expectNoMessage(100 millis) + } + + test("recv CommitSig (invalid sig)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val aliceCommit = alice2bob.expectMsgType[CommitSig] + val bobCommit = bob2alice.expectMsgType[CommitSig] + + alice2bob.forward(bob, aliceCommit.copy(signature = bobCommit.signature)) + bob2alice.expectMsgType[Error] + + bob2alice.forward(alice, bobCommit.copy(signature = aliceCommit.signature)) + alice2bob.expectMsgType[Error] + } + + test("recv TxSignatures", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val txListener = TestProbe() + system.eventStream.subscribe(txListener.ref, classOf[TransactionPublished]) + + val fundingTxId = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].fundingTx.txid + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + // Bob signs the funding transaction with his wallet. + bob ! PartiallySignedSharedTransaction(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx, bobSigs) + bob2alice.expectMsg(bobSigs) + bob2alice.forward(alice) + bob2alice.expectNoMessage(100 millis) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId === fundingTxId) + bob2blockchain.expectNoMessage(100 millis) + txListener.expectNoMessage(100 millis) + awaitCond(bob.stateName === WAIT_FOR_DUAL_FUNDING_CONFIRMED) + + // Alice signs the funding transaction with her wallet. + alice ! PartiallySignedSharedTransaction(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx, aliceSigs) + alice2bob.expectMsg(aliceSigs) + alice2bob.expectNoMessage(100 millis) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId === fundingTxId) + alice2blockchain.expectNoMessage(100 millis) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelOpened] + assert(txListener.expectMsgType[TransactionPublished].tx.txid === fundingTxId) + awaitCond(bob.stateName === WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + + test("recv TxSignatures (before CommitSig)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[CommitSig] + bob2alice.expectMsgType[CommitSig] + + alice ! bobSigs + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! aliceSigs + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + test("recv TxSignatures (invalid sig)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + val invalidBobSigs = bobSigs.copy(witnesses = aliceSigs.witnesses) + alice ! invalidBobSigs + alice ! PartiallySignedSharedTransaction(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx, aliceSigs) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + } + + test("recv Status.Failure (wallet error)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + alice ! Status.Failure(new RuntimeException("could not sign inputs")) + alice2bob.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! Status.Failure(new RuntimeException("could not sign inputs")) + bob2alice.expectMsgType[Error] + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice ! Error(channelId(alice), "oops") + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[Status.Failure] + + bob ! Error(channelId(bob), "oops") + awaitCond(wallet.rolledback.size == 2) + awaitCond(bob.stateName == CLOSED) + } + + 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, channelId(alice))) + awaitCond(wallet.rolledback.size == 1) + awaitCond(alice.stateName == CLOSED) + aliceOrigin.expectMsgType[ChannelOpenResponse.ChannelClosed] + + bob ! c + sender.expectMsg(RES_SUCCESS(c, channelId(bob))) + 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] + } + +} From 728344aa203434af8d9512ef065584093c625b08 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 9 May 2022 18:10:56 +0200 Subject: [PATCH 9/9] Wait for dual funding tx confirmation We wait for the funding tx to confirm and handle various error cases. We avoid force-closing while we don't know which funding tx will confirm. Instead we just wait for either one of the funding txs to confirm or all funding attempts to be double-spent, which we check regularly. --- .../acinq/eclair/balance/CheckBalance.scala | 2 + .../fr/acinq/eclair/channel/ChannelData.scala | 7 + .../eclair/channel/ChannelExceptions.scala | 1 + .../fr/acinq/eclair/channel/Commitments.scala | 12 +- .../fr/acinq/eclair/channel/Helpers.scala | 18 +- .../acinq/eclair/channel/InteractiveTx.scala | 12 + .../fr/acinq/eclair/channel/fsm/Channel.scala | 85 ++++-- .../channel/fsm/ChannelOpenDualFunded.scala | 134 ++++++++- .../channel/fsm/DualFundingHandlers.scala | 43 ++- .../eclair/channel/fsm/ErrorHandlers.scala | 33 ++- .../fr/acinq/eclair/db/DbEventHandler.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 16 +- .../channel/version3/ChannelCodecs3.scala | 21 +- .../wire/protocol/LightningMessageTypes.scala | 4 + .../scala/fr/acinq/eclair/TestDatabases.scala | 1 + .../blockchain/DummyOnChainWallet.scala | 3 +- .../eclair/channel/InteractiveTxSpec.scala | 4 + .../publish/ReplaceableTxPublisherSpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 190 ++++++++++-- .../a/WaitForAcceptChannelStateSpec.scala | 4 +- ...tForAcceptDualFundedChannelStateSpec.scala | 6 +- .../a/WaitForOpenChannelStateSpec.scala | 2 +- ...aitForOpenDualFundedChannelStateSpec.scala | 2 +- .../WaitForDualFundingCreatedStateSpec.scala | 2 +- .../WaitForDualFundingInternalStateSpec.scala | 2 +- .../b/WaitForDualFundingSignedStateSpec.scala | 53 +--- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingInternalStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- ...WaitForDualFundingConfirmedStateSpec.scala | 280 ++++++++++++++++++ .../c/WaitForDualFundingLockedStateSpec.scala | 156 ++++++++++ .../c/WaitForFundingConfirmedStateSpec.scala | 4 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 23 +- .../integration/ChannelIntegrationSpec.scala | 20 ++ .../eclair/integration/IntegrationSpec.scala | 4 + 36 files changed, 996 insertions(+), 160 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingLockedStateSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index 8dd65b359c..8e10d7116d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -202,6 +202,8 @@ object CheckBalance { .foldLeft(OffChainBalance()) { case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) case (r, d: DATA_WAIT_FOR_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit)) + case (r, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit)) + case (r, d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit)) case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages)) case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.localCommit)) 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 e013a6aa49..60cafc6a61 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 @@ -63,6 +63,7 @@ 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 case object WAIT_FOR_DUAL_FUNDING_CONFIRMED extends ChannelState +case object WAIT_FOR_DUAL_FUNDING_LOCKED extends ChannelState // Channel opened: case object NORMAL extends ChannelState case object SHUTDOWN extends ChannelState @@ -487,9 +488,15 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_SIGNED(channelId: ByteVector32, } final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: SignedSharedTransaction, + fundingParams: InteractiveTxParams, previousFundingTxs: Seq[DualFundingTx], waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm + lastChecked: BlockHeight, // last time we checked if the channel was double-spent deferred: Option[FundingLocked]) extends PersistentChannelData +final case class DATA_WAIT_FOR_DUAL_FUNDING_LOCKED(commitments: Commitments, + shortChannelId: ShortChannelId, + otherFundingTxs: Seq[DualFundingTx], + lastSent: FundingLocked) extends PersistentChannelData 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 0496d2ed0c..0b0ec3644d 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 @@ -74,6 +74,7 @@ case class ChannelUnavailable (override val channelId: Byte case class InvalidFinalScript (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid final script") case class MissingUpfrontShutdownScript (override val channelId: ByteVector32) extends ChannelException(channelId, "missing upfront shutdown script") case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out") +case class FundingTxDoubleSpent (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx double spent") case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}") case class HtlcsTimedoutDownstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out downstream: ids=${htlcs.take(10).map(_.id).mkString(",")}") // we only display the first 10 ids case class HtlcsWillTimeoutUpstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs that should be fulfilled are close to timing out upstream: ids=${htlcs.take(10).map(_.id).mkString}") // we only display the first 10 ids 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 999dbecd4e..53b6fab123 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 @@ -212,10 +212,18 @@ case class Commitments(channelId: ByteVector32, val capacity: Satoshi = commitInput.txOut.amount /** Channel reserve that applies to our funds. */ - val localChannelReserve: Satoshi = remoteParams.requestedChannelReserve + val localChannelReserve: Satoshi = if (channelFeatures.hasFeature(Features.DualFunding)) { + (capacity / 100).max(remoteParams.dustLimit) + } else { + remoteParams.requestedChannelReserve + } /** Channel reserve that applies to our peer's funds. */ - val remoteChannelReserve: Satoshi = localParams.requestedChannelReserve + val remoteChannelReserve: Satoshi = if (channelFeatures.hasFeature(Features.DualFunding)) { + (capacity / 100).max(localParams.dustLimit) + } else { + 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 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 f86e7b9364..00bcd4eb6d 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 @@ -58,6 +58,7 @@ object Helpers { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = commitments1) case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.copy(commitments = commitments1) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = commitments1) + case d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED => d.copy(commitments = commitments1) case d: DATA_NORMAL => d.copy(commitments = commitments1) case d: DATA_SHUTDOWN => d.copy(commitments = commitments1) case d: DATA_NEGOTIATING => d.copy(commitments = commitments1) @@ -512,12 +513,17 @@ object Helpers { * * @return true if channel was never open, or got closed immediately, had never any htlcs and local never had a positive balance */ - def nothingAtStake(data: PersistentChannelData): Boolean = - data.commitments.localCommit.index == 0 && - data.commitments.localCommit.spec.toLocal == 0.msat && - data.commitments.remoteCommit.index == 0 && - data.commitments.remoteCommit.spec.toRemote == 0.msat && - data.commitments.remoteNextCommitInfo.isRight + def nothingAtStake(data: PersistentChannelData): Boolean = data match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => (d.commitments +: d.previousFundingTxs.map(_.commitments)).forall(commitments => nothingAtStake(commitments)) + case _ => nothingAtStake(data.commitments) + } + + def nothingAtStake(commitments: Commitments): Boolean = + commitments.localCommit.index == 0 && + commitments.localCommit.spec.toLocal == 0.msat && + commitments.remoteCommit.index == 0 && + commitments.remoteCommit.spec.toRemote == 0.msat && + commitments.remoteNextCommitInfo.isRight /** * As soon as a tx spending the funding tx has reached min_depth, we know what the closing type will be, before 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 index 1b00043bba..d08b4a43a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTx.scala @@ -102,6 +102,11 @@ object InteractiveTx { val totalAmountIn: Satoshi = localAmountIn + remoteAmountIn val fees: Satoshi = totalAmountIn - localOutputs.map(_.amount).sum - remoteOutputs.map(_.amount).sum + def localFees(params: InteractiveTxParams): Satoshi = { + val localAmountOut = params.localAmount + localOutputs.filter(_.pubkeyScript != params.fundingPubkeyScript).map(_.amount).sum + localAmountIn - localAmountOut + } + def buildUnsignedTx(): Transaction = { val localTxIn = localInputs.map(i => (i.serialId, TxIn(toOutPoint(i), ByteVector.empty, i.sequence))) val remoteTxIn = remoteInputs.map(i => (i.serialId, TxIn(i.outPoint, ByteVector.empty, i.sequence))) @@ -331,6 +336,13 @@ object InteractiveTx { Transaction(2, inputs, outputs, 0) } + /** Return a dummy transaction containing local contributions from every given transaction. */ + def dummyLocalTx(sharedTxs: Seq[SharedTransaction]): Transaction = { + val inputs = sharedTxs.flatMap(_.localInputs).distinctBy(_.serialId).map(i => TxIn(toOutPoint(i), ByteVector.empty, i.sequence)) + val outputs = sharedTxs.flatMap(_.localOutputs).distinctBy(_.serialId).map(o => TxOut(o.amount, o.pubkeyScript)) + Transaction(2, inputs, outputs, 0) + } + private def spendSameOutpoint(input1: TxAddInput, input2: TxAddInput): Boolean = { input1.previousTx.txid == input2.previousTx.txid && input1.previousTxOutput == input2.previousTxOutput } 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 2b35cc6e44..aed91fcab1 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 @@ -137,6 +137,7 @@ object Channel { private[channel] sealed trait BitcoinEvent extends PossiblyHarmful private[channel] case object BITCOIN_FUNDING_PUBLISH_FAILED extends BitcoinEvent private[channel] case object BITCOIN_FUNDING_TIMEOUT extends BitcoinEvent + private[channel] case class BITCOIN_FUNDING_DOUBLE_SPENT(fundingTxIds: Set[ByteVector32]) extends BitcoinEvent // @formatter:on case object TickChannelOpenTimeout @@ -166,7 +167,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val with ChannelOpenSingleFunder with ChannelOpenDualFunded with CommonHandlers - with SingleFundingHandlers with ErrorHandlers { import Channel._ @@ -321,7 +321,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // if commitment number is zero, we also need to make sure that the funding tx has been published if (closing.commitments.localCommit.index == 0 && closing.commitments.remoteCommit.index == 0) { - blockchain ! GetTxWithMeta(self, closing.commitments.commitInput.outPoint.txid) + if (closing.commitments.channelFeatures.hasFeature(Features.DualFunding)) { + closing.fundingTx.foreach(tx => wallet.publishTransaction(tx)) + } else { + blockchain ! GetTxWithMeta(self, closing.commitments.commitInput.outPoint.txid) + } } } // no need to go OFFLINE, we can directly switch to CLOSING @@ -362,6 +366,12 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val goto(OFFLINE) using funding } + case funding: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => + // we make sure that the funding tx with the highest feerate has been published + // NB: with dual-funding, we only watch the funding tx once it has been confirmed + publishFundingTx(funding) + goto(OFFLINE) using funding + case _ => watchFundingTx(data.commitments) goto(OFFLINE) using data @@ -1304,9 +1314,6 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val goto(SYNCING) using d1 sending channelReestablish - // note: this can only happen if state is NORMAL or SHUTDOWN - // -> in NEGOTIATING there are no more htlcs - // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway case Event(ProcessCurrentBlockHeight(c), d: PersistentChannelData) => handleNewBlock(c, d) case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) @@ -1321,6 +1328,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingTimeout(d) + case Event(e: BITCOIN_FUNDING_DOUBLE_SPENT, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleDualFundingDoubleSpent(e, d) + // just ignore this, we will put a new watch when we reconnect, and we'll be notified again case Event(WatchFundingConfirmedTriggered(_, _, _), _) => stay() @@ -1351,6 +1360,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + val minDepth = Helpers.minDepthForFunding(nodeParams.channelConf, d.commitments.commitInput.txOut.amount) + (d.commitments +: d.previousFundingTxs.map(_.commitments)).foreach(commitments => blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, minDepth)) + goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => log.debug("re-sending fundingLocked") val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) @@ -1358,6 +1372,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) goto(WAIT_FOR_FUNDING_LOCKED) sending fundingLocked + case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) => + log.debug("re-sending fundingLocked") + val channelKeyPath = keyManager.keyPath(d.commitments.localParams, d.commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + val fundingLocked = FundingLocked(d.commitments.channelId, nextPerCommitmentPoint) + goto(WAIT_FOR_DUAL_FUNDING_LOCKED) sending fundingLocked + case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) => Syncing.checkSync(keyManager, d, channelReestablish) match { case syncFailure: SyncResult.Failure => @@ -1650,7 +1671,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match { case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) - case (WAIT_FOR_FUNDING_LOCKED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) + case (WAIT_FOR_FUNDING_LOCKED | WAIT_FOR_DUAL_FUNDING_LOCKED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate(d)) case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) case (NORMAL, OFFLINE, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate(d2)) @@ -1674,6 +1695,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2) // WAIT_FOR_FUNDING_LOCKED->NORMAL case (_: DATA_WAIT_FOR_FUNDING_LOCKED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) + case (_: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2) case _ => () } } @@ -1874,31 +1896,38 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val } private def handleNewBlock(c: CurrentBlockHeight, d: PersistentChannelData) = { - val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockHeight) - val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.channelConf.fulfillSafetyBeforeTimeout) - if (timedOutOutgoing.nonEmpty) { - // Downstream timed out. - handleLocalError(HtlcsTimedoutDownstream(d.channelId, timedOutOutgoing), d, Some(c)) - } else if (almostTimedOutIncoming.nonEmpty) { - // Upstream is close to timing out, we need to test if we have funds at risk: htlcs for which we know the preimage - // that are still in our commitment (upstream will try to timeout on-chain). - val relayedFulfills = d.commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.id }.toSet - val offendingRelayedHtlcs = almostTimedOutIncoming.filter(htlc => relayedFulfills.contains(htlc.id)) - if (offendingRelayedHtlcs.nonEmpty) { - handleLocalError(HtlcsWillTimeoutUpstream(d.channelId, offendingRelayedHtlcs), d, Some(c)) - } else { - // There might be pending fulfill commands that we haven't relayed yet. - // Since this involves a DB call, we only want to check it if all the previous checks failed (this is the slow path). - val pendingRelayFulfills = nodeParams.db.pendingCommands.listSettlementCommands(d.channelId).collect { case c: CMD_FULFILL_HTLC => c.id } - val offendingPendingRelayFulfills = almostTimedOutIncoming.filter(htlc => pendingRelayFulfills.contains(htlc.id)) - if (offendingPendingRelayFulfills.nonEmpty) { - handleLocalError(HtlcsWillTimeoutUpstream(d.channelId, offendingPendingRelayFulfills), d, Some(c)) + d match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => handleNewBlockDualFundingUnconfirmed(c, d) + case _ => + // note: this can only happen if state is NORMAL or SHUTDOWN + // -> in NEGOTIATING there are no more htlcs + // -> in CLOSING we either have mutual closed (so no more htlcs), or already have unilaterally closed (so no action required), and we can't be in OFFLINE state anyway + val timedOutOutgoing = d.commitments.timedOutOutgoingHtlcs(c.blockHeight) + val almostTimedOutIncoming = d.commitments.almostTimedOutIncomingHtlcs(c.blockHeight, nodeParams.channelConf.fulfillSafetyBeforeTimeout) + if (timedOutOutgoing.nonEmpty) { + // Downstream timed out. + handleLocalError(HtlcsTimedoutDownstream(d.channelId, timedOutOutgoing), d, Some(c)) + } else if (almostTimedOutIncoming.nonEmpty) { + // Upstream is close to timing out, we need to test if we have funds at risk: htlcs for which we know the preimage + // that are still in our commitment (upstream will try to timeout on-chain). + val relayedFulfills = d.commitments.localChanges.all.collect { case u: UpdateFulfillHtlc => u.id }.toSet + val offendingRelayedHtlcs = almostTimedOutIncoming.filter(htlc => relayedFulfills.contains(htlc.id)) + if (offendingRelayedHtlcs.nonEmpty) { + handleLocalError(HtlcsWillTimeoutUpstream(d.channelId, offendingRelayedHtlcs), d, Some(c)) + } else { + // There might be pending fulfill commands that we haven't relayed yet. + // Since this involves a DB call, we only want to check it if all the previous checks failed (this is the slow path). + val pendingRelayFulfills = nodeParams.db.pendingCommands.listSettlementCommands(d.channelId).collect { case c: CMD_FULFILL_HTLC => c.id } + val offendingPendingRelayFulfills = almostTimedOutIncoming.filter(htlc => pendingRelayFulfills.contains(htlc.id)) + if (offendingPendingRelayFulfills.nonEmpty) { + handleLocalError(HtlcsWillTimeoutUpstream(d.channelId, offendingPendingRelayFulfills), d, Some(c)) + } else { + stay() + } + } } else { stay() } - } - } else { - stay() } } 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 397bd04828..48adb72ef7 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,23 +16,26 @@ package fr.acinq.eclair.channel.fsm -import akka.actor.Status import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} +import akka.actor.{ActorRef, Status} import akka.pattern.pipe -import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmed -import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.channel.InteractiveTx.{InteractiveTxParams, PartiallySignedSharedTransaction} +import fr.acinq.bitcoin.ScriptFlags +import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script, Transaction} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel.Helpers.{Funding, getRelayFees} +import fr.acinq.eclair.channel.InteractiveTx.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.fsm.Channel._ import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.crypto.ShaChain +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._ -import fr.acinq.eclair.{Features, MilliSatoshiLong, randomKey} +import fr.acinq.eclair.{Features, MilliSatoshiLong, ShortChannelId, ToMilliSatoshiConversion, randomKey} -import scala.util.{Failure, Success} +import scala.concurrent.duration.DurationInt +import scala.util.{Failure, Success, Try} /** * Created by t-bast on 19/04/2022. @@ -235,10 +238,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { 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 - } + goto(WAIT_FOR_DUAL_FUNDING_CREATED) using nextData sending msg_opt case Event(InteractiveTxFunder.FundingFailed(t), d: DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL) => log.error(t, s"could not fund dual-funded channel: ") @@ -321,7 +321,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { when(WAIT_FOR_DUAL_FUNDING_SIGNED)(handleExceptions { case Event(commit: CommitSig, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => - Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.fundingAmount, 0 msat, d.commitTxFeerate, d.fundingTx.hash, d.fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { + Funding.makeFirstCommitTxs(keyManager, d.channelConfig, d.channelFeatures, d.channelId, d.localParams, d.remoteParams, d.fundingParams.localAmount, d.fundingParams.remoteAmount, 0 msat, d.commitTxFeerate, d.fundingTx.hash, d.fundingOutputIndex, d.remoteFirstPerCommitmentPoint) match { case Left(cause) => handleInteractiveTxError(d.fundingTx, cause, d, Some(commit)) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => val fundingPubKey = keyManager.fundingPublicKey(d.localParams.fundingKeyPath) @@ -375,7 +375,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { blockchain ! WatchFundingConfirmed(self, d.fundingTx.txid, fundingMinDepth) // NB: we publish the funding tx only *after* the channel state has been written to disk because we want // to make sure we first persist the commitment that returns back the funds to us in case of problem - val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, signedTx, Nil, nodeParams.currentBlockHeight, None) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, signedTx, d.fundingParams, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, None) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using nextData storing() sending partiallySignedTx.localSigs calling publishFundingTx(nextData) } case None => @@ -383,7 +383,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { // signed funding transaction yet. Our peer may publish the funding transaction without explicitly sending // us their signatures. blockchain ! WatchFundingConfirmed(self, d.fundingTx.txid, fundingMinDepth) - val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, partiallySignedTx, Nil, nodeParams.currentBlockHeight, None) + val nextData = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, partiallySignedTx, d.fundingParams, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, None) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using nextData storing() sending partiallySignedTx.localSigs } case None => @@ -417,7 +417,109 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_DUAL_FUNDING_CONFIRMED)(handleExceptions { - case Event(msg, d) => ??? + case Event(txSigs: TxSignatures, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + d.fundingTx match { + case fundingTx: PartiallySignedSharedTransaction => InteractiveTx.addRemoteSigs(d.fundingParams, fundingTx, txSigs) match { + case Left(cause) => + log.warning("received invalid tx_signatures: {}", cause.getMessage) + // The funding transaction may still confirm, so we cannot close the channel yet. + stay() sending Error(d.channelId, InvalidFundingSignature(d.channelId, Some(d.fundingTx.tx.buildUnsignedTx())).getMessage) + case Right(fundingTx) => + log.info("publishing funding tx for channelId={} fundingTxId={}", d.channelId, fundingTx.signedTx.txid) + val nextData = d.copy(fundingTx = fundingTx) + stay() using nextData storing() calling publishFundingTx(nextData) + } + case _: FullySignedSharedTransaction => + log.info("received duplicate tx_signatures") + stay() + } + + case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, confirmedTx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + // We find which funding transaction got confirmed. + val allFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs + allFundingTxs.find(_.commitments.commitInput.outPoint.txid == confirmedTx.txid) match { + case Some(DualFundingTx(_, commitments)) => + Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(confirmedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { + case Success(_) => + log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex with funding txid=${commitments.commitInput.outPoint.txid}") + val commitTxs = Set(commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitments.remoteCommit.txid) + blockchain ! WatchFundingSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitTxs) + context.system.eventStream.publish(TransactionConfirmed(commitments.channelId, remoteNodeId, confirmedTx)) + val channelKeyPath = keyManager.keyPath(commitments.localParams, commitments.channelConfig) + val nextPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 1) + val fundingLocked = FundingLocked(commitments.channelId, nextPerCommitmentPoint) + d.deferred.foreach(self ! _) + val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) + val otherFundingTxs = allFundingTxs.filter(_.commitments.commitInput.outPoint.txid != confirmedTx.txid) + if (otherFundingTxs.nonEmpty) { + wallet.rollback(InteractiveTx.dummyLocalTx(otherFundingTxs.map(_.fundingTx.tx))) + } + goto(WAIT_FOR_DUAL_FUNDING_LOCKED) using DATA_WAIT_FOR_DUAL_FUNDING_LOCKED(commitments, shortChannelId, otherFundingTxs, fundingLocked) storing() sending fundingLocked + case Failure(t) => + log.error(t, s"rejecting channel with invalid funding tx: ${confirmedTx.bin}") + allFundingTxs.foreach(f => wallet.rollback(f.fundingTx.tx.buildUnsignedTx())) + goto(CLOSED) + } + case None => + log.error(s"rejecting channel with invalid funding tx that doesn't match any of our funding txs: ${confirmedTx.bin}") + allFundingTxs.foreach(f => wallet.rollback(f.fundingTx.tx.buildUnsignedTx())) + goto(CLOSED) + } + + case Event(ProcessCurrentBlockHeight(c), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleNewBlockDualFundingUnconfirmed(c, d) + + case Event(e: BITCOIN_FUNDING_DOUBLE_SPENT, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleDualFundingDoubleSpent(e, d) + + case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + log.info("our peer wants to raise the feerate of the funding transaction (target={})", msg.feerate) + stay() sending TxAbort(d.channelId, "rbf not supported yet") + + case Event(msg: FundingLocked, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + log.info("received their funding_locked, deferring message") + stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(c: CMD_FORCECLOSE, d) => + // We can't easily force-close until we know which funding transaction confirms. + // A better option would be to double-spend the funding transaction(s). + log.warning("cannot force-close while dual-funded transactions are unconfirmed") + val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo + replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "force-close", stateName)) + stay() + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleRemoteError(e, d) + }) + + when(WAIT_FOR_DUAL_FUNDING_LOCKED)(handleExceptions { + case Event(FundingLocked(_, nextPerCommitmentPoint, _), d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) => + // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + context.system.eventStream.publish(ShortChannelIdAssigned(self, d.commitments.channelId, d.shortChannelId, None)) + // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced + val fees = getRelayFees(nodeParams, remoteNodeId, d.commitments) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network + context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) + goto(NORMAL) using DATA_NORMAL(d.commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), d.shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() + + case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) if d.commitments.announceChannel => + log.debug("received remote announcement signatures, delaying") + // we may receive their announcement sigs before our watcher notifies us that the channel has reached min_conf (especially during testing when blocks are generated in bulk) + // note: no need to persist their message, in case of disconnection they will resend it + context.system.scheduler.scheduleOnce(2 seconds, self, remoteAnnSigs) + stay() + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d) + + case Event(WatchFundingSpentTriggered(tx), d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) => handleInformationLeak(tx, d) + + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED) => handleRemoteError(e, d) }) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index ddf22c8b49..6a821b6267 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -16,8 +16,13 @@ package fr.acinq.eclair.channel.fsm +import fr.acinq.eclair.blockchain.CurrentBlockHeight +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel.BITCOIN_FUNDING_DOUBLE_SPENT +import fr.acinq.eclair.wire.protocol.Error +import scala.concurrent.Future import scala.util.{Failure, Success} /** @@ -40,13 +45,47 @@ trait DualFundingHandlers extends CommonHandlers { // to publish and we may be able to RBF. wallet.publishTransaction(fundingTx.signedTx).onComplete { case Success(_) => - context.system.eventStream.publish(TransactionPublished(d.commitments.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.fees, "funding")) + context.system.eventStream.publish(TransactionPublished(d.commitments.channelId, remoteNodeId, fundingTx.signedTx, fundingTx.tx.localFees(d.fundingParams), "funding")) channelOpenReplyToUser(Right(ChannelOpenResponse.ChannelOpened(d.commitments.channelId))) case Failure(t) => channelOpenReplyToUser(Left(LocalError(t))) - log.error(t, "error while committing funding tx: ") // tx may be published by our peer, we can't fail-fast + log.warning("error while publishing funding tx: {}", t.getMessage) // tx may be published by our peer, we can't fail-fast } } } + def handleNewBlockDualFundingUnconfirmed(c: CurrentBlockHeight, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { + if (Channel.FUNDING_TIMEOUT_FUNDEE < c.blockHeight - d.waitingSince && Closing.nothingAtStake(d)) { + log.warning("funding transaction did not confirm in {} blocks and we have nothing at stake, forgetting channel", Channel.FUNDING_TIMEOUT_FUNDEE) + handleFundingTimeout(d) + } else if (d.lastChecked + 6 < c.blockHeight) { + log.debug("checking if funding transactions have been double-spent") + val fundingTxs = (d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)).map(_.tx.buildUnsignedTx()) + // We check whether *all* funding attempts have been double-spent. + // Since we only consider a transaction double-spent when the spending transaction is confirmed, this will not + // return false positives when one of our transactions is confirmed, because its individual result will be false. + Future.sequence(fundingTxs.map(tx => wallet.doubleSpent(tx))).map(_.forall(_ == true)).map { + case true => self ! BITCOIN_FUNDING_DOUBLE_SPENT(fundingTxs.map(_.txid).toSet) + case false => publishFundingTx(d) // we republish the highest feerate funding transaction + } + stay() using d.copy(lastChecked = c.blockHeight) storing() + } else { + stay() + } + } + + def handleDualFundingDoubleSpent(e: BITCOIN_FUNDING_DOUBLE_SPENT, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { + val fundingTxIds = (d.commitments +: d.previousFundingTxs.map(_.commitments)).map(_.commitInput.outPoint.txid).toSet + if (fundingTxIds.subsetOf(e.fundingTxIds)) { + log.warning("{} funding attempts have been double-spent, forgetting channel", fundingTxIds.size) + (d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)).foreach(tx => wallet.rollback(tx.tx.buildUnsignedTx())) + channelOpenReplyToUser(Left(LocalError(FundingTxDoubleSpent(d.channelId)))) + goto(CLOSED) sending Error(d.channelId, FundingTxDoubleSpent(d.channelId).getMessage) + } else { + // Not all funding attempts have been double-spent, the channel may still confirm. + // For example, we may have published an RBF attempt while we were checking if funding attempts were double-spent. + stay() + } + } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 30d8735e02..b2a0cf2750 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -134,6 +134,10 @@ trait ErrorHandlers extends CommonHandlers { // if we were in the process of closing and already received a closing sig from the counterparty, it's always better to use that handleMutualClose(bestUnpublishedClosingTx, Left(negotiating)) case d: DATA_WAIT_FOR_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // the channel was never used and the funding tx may be double-spent + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) // we didn't add funds to any of the funding attempts, so we can simply forget about the channel + case _: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => + log.info("cannot close channel while dual-funding txs are unconfirmed: waiting for a transaction to confirm or be double-spent") + stay() case hasCommitments: PersistentChannelData => spendLocalCurrent(hasCommitments) // NB: we publish the commitment even if we have nothing at stake (in a dataloss situation our peer will send us an error just for that) case _: TransientChannelData => goto(CLOSED) // when there is no commitment yet, we just go to CLOSED state in case an error occurs } @@ -182,15 +186,22 @@ trait ErrorHandlers extends CommonHandlers { log.warning("we have an outdated commitment: will not publish our local tx") stay() } else { - val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - val nextData = d match { - case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) - case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) - case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) - case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + d match { + case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED if Closing.nothingAtStake(d) => goto(CLOSED) + case _: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => + log.info("cannot spend our commitment while dual-funding txs are unconfirmed: waiting for a transaction to confirm or be double-spent") + stay() + case _ => + val commitTx = d.commitments.fullySignedLocalCommitTx(keyManager).tx + val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, d.commitments, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextData = d match { + case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) + case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) + } + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) } - goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, d.commitments) } } @@ -233,6 +244,12 @@ trait ErrorHandlers extends CommonHandlers { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) + case waitForFundingConfirmed: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => + val dualFundedTx_opt = waitForFundingConfirmed.fundingTx match { + case _: InteractiveTx.PartiallySignedSharedTransaction => None + case tx: InteractiveTx.FullySignedSharedTransaction => Some(tx.signedTx) + } + DATA_CLOSING(d.commitments, fundingTx = dualFundedTx_opt, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index e112fea7b6..a0278d2d1f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -108,7 +108,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with DiagnosticActorL case e: ChannelStateChanged => // NB: order matters! e match { - case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => + case ChannelStateChanged(_, channelId, _, remoteNodeId, WAIT_FOR_FUNDING_LOCKED | WAIT_FOR_DUAL_FUNDING_LOCKED, NORMAL, Some(commitments: Commitments)) => ChannelMetrics.ChannelLifecycleEvents.withTag(ChannelTags.Event, ChannelTags.Events.Created).increment() val event = ChannelEvent.EventType.Created auditDb.add(ChannelEvent(channelId, remoteNodeId, commitments.capacity, commitments.localParams.isInitiator, !commitments.announceChannel, event)) 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 1acbcff540..804983e995 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 @@ -36,7 +36,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Feature, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} +import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature} import org.json4s import org.json4s.JsonAST._ import org.json4s.jackson.Serialization @@ -151,6 +151,10 @@ object CltvExpiryDeltaSerializer extends MinimalSerializer({ case x: CltvExpiryDelta => JInt(x.toInt) }) +object BlockHeightSerializer extends MinimalSerializer({ + case h: BlockHeight => JLong(h.toLong) +}) + object FeeratePerKwSerializer extends MinimalSerializer({ case x: FeeratePerKw => JLong(x.toLong) }) @@ -489,8 +493,15 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], - classOf[DATA_WAIT_FOR_FUNDING_LOCKED], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], + classOf[DATA_WAIT_FOR_FUNDING_LOCKED], + classOf[DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL], + classOf[DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL], + classOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL], + classOf[DATA_WAIT_FOR_DUAL_FUNDING_CREATED], + classOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED], + classOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED], + classOf[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED], classOf[DATA_NORMAL], classOf[DATA_SHUTDOWN], classOf[DATA_NEGOTIATING], @@ -519,6 +530,7 @@ object JsonSerializers { BtcSerializer + SatoshiSerializer + MilliSatoshiSerializer + + BlockHeightSerializer + CltvExpirySerializer + CltvExpiryDeltaSerializer + FeeratePerKwSerializer + 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 e7c31876ba..b3224f8ec8 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 @@ -338,7 +338,7 @@ private[channel] object ChannelCodecs3 { private val remoteTxAddOutputCodec: Codec[RemoteTxAddOutput] = ( ("serialId" | uint64) :: ("amount" | satoshi) :: - ("scriptPubKey" | variableSizeBytes(uint16, bytes))).as[RemoteTxAddOutput] + ("scriptPubKey" | lengthDelimited(bytes))).as[RemoteTxAddOutput] private val sharedTransactionCodec: Codec[SharedTransaction] = ( ("localInputs" | seqOfN(uint16, lengthDelimited(txAddInputCodec))) :: @@ -364,13 +364,31 @@ private[channel] object ChannelCodecs3 { ("fundingTx" | signedSharedTransactionCodec) :: ("commitments" | commitmentsCodec)).as[DualFundingTx] + private val fundingParamsCodec: Codec[InteractiveTxParams] = ( + ("channelId" | bytes32) :: + ("isInitiator" | bool8) :: + ("localAmount" | satoshi) :: + ("remoteAmount" | satoshi) :: + ("fundingPubkeyScript" | lengthDelimited(bytes)) :: + ("lockTime" | uint32) :: + ("dustLimit" | satoshi) :: + ("targetFeerate" | feeratePerKw)).as[InteractiveTxParams] + val DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | signedSharedTransactionCodec) :: + ("fundingParams" | fundingParamsCodec) :: ("previousFundingTxs" | seqOfN(uint16, dualFundingTxCodec)) :: ("waitingSince" | blockHeight) :: + ("lastChecked" | blockHeight) :: ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val DATA_WAIT_FOR_DUAL_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED] = ( + ("commitments" | commitmentsCodec) :: + ("shortChannelId" | shortchannelid) :: + ("otherFundingTxs" | seqOfN(uint16, dualFundingTxCodec)) :: + ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED] + val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: @@ -430,6 +448,7 @@ private[channel] object ChannelCodecs3 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x0a, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_LOCKED_Codec) .typecase(0x09, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED_Codec) .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) .typecase(0x07, Codecs.DATA_NORMAL_Codec) 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 65ec641c98..ede5a0de97 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 @@ -123,6 +123,10 @@ case class TxAbort(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[TxAbortTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId +object TxAbort { + def apply(channelId: ByteVector32, msg: String): TxAbort = TxAbort(channelId, ByteVector.view(msg.getBytes(Charsets.US_ASCII))) +} + case class ChannelReestablish(channelId: ByteVector32, nextLocalCommitmentNumber: Long, nextRemoteRevocationNumber: Long, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 1212fdbd50..05415b1f1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -71,6 +71,7 @@ object TestDatabases { case d: DATA_WAIT_FOR_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.copy(commitments = freeze2(d.commitments)) + case d: DATA_WAIT_FOR_DUAL_FUNDING_LOCKED => d.copy(commitments = freeze2(d.commitments)) case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => d.copy(commitments = freeze2(d.commitments)) case d: DATA_NORMAL => d.copy(commitments = freeze2(d.commitments)) case d: DATA_CLOSING => d.copy(commitments = freeze2(d.commitments)) 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 2af77eca29..ca0f2f6651 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 @@ -67,6 +67,7 @@ class NoOpOnChainWallet extends OnChainWallet { import DummyOnChainWallet._ var rolledback = Seq.empty[Transaction] + var doubleSpent = Set.empty[ByteVector32] override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) @@ -91,7 +92,7 @@ class NoOpOnChainWallet extends OnChainWallet { Future.successful(true) } - override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false) + override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid)) } 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 index 04c1e1ba9f..8f047dfddf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxSpec.scala @@ -90,6 +90,8 @@ class InteractiveTxSpec extends AnyFunSuiteLike { assert(tx.txIn.length === 2) assert(tx.txOut.length === 1) assert(tx.txOut.head === TxOut(initiatorParams.fundingAmount, fundingScript)) + assert(initiatorTx.localFees(initiatorParams) === 5000.sat) + assert(nonInitiatorTx.localFees(nonInitiatorParams) === 0.sat) } test("initiator and non-initiator") { @@ -161,6 +163,8 @@ class InteractiveTxSpec extends AnyFunSuiteLike { assert(tx.txIn.length === 3) assert(tx.txOut.length === 3) assert(tx.txOut.head === TxOut(initiatorParams.fundingAmount, fundingScript)) + assert(initiatorTx.localFees(initiatorParams) === 2500.sat) + assert(nonInitiatorTx.localFees(nonInitiatorParams) === 1000.sat) } test("invalid input") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index cf1a130cdd..18cc43faf7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -129,7 +129,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val blockHeight = new AtomicLong() blockHeight.set(currentBlockHeight(probe).toLong) val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) - val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), walletClient) + val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), Some(walletClient)) val testTags = channelType match { case ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) case ChannelTypes.AnchorOutputs => Set(ChannelStateTestsTags.AnchorOutputs) 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 ba9aa1f45c..cab18dba60 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,14 +20,14 @@ 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.eclair.TestConstants.{Alice, Bob} +import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{ScriptFlags, SigHash, SigVersion} +import fr.acinq.eclair.TestConstants.{Alice, Bob, nonInitiatorFundingSatoshis} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeTargets, FeeratePerKw} -import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainWallet} +import fr.acinq.eclair.blockchain.{DummyOnChainWallet, NoOpOnChainWallet, OnChainWallet} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher @@ -40,6 +40,7 @@ import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol._ import org.scalatest.{FixtureTestSuite, ParallelTestExecution} +import scodec.bits.ByteVector import java.util.UUID import scala.concurrent.duration._ @@ -75,8 +76,6 @@ 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. */ @@ -110,7 +109,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def currentBlockHeight: BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight } - def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet: OnChainWallet = new DummyOnChainWallet(), tags: Set[String] = Set.empty): SetupFixture = { + def init(nodeParamsA: NodeParams = TestConstants.Alice.nodeParams, nodeParamsB: NodeParams = TestConstants.Bob.nodeParams, wallet_opt: Option[OnChainWallet] = None, tags: Set[String] = Set.empty): SetupFixture = { val aliceOrigin = TestProbe() val alice2bob = TestProbe() val bob2alice = TestProbe() @@ -136,6 +135,10 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { .modify(_.channelConf.dustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(5000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceAliceBob))(10000 sat) .modify(_.channelConf.maxRemoteDustLimit).setToIf(tags.contains(ChannelStateTestsTags.HighDustLimitDifferenceBobAlice))(10000 sat) + val wallet = wallet_opt match { + case Some(wallet) => wallet + case None => if (tags.contains(ChannelStateTestsTags.DualFunding)) new NoOpOnChainWallet() else new DummyOnChainWallet() + } val alice: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsA, wallet, finalNodeParamsB.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain), origin_opt = Some(aliceOrigin.ref)), alicePeer.ref) val bob: TestFSMRef[ChannelState, ChannelData, Channel] = TestFSMRef(new Channel(finalNodeParamsB, wallet, finalNodeParamsA.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) SetupFixture(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) @@ -195,10 +198,10 @@ 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 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 fundingAmount = TestConstants.fundingSatoshis + val pushMsat = if (tags.contains(ChannelStateTestsTags.NoPushMsat) || dualFunded) 0 msat else TestConstants.pushMsat + val nonInitiatorFundingAmount = if (dualFunded) Some(TestConstants.nonInitiatorFundingSatoshis) else None val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) @@ -206,26 +209,155 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) 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] + if (!dualFunded) { + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + alice2bob.expectMsgType[FundingCreated] + alice2bob.forward(bob) + bob2alice.expectMsgType[FundingSigned] + bob2alice.forward(alice) + assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) + alice2blockchain.expectMsgType[WatchFundingSpent] + alice2blockchain.expectMsgType[WatchFundingConfirmed] + assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) + bob2blockchain.expectMsgType[WatchFundingSpent] + bob2blockchain.expectMsgType[WatchFundingConfirmed] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get + alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) + alice2blockchain.expectMsgType[WatchFundingLost] + bob2blockchain.expectMsgType[WatchFundingLost] + alice2bob.expectMsgType[FundingLocked] + alice2bob.forward(bob) + bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[WatchFundingDeeplyBuried] + bob2blockchain.expectMsgType[WatchFundingDeeplyBuried] + awaitCond(alice.stateName == NORMAL) + awaitCond(bob.stateName == NORMAL) + } else { + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) + assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) + val (aliceWalletKey, bobWalletKey) = (randomKey(), randomKey()) + val (aliceInputAmount, aliceChangeAmount) = (fundingAmount + 75_000.sat, 60_000.sat) + val (bobInputAmount, bobChangeAmount) = (nonInitiatorFundingSatoshis + 25_000.sat, 20_000.sat) + fundDualFundingChannel(alice, aliceWalletKey, aliceInputAmount, aliceChangeAmount, bob, bobWalletKey, bobInputAmount, bobChangeAmount, alice2bob, bob2alice) + signDualFundedChannel(alice, aliceWalletKey, aliceInputAmount, bob, bobWalletKey, bobInputAmount, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + confirmDualFundedChannel(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + } + val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + assert(bobCommitments.availableBalanceForSend == (nonInitiatorFundingAmount.getOrElse(0 sat) + pushMsat - aliceCommitments.remoteChannelReserve).max(0 msat)) + // x2 because alice and bob share the same relayer + channelUpdateListener.expectMsgType[LocalChannelUpdate] + channelUpdateListener.expectMsgType[LocalChannelUpdate] + } + + /** Fund a dual-funded channel with one input and one change output for each peer. */ + def fundDualFundingChannel(alice: TestFSMRef[ChannelState, ChannelData, Channel], + aliceWalletKey: PrivateKey, + aliceInputAmount: Satoshi, + aliceChangeAmount: Satoshi, + bob: TestFSMRef[ChannelState, ChannelData, Channel], + bobWalletKey: PrivateKey, + bobInputAmount: Satoshi, + bobChangeAmount: Satoshi, + alice2bob: TestProbe, + bob2alice: TestProbe): Unit = { + assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_INTERNAL) + + val fundingScript = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams.fundingPubkeyScript + val aliceFunding = InteractiveTxFunder.FundingSucceeded(InteractiveTx.FundingContributions( + Seq(TxAddInput(channelId(alice), UInt64(0), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 2), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(aliceInputAmount, Script.pay2wpkh(aliceWalletKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(channelId(alice), UInt64(0), TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis, fundingScript), TxAddOutput(channelId(alice), UInt64(2), aliceChangeAmount, Script.write(Script.pay2wpkh(randomKey().publicKey)))), + )) + alice ! aliceFunding + + val bobFunding = InteractiveTxFunder.FundingSucceeded(InteractiveTx.FundingContributions( + Seq(TxAddInput(channelId(bob), UInt64(1), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(bobInputAmount, Script.pay2wpkh(bobWalletKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(channelId(bob), UInt64(1), bobChangeAmount, Script.write(Script.pay2wpkh(randomKey().publicKey)))), + )) + bob ! bobFunding + + // Alice and Bob go through the interactive tx construction. + alice2bob.expectMsgType[TxAddInput] alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] + bob2alice.expectMsgType[TxAddInput] bob2alice.forward(alice) - alice2bob.expectMsgType[FundingCreated] + alice2bob.expectMsgType[TxAddOutput] alice2bob.forward(bob) - bob2alice.expectMsgType[FundingSigned] + bob2alice.expectMsgType[TxAddOutput] bob2alice.forward(alice) - assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) - alice2blockchain.expectMsgType[WatchFundingSpent] - alice2blockchain.expectMsgType[WatchFundingConfirmed] - assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId != ByteVector32.Zeroes) - bob2blockchain.expectMsgType[WatchFundingSpent] - bob2blockchain.expectMsgType[WatchFundingConfirmed] - awaitCond(alice.stateName == WAIT_FOR_FUNDING_CONFIRMED) - val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].fundingTx.get - alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx) - alice2blockchain.expectMsgType[WatchFundingLost] - bob2blockchain.expectMsgType[WatchFundingLost] + 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) + } + + /** Both peers sign the dual-funding transaction. */ + def signDualFundedChannel(alice: TestFSMRef[ChannelState, ChannelData, Channel], + aliceWalletKey: PrivateKey, + aliceInputAmount: Satoshi, + bob: TestFSMRef[ChannelState, ChannelData, Channel], + bobWalletKey: PrivateKey, + bobInputAmount: Satoshi, + alice2bob: TestProbe, + bob2alice: TestProbe, + alice2blockchain: TestProbe, + bob2blockchain: TestProbe): Unit = { + assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + + val bobSharedTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx + val bobSig = Transaction.signInput(bobSharedTx.buildUnsignedTx(), 1, Script.pay2pkh(bobWalletKey.publicKey), SigHash.SIGHASH_ALL, bobInputAmount, SigVersion.SIGVERSION_WITNESS_V0, bobWalletKey) + bob ! InteractiveTx.PartiallySignedSharedTransaction(bobSharedTx, TxSignatures(channelId(bob), bobSharedTx.buildUnsignedTx().txid, Seq(Script.witnessPay2wpkh(bobWalletKey.publicKey, bobSig)))) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + + val aliceSharedTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx + val aliceSig = Transaction.signInput(aliceSharedTx.buildUnsignedTx(), 0, Script.pay2pkh(aliceWalletKey.publicKey), SigHash.SIGHASH_ALL, aliceInputAmount, SigVersion.SIGVERSION_WITNESS_V0, aliceWalletKey) + alice ! InteractiveTx.PartiallySignedSharedTransaction(aliceSharedTx, TxSignatures(channelId(alice), aliceSharedTx.buildUnsignedTx().txid, Seq(Script.witnessPay2wpkh(aliceWalletKey.publicKey, aliceSig)))) + alice2blockchain.expectMsgType[WatchFundingConfirmed] // alice publishes the funding tx + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + bob2blockchain.expectMsgType[WatchFundingConfirmed] // bob publishes the funding tx + } + + def confirmDualFundedChannel(alice: TestFSMRef[ChannelState, ChannelData, Channel], + bob: TestFSMRef[ChannelState, ChannelData, Channel], + alice2bob: TestProbe, + bob2alice: TestProbe, + alice2blockchain: TestProbe, + bob2blockchain: TestProbe): Unit = { + assert(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + assert(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.tx.buildUnsignedTx() + alice ! WatchFundingConfirmedTriggered(BlockHeight(TestConstants.defaultBlockHeight), 42, fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + bob ! WatchFundingConfirmedTriggered(BlockHeight(TestConstants.defaultBlockHeight), 42, fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_LOCKED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_LOCKED) + alice2bob.expectMsgType[FundingLocked] alice2bob.forward(bob) bob2alice.expectMsgType[FundingLocked] @@ -234,10 +366,6 @@ 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.requestedChannelReserve).max(0 msat)) - // x2 because alice and bob share the same relayer - channelUpdateListener.expectMsgType[LocalChannelUpdate] - channelUpdateListener.expectMsgType[LocalChannelUpdate] } def localOrigin(replyTo: ActorRef): Origin.LocalHot = Origin.LocalHot(replyTo, UUID.randomUUID()) 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 e2c57046b6..5751282b31 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 @@ -52,7 +52,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS .modify(_.chainHash).setToIf(test.tags.contains("mainnet"))(Block.LivenetGenesisBlock.hash) .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-size"))(Btc(100)) - val setup = init(aliceNodeParams, bobNodeParams, wallet = new NoOpOnChainWallet()) + val setup = init(aliceNodeParams, bobNodeParams, wallet_opt = Some(new NoOpOnChainWallet()), test.tags) import setup._ val channelConfig = ChannelConfig.standard @@ -149,7 +149,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS } test("recv AcceptChannel (anchor outputs channel type without enabling the feature)") { _ => - val setup = init(Alice.nodeParams, Bob.nodeParams, wallet = new NoOpOnChainWallet()) + val setup = init(Alice.nodeParams, Bob.nodeParams, wallet_opt = Some(new NoOpOnChainWallet())) import setup._ val channelConfig = ChannelConfig.standard 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 index a51d8f1059..09ce11c4e4 100644 --- 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 @@ -37,14 +37,14 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt 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()) + val setup = init(tags = test.tags) 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 + val nonInitiatorContribution = if (test.tags.contains("dual_funding_contribution")) 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) @@ -75,7 +75,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt aliceOrigin.expectNoMessage() } - test("recv AcceptDualFundedChannel (with non-initiator contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.DualFundingContribution)) { f => + test("recv AcceptDualFundedChannel (with non-initiator contribution)", Tag(ChannelStateTestsTags.DualFunding), Tag("dual_funding_contribution")) { f => import f._ val accept = bob2alice.expectMsgType[AcceptDualFundedChannel] 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 0e03c1006d..13f0da1670 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 @@ -46,7 +46,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val bobNodeParams = Bob.nodeParams .modify(_.channelConf.maxFundingSatoshis).setToIf(test.tags.contains("max-funding-satoshis"))(Btc(1)) - val setup = init(nodeParamsB = bobNodeParams) + val setup = init(nodeParamsB = bobNodeParams, tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard 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 index ac321f70a0..18c684aa09 100644 --- 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 @@ -34,7 +34,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur 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() + val setup = init(tags = test.tags) import setup._ val listener = TestProbe() 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 index 5c1a4c114a..8ee9a473db 100644 --- 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 @@ -41,7 +41,7 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn override def withFixture(test: OneArgTest): Outcome = { val wallet = new NoOpOnChainWallet() - val setup = init(wallet = wallet) + val setup = init(wallet_opt = Some(wallet), tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) 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 index 94dcfb41b7..5f67c56b01 100644 --- 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 @@ -39,7 +39,7 @@ class WaitForDualFundingInternalStateSpec extends TestKitBaseClass with FixtureA 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()) + val setup = init(tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala index b76f8843cf..16d17d26dc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForDualFundingSignedStateSpec.scala @@ -18,23 +18,20 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction} import fr.acinq.bitcoin.{SigHash, SigVersion} import fr.acinq.eclair.blockchain.NoOpOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmed -import fr.acinq.eclair.channel.InteractiveTx.{FundingContributions, PartiallySignedSharedTransaction} -import fr.acinq.eclair.channel.InteractiveTxSpec.createChangeScript +import fr.acinq.eclair.channel.InteractiveTx.PartiallySignedSharedTransaction 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.publish.TxPublisher.SetChannelId import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.transactions.Transactions.{PlaceHolderPubKey, PlaceHolderSig} -import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, CommitSig, Error, Init, OpenDualFundedChannel, TxAddInput, TxAddOutput, TxComplete, TxSignatures} -import fr.acinq.eclair.{TestConstants, TestKitBaseClass, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.wire.protocol.{AcceptDualFundedChannel, CommitSig, Error, Init, OpenDualFundedChannel, TxSignatures} +import fr.acinq.eclair.{TestConstants, TestKitBaseClass, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector import scala.concurrent.duration.DurationInt @@ -44,7 +41,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny override def withFixture(test: OneArgTest): Outcome = { val wallet = new NoOpOnChainWallet() - val setup = init(wallet = wallet) + val setup = init(wallet_opt = Some(wallet), tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) @@ -62,49 +59,19 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny alice2blockchain.expectMsgType[SetChannelId] // final channel id bob2blockchain.expectMsgType[SetChannelId] // final channel id - // Alice and Bob both contribute one input and one output. - val cid = channelId(bob) val privKey = randomKey() - val fundingScript = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams.fundingPubkeyScript - val aliceInputAmount = TestConstants.fundingSatoshis + 75_000.sat - val aliceFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( - Seq(TxAddInput(cid, UInt64(0), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 2), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(aliceInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), - Seq(TxAddOutput(cid, UInt64(0), TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis, fundingScript), TxAddOutput(cid, UInt64(2), 60_000 sat, createChangeScript())), - )) - alice ! aliceFunding - val bobInputAmount = TestConstants.nonInitiatorFundingSatoshis + 25_000.sat - val bobFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( - Seq(TxAddInput(cid, UInt64(1), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(bobInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), - Seq(TxAddOutput(cid, UInt64(1), 20_000 sat, createChangeScript())), - )) - bob ! bobFunding - - // Alice and Bob go through the interactive tx construction. - alice2bob.expectMsgType[TxAddInput] - 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) + val (aliceInputAmount, aliceChangeAmount) = (TestConstants.fundingSatoshis + 75_000.sat, 60_000.sat) + val (bobInputAmount, bobChangeAmount) = (TestConstants.nonInitiatorFundingSatoshis + 25_000.sat, 20_000.sat) + fundDualFundingChannel(alice, privKey, aliceInputAmount, aliceChangeAmount, bob, privKey, bobInputAmount, bobChangeAmount, alice2bob, bob2alice) val unsignedTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].fundingTx val aliceSigs = { val sig = Transaction.signInput(unsignedTx, 0, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, aliceInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) - TxSignatures(cid, unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) + TxSignatures(channelId(alice), unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) } val bobSigs = { val sig = Transaction.signInput(unsignedTx, 1, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, bobInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) - TxSignatures(cid, unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) + TxSignatures(channelId(bob), unsignedTx.txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, sig))) } withFixture(test.toNoArgTest(FixtureParam(alice, bob, aliceOrigin, alice2bob, bob2alice, alice2blockchain, bob2blockchain, aliceSigs, bobSigs, wallet))) } 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 5ca513df88..552467957f 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 @@ -56,7 +56,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun (TestConstants.fundingSatoshis, TestConstants.pushMsat) } - val setup = init(aliceNodeParams, bobNodeParams) + val setup = init(aliceNodeParams, bobNodeParams, tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard 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 8b22e15554..af195b45ab 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 @@ -40,7 +40,7 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceOrigin: TestProbe, alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val setup = init(wallet = new NoOpOnChainWallet()) + val setup = init(wallet_opt = Some(new NoOpOnChainWallet()), tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) 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 8ae744e25e..17a2fc28b1 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 @@ -55,7 +55,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS (TestConstants.fundingSatoshis, TestConstants.pushMsat) } - val setup = init(aliceNodeParams, bobNodeParams) + val setup = init(aliceNodeParams, bobNodeParams, tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala new file mode 100644 index 0000000000..f7c5af45de --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -0,0 +1,280 @@ +/* + * 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.c + +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.{SigHash, SigVersion} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingConfirmed, WatchFundingConfirmedTriggered, WatchFundingSpent} +import fr.acinq.eclair.blockchain.{CurrentBlockHeight, NoOpOnChainWallet} +import fr.acinq.eclair.channel.InteractiveTx.{FullySignedSharedTransaction, FundingContributions, PartiallySignedSharedTransaction} +import fr.acinq.eclair.channel.InteractiveTxSpec.createChangeScript +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.transactions.Transactions.{PlaceHolderPubKey, PlaceHolderSig} +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass, UInt64, randomBytes32, randomKey} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} +import scodec.bits.ByteVector + +import scala.concurrent.duration.DurationInt + +class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, listener: TestProbe, wallet: NoOpOnChainWallet) + + override def withFixture(test: OneArgTest): Outcome = { + val wallet = new NoOpOnChainWallet() + val setup = init(wallet_opt = Some(wallet), tags = test.tags) + import setup._ + + val listener = TestProbe() + system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) + system.eventStream.subscribe(listener.ref, classOf[TransactionConfirmed]) + + val channelConfig = ChannelConfig.standard + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + val bobContribution = if (test.tags.contains("no-funding-contribution")) None else Some(TestConstants.nonInitiatorFundingSatoshis) + val fundingAmount = bobContribution match { + case Some(_) => TestConstants.fundingSatoshis + TestConstants.nonInitiatorFundingSatoshis + case None => TestConstants.fundingSatoshis + } + 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, bobContribution, dualFunded = true, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice2blockchain.expectMsgType[SetChannelId] // temporary channel id + bob2blockchain.expectMsgType[SetChannelId] // temporary channel id + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[SetChannelId] // final channel id + bob2blockchain.expectMsgType[SetChannelId] // final channel id + + // Alice always contributes one input and one output. + val cid = channelId(bob) + val privKey = randomKey() + val fundingScript = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_INTERNAL].fundingParams.fundingPubkeyScript + val aliceInputAmount = TestConstants.fundingSatoshis + 75_000.sat + val aliceFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(TxAddInput(cid, UInt64(0), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 2), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(aliceInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(cid, UInt64(0), fundingAmount, fundingScript), TxAddOutput(cid, UInt64(2), 60_000 sat, createChangeScript())), + )) + alice ! aliceFunding + + // Bob also contributes one input and one output. + val bobInputAmount = TestConstants.nonInitiatorFundingSatoshis + 25_000.sat + val bobFunding = InteractiveTxFunder.FundingSucceeded(FundingContributions( + Seq(TxAddInput(cid, UInt64(1), Transaction(2, Seq(TxIn(OutPoint(randomBytes32(), 1), ByteVector.empty, 0, Script.witnessPay2wpkh(PlaceHolderPubKey, PlaceHolderSig.bytes))), Seq(TxOut(bobInputAmount, Script.pay2wpkh(privKey.publicKey))), 0), 0, 0)), + Seq(TxAddOutput(cid, UInt64(1), 20_000 sat, createChangeScript())), + )) + bobContribution match { + case Some(_) => bob ! bobFunding + case None => // nothing to do + } + + // Alice and Bob go through the interactive tx construction. + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bobContribution match { + case Some(_) => bob2alice.expectMsgType[TxAddInput] + case None => bob2alice.expectMsgType[TxComplete] + } + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bobContribution match { + case Some(_) => bob2alice.expectMsgType[TxAddOutput] + case None => bob2alice.expectMsgType[TxComplete] + } + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + + // Alice and Bob sign the commitment and funding transaction. + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED && bob.stateName == WAIT_FOR_DUAL_FUNDING_SIGNED) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + val bobSharedTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx + bobContribution match { + case Some(_) => + val bobSig = Transaction.signInput(bobSharedTx.buildUnsignedTx(), 1, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, bobInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) + bob ! PartiallySignedSharedTransaction(bobSharedTx, TxSignatures(cid, bobSharedTx.buildUnsignedTx().txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, bobSig)))) + case None => // nothing to do + } + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + + val aliceSharedTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED].sharedTx + val aliceSig = Transaction.signInput(aliceSharedTx.buildUnsignedTx(), 0, Script.pay2pkh(privKey.publicKey), SigHash.SIGHASH_ALL, aliceInputAmount, SigVersion.SIGVERSION_WITNESS_V0, privKey) + val aliceTxSigs = TxSignatures(cid, aliceSharedTx.buildUnsignedTx().txid, Seq(Script.witnessPay2wpkh(privKey.publicKey, aliceSig))) + alice ! PartiallySignedSharedTransaction(aliceSharedTx, aliceTxSigs) + // Alice publishes the funding tx. + val fundingTx = listener.expectMsgType[TransactionPublished].tx + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId === fundingTx.txid) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + // Bob publishes the funding tx. + assert(listener.expectMsgType[TransactionPublished].tx.txid === fundingTx.txid) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId === fundingTx.txid) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, listener, wallet))) + } + } + + test("recv WatchFundingConfirmedTriggered", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + alice2bob.expectMsgType[FundingLocked] + awaitCond(alice.stateName === WAIT_FOR_DUAL_FUNDING_LOCKED) + } + + test("recv CurrentBlockCount (funding in progress)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 + alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) + // Alice republishes the highest feerate funding tx. + assert(listener.expectMsgType[TransactionPublished].tx.txid === fundingTx.txid) + alice2bob.expectNoMessage(100 millis) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateName === WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + + test("recv CurrentBlockCount (funding in progress while offline)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) + // Alice republishes the highest feerate funding tx. + assert(listener.expectMsgType[TransactionPublished].tx.txid === fundingTx.txid) + alice2bob.expectNoMessage(100 millis) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateName === OFFLINE) + } + + test("recv CurrentBlockCount (funding double-spent)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 + wallet.doubleSpent = Set(fundingTx.txid) + alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) + alice2bob.expectMsgType[Error] + alice2blockchain.expectNoMessage(100 millis) + awaitCond(wallet.rolledback.map(_.txid) === Seq(fundingTx.txid)) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CurrentBlockCount (funding double-spent while offline)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val currentBlock = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + 10 + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + wallet.doubleSpent = Set(fundingTx.txid) + alice ! ProcessCurrentBlockHeight(CurrentBlockHeight(currentBlock)) + alice2bob.expectMsgType[Error] + alice2blockchain.expectNoMessage(100 millis) + awaitCond(wallet.rolledback.map(_.txid) === Seq(fundingTx.txid)) + awaitCond(alice.stateName == CLOSED) + } + + test("recv CurrentBlockCount (funding timeout reached)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f => + import f._ + val timeoutBlock = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + Channel.FUNDING_TIMEOUT_FUNDEE + 1 + bob ! ProcessCurrentBlockHeight(CurrentBlockHeight(timeoutBlock)) + bob2alice.expectMsgType[Error] + bob2blockchain.expectNoMessage(100 millis) + awaitCond(bob.stateName == CLOSED) + } + + test("recv CurrentBlockCount (funding timeout reached while offline)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f => + import f._ + val timeoutBlock = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].waitingSince + Channel.FUNDING_TIMEOUT_FUNDEE + 1 + bob ! INPUT_DISCONNECTED + awaitCond(bob.stateName == OFFLINE) + bob ! ProcessCurrentBlockHeight(CurrentBlockHeight(timeoutBlock)) + bob2alice.expectMsgType[Error] + bob2blockchain.expectNoMessage(100 millis) + awaitCond(bob.stateName == CLOSED) + } + + test("recv FundingLocked", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + bob ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(listener.expectMsgType[TransactionConfirmed].tx === fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + val fundingLocked = bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) + awaitCond(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].deferred.contains(fundingLocked)) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + awaitCond(bob.stateName === WAIT_FOR_DUAL_FUNDING_LOCKED) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice ! Error(ByteVector32.Zeroes, "dual funding d34d") + // We don't force-close yet because we don't know which funding tx will be confirmed. + alice2blockchain.expectNoMessage(100 millis) + alice2bob.expectNoMessage(100 millis) + assert(alice.stateName === WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f => + import f._ + bob ! Error(ByteVector32.Zeroes, "dual funding d34d") + bob2blockchain.expectNoMessage(100 millis) // we don't publish our commit tx when we have nothing at stake + awaitCond(bob.stateName == CLOSED) + } + + 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_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_DUAL_FUNDING_CONFIRMED))) + } + + test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val sender = TestProbe() + val c = CMD_FORCECLOSE(sender.ref) + alice ! c + sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "force-close", WAIT_FOR_DUAL_FUNDING_CONFIRMED))) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingLockedStateSpec.scala new file mode 100644 index 0000000000..ce23821e05 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingLockedStateSpec.scala @@ -0,0 +1,156 @@ +/* + * 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.c + +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} +import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.publish.TxPublisher +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomKey} +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} + +import scala.concurrent.duration.DurationInt + +class WaitForDualFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val setup = init(tags = test.tags) + 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) + alice2blockchain.expectMsgType[SetChannelId] // temporary channel id + bob2blockchain.expectMsgType[SetChannelId] // temporary channel id + alice2bob.expectMsgType[OpenDualFundedChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptDualFundedChannel] + bob2alice.forward(alice) + alice2blockchain.expectMsgType[SetChannelId] // final channel id + bob2blockchain.expectMsgType[SetChannelId] // final channel id + + val privKey = randomKey() + fundDualFundingChannel(alice, privKey, TestConstants.fundingSatoshis + 75_000.sat, 60_000.sat, bob, privKey, TestConstants.nonInitiatorFundingSatoshis + 25_000.sat, 20_000.sat, alice2bob, bob2alice) + signDualFundedChannel(alice, privKey, TestConstants.fundingSatoshis + 75_000.sat, bob, privKey, TestConstants.nonInitiatorFundingSatoshis + 25_000.sat, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.tx.buildUnsignedTx() + alice ! WatchFundingConfirmedTriggered(BlockHeight(TestConstants.defaultBlockHeight), 42, fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + bob ! WatchFundingConfirmedTriggered(BlockHeight(TestConstants.defaultBlockHeight), 42, fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId === fundingTx.txid) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_LOCKED) + awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_LOCKED) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain))) + } + } + + test("recv FundingLocked", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bob.underlyingActor.nodeParams.nodeId, RelayFees(20 msat, 125)) + bob.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(alice.underlyingActor.nodeParams.nodeId, RelayFees(25 msat, 90)) + + alice2bob.expectMsgType[FundingLocked] + alice2bob.forward(bob) + awaitCond(bob.stateName == NORMAL) + bob2alice.expectMsgType[FundingLocked] + bob2alice.forward(alice) + awaitCond(alice.stateName == NORMAL) + + val aliceCommitments = alice.stateData.asInstanceOf[DATA_NORMAL].commitments + val aliceUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(aliceUpdate.feeBaseMsat === 20.msat) + assert(aliceUpdate.feeProportionalMillionths === 125) + assert(aliceCommitments.localChannelReserve === aliceCommitments.commitInput.txOut.amount / 100) + assert(aliceCommitments.localChannelReserve === aliceCommitments.remoteChannelReserve) + val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments + val bobUpdate = bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate + assert(bobUpdate.feeBaseMsat === 25.msat) + assert(bobUpdate.feeProportionalMillionths === 90) + assert(bobCommitments.localChannelReserve === aliceCommitments.remoteChannelReserve) + assert(bobCommitments.localChannelReserve === bobCommitments.remoteChannelReserve) + + alice2bob.expectNoMessage(100 millis) + bob2alice.expectNoMessage(100 millis) + } + + test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + // bob publishes his commitment tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(bobCommitTx) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobCommitTx.txid) + awaitCond(alice.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered (other commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + alice2bob.expectMsgType[FundingLocked] + val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice2bob.expectMsgType[Error] + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === commitTx.txid) + awaitCond(alice.stateName == ERR_INFORMATION_LEAK) + } + + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! Error(ByteVector32.Zeroes, "dual funding failure") + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === commitTx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] // anchor output + alice2blockchain.expectMsgType[TxPublisher.PublishTx] // main output + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === commitTx.txid) + } + + 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_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_DUAL_FUNDING_LOCKED))) + } + + test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + val sender = TestProbe() + val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_LOCKED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + alice ! CMD_FORCECLOSE(sender.ref) + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid === commitTx.txid) + alice2blockchain.expectMsgType[TxPublisher.PublishTx] // anchor output + alice2blockchain.expectMsgType[TxPublisher.PublishTx] // main output + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === commitTx.txid) + } + +} 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 d6b99ae9c4..b06dd83ec7 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 @@ -42,9 +42,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, listener: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - - val setup = init() - + val setup = init(tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat 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 f1c17b8b3d..e9eb56b719 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 @@ -42,7 +42,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, router: TestProbe) override def withFixture(test: OneArgTest): Outcome = { - val setup = init() + val setup = init(tags = test.tags) import setup._ val channelConfig = ChannelConfig.standard val pushMsat = if (test.tags.contains(ChannelStateTestsTags.NoPushMsat)) 0.msat else TestConstants.pushMsat 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 a209471de2..f96d017077 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 @@ -18,9 +18,9 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} -import fr.acinq.bitcoin.ScriptFlags import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -69,7 +69,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("recv CMD_ADD_HTLC (empty origin)") { f => + private def testRecvCmdAddHtlcEmptyOrigin(f: FixtureParam): Unit = { import f._ val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() @@ -91,6 +91,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ))) } + test("recv CMD_ADD_HTLC (empty origin)") { f => + testRecvCmdAddHtlcEmptyOrigin(f) + } + + test("recv CMD_ADD_HTLC (empty origin, dual funding)", Tag(ChannelStateTestsTags.DualFunding)) { f => + testRecvCmdAddHtlcEmptyOrigin(f) + } + test("recv CMD_ADD_HTLC (incrementing ids)") { f => import f._ val sender = TestProbe() @@ -3411,6 +3419,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with channelUpdateListener.expectNoMessage(1 second) } + test("recv WatchFundingDeeplyBuriedTriggered (dual funding)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.ChannelsPublic)) { f => + import f._ + alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400000), 42, null) + val annSigs = alice2bob.expectMsgType[AnnouncementSignatures] + // public channel: we don't send the channel_update directly to the peer + alice2bob.expectNoMessage(1 second) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].shortChannelId == annSigs.shortChannelId && alice.stateData.asInstanceOf[DATA_NORMAL].buried) + // we don't re-publish the same channel_update if there was no change + channelUpdateListener.expectNoMessage(1 second) + } + test("recv WatchFundingDeeplyBuriedTriggered (short channel id changed)", Tag(ChannelStateTestsTags.ChannelsPublic)) { f => import f._ alice ! WatchFundingDeeplyBuriedTriggered(BlockHeight(400001), 22, null) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index c04f621937..ace0d00c76 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -853,3 +853,23 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte } } + +class DualFundingChannelIntegrationSpec extends AnchorChannelIntegrationSpec { + + override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat + + test("start eclair nodes") { + instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 29770, "eclair.api.port" -> 28106).asJava).withFallback(withDualFunding).withFallback(commonConfig)) + instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 29771, "eclair.api.port" -> 28107).asJava).withFallback(withDualFunding).withFallback(commonConfig)) + instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.server.port" -> 29772, "eclair.api.port" -> 28108).asJava).withFallback(withDualFunding).withFallback(commonConfig)) + } + + test("connect nodes") { + connectNodes(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + } + + test("open channel C <-> F, send payments and close (dual funding)") { + testOpenPayClose(ChannelTypes.AnchorOutputsZeroFeeHtlcTx) + } + +} 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 b05f48ec21..2005c37a9a 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 @@ -119,6 +119,10 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "optional" ).asJava).withFallback(withStaticRemoteKey) + val withDualFunding = ConfigFactory.parseMap(Map( + s"eclair.features.${DualFunding.rfcName}" -> "optional" + ).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs) + implicit val formats: Formats = DefaultFormats override def beforeAll(): Unit = {