From 77933ede1c0fd739acc7fc07aed41c11d7c96826 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 22 Jul 2024 10:37:02 +0200 Subject: [PATCH 1/3] Add optional `maxExcess` parameter to channel `open` API This change passes `maxExcess` to Peer via OpenChannel and from Peer to Channel via INPUT_INIT_CHANNEL_INITIATOR. The `maxExcess` parameter will be used by bitcoind funding calls but is not currently used. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 5 ++-- .../fr/acinq/eclair/channel/ChannelData.scala | 1 + .../main/scala/fr/acinq/eclair/io/Peer.scala | 4 +++- .../fr/acinq/eclair/EclairImplSpec.scala | 12 +++++----- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../ChannelStateTestsHelperMethods.scala | 2 +- .../a/WaitForAcceptChannelStateSpec.scala | 4 ++-- ...tForAcceptDualFundedChannelStateSpec.scala | 2 +- .../a/WaitForOpenChannelStateSpec.scala | 2 +- ...aitForOpenDualFundedChannelStateSpec.scala | 2 +- .../WaitForDualFundingCreatedStateSpec.scala | 2 +- .../b/WaitForDualFundingSignedStateSpec.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingInternalStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForChannelReadyStateSpec.scala | 2 +- ...WaitForDualFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForDualFundingReadyStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../eclair/integration/IntegrationSpec.scala | 1 + .../basic/fixtures/MinimalNodeFixture.scala | 2 +- .../interop/rustytests/RustyTestsSpec.scala | 2 +- .../io/OpenChannelInterceptorSpec.scala | 4 ++-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 24 +++++++++---------- .../acinq/eclair/api/handlers/Channel.scala | 6 ++--- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 18 +++++++------- 27 files changed, 59 insertions(+), 54 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 9aeec55496..dcb5ac163a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -86,7 +86,7 @@ trait Eclair { def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeerate_opt: Option[FeeratePerByte], fundingFeeBudget_opt: Option[Satoshi], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] + def open(nodeId: PublicKey, fundingAmount: Satoshi, maxExcess_opt: Option[Satoshi], pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeerate_opt: Option[FeeratePerByte], fundingFeeBudget_opt: Option[Satoshi], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] @@ -206,7 +206,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[Peer.DisconnectResponse].map(_.toString) } - override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeerate_opt: Option[FeeratePerByte], fundingFeeBudget_opt: Option[Satoshi], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, maxExcess_opt: Option[Satoshi], pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeerate_opt: Option[FeeratePerByte], fundingFeeBudget_opt: Option[Satoshi], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds)) // if no budget is provided for the mining fee of the funding tx, we use a default of 0.1% of the funding amount as a safety measure @@ -216,6 +216,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { open = Peer.OpenChannel( remoteNodeId = nodeId, fundingAmount = fundingAmount, + maxExcess_opt = maxExcess_opt, channelType_opt = channelType_opt, pushAmount_opt = pushAmount_opt, fundingTxFeerate_opt = fundingFeerate_opt.map(FeeratePerKw(_)), 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 05aeb6f985..dcfd390d66 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 @@ -92,6 +92,7 @@ case object ERR_INFORMATION_LEAK extends ChannelState case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, + maxExcess_opt: Option[Satoshi], dualFunded: Boolean, commitTxFeerate: FeeratePerKw, fundingTxFeerate: FeeratePerKw, 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 296ae08f26..673918dd26 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 @@ -173,7 +173,7 @@ class Peer(val nodeParams: NodeParams, val fundingTxFeerate = c.fundingTxFeerate_opt.getOrElse(nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeerates)) val commitTxFeerate = nodeParams.onChainFeeConf.getCommitmentFeerate(nodeParams.currentFeerates, remoteNodeId, channelType.commitmentFormat, c.fundingAmount) 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.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, c.channelOrigin, replyTo) + channel ! INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId, c.fundingAmount, c.maxExcess_opt, dualFunded, commitTxFeerate, fundingTxFeerate, c.fundingTxFeeBudget_opt, c.pushAmount_opt, requireConfirmedInputs, localParams, d.peerConnection, d.remoteInit, c.channelFlags_opt.getOrElse(nodeParams.channelConf.channelFlags), channelConfig, channelType, c.channelOrigin, replyTo) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(open: protocol.OpenChannel, d: ConnectedData) => @@ -512,6 +512,7 @@ object Peer { case class OpenChannel(remoteNodeId: PublicKey, fundingAmount: Satoshi, + maxExcess_opt: Option[Satoshi], channelType_opt: Option[SupportedChannelType], pushAmount_opt: Option[MilliSatoshi], fundingTxFeerate_opt: Option[FeeratePerKw], @@ -523,6 +524,7 @@ object Peer { channelOrigin: ChannelOrigin = ChannelOrigin.Default) extends PossiblyHarmful { require(!(channelType_opt.exists(_.features.contains(Features.ScidAlias)) && channelFlags_opt.exists(_.announceChannel)), "option_scid_alias is not compatible with public channels") require(fundingAmount > 0.sat, s"funding amount must be positive") + maxExcess_opt.foreach(maxExcess => { require(maxExcess >= 0.sat, s"maxExcess 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") 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 cb2a6b1fb3..d2de66d0d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -100,12 +100,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, channelType_opt = None, fundingFeerate_opt = Some(FeeratePerByte(5 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, maxExcess_opt = None, pushAmount_opt = None, channelType_opt = None, fundingFeerate_opt = Some(FeeratePerByte(5 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] assert(open.fundingTxFeerate_opt.contains(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()), fundingFeerate_opt = Some(FeeratePerByte(1 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, maxExcess_opt = None, pushAmount_opt = None, channelType_opt = Some(ChannelTypes.StaticRemoteKey()), fundingFeerate_opt = Some(FeeratePerByte(1 sat)), fundingFeeBudget_opt = None, announceChannel_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeerate_opt.contains(FeeratePerKw.MinimumFeeratePerKw)) assert(open1.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) @@ -251,16 +251,16 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val eclair = new EclairImpl(kit) // option_scid_alias is not compatible with public channels - eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, None, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) assert(sender.expectMsgType[Status.Failure].cause.getMessage.contains("option_scid_alias is not compatible with public channels")) - eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, None, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) switchboard.expectMsgType[Peer.OpenChannel] - eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), None, None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, None, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), None, None, announceChannel_opt = Some(true), None).pipeTo(sender.ref) switchboard.expectMsgType[Peer.OpenChannel] - eclair.open(randomKey().publicKey, 123456 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), None, None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, None, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(zeroConf = true)), None, None, announceChannel_opt = Some(false), None).pipeTo(sender.ref) switchboard.expectMsgType[Peer.OpenChannel] } 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 dc25ecedc6..16e0518049 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 @@ -77,7 +77,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe aliceRegister ! alice bobRegister ! bob // no announcements - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, channelFlags, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelTypes.Standard()) bob2blockchain.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 ebc97c182a..56c81b1bbf 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 @@ -255,7 +255,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, maxExcess_opt = None, dualFunded, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId == ByteVector32.Zeroes) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorFundingAmount, dualFunded, nonInitiatorPushAmount, 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 0f94af78b0..6a25ac5c40 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 @@ -66,7 +66,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) val fundingAmount = if (test.tags.contains(LargeChannel)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, maxExcess_opt = None, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -172,7 +172,7 @@ 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_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs()) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs())) 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 34c505afb3..c3f4d30531 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 @@ -59,7 +59,7 @@ class WaitForAcceptDualFundedChannelStateSpec extends TestKitBaseClass with Fixt val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, nonInitiatorContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) val open = alice2bob.expectMsgType[OpenDualFundedChannel] alice2bob.forward(bob, open) 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 ca8a8672a6..ab0495da97 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 @@ -57,7 +57,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val listener = TestProbe() within(30 seconds) { bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain, listener))) 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 fc1cec79b2..a0faa13322 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 @@ -57,7 +57,7 @@ class WaitForOpenDualFundedChannelStateSpec extends TestKitBaseClass with Fixtur val bobInit = Init(bobParams.initFeatures) val requireConfirmedInputs = test.tags.contains(aliceRequiresConfirmedInputs) within(30 seconds) { - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushAmount, requireConfirmedInputs, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushAmount, requireConfirmedInputs, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) awaitCond(bob.stateName == WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, aliceListener, bobListener))) 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 9458661e32..239dea6d4c 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 @@ -53,7 +53,7 @@ class WaitForDualFundingCreatedStateSpec extends TestKitBaseClass with FixtureAn within(30 seconds) { alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id 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 75e26ecfd9..72b18f3a07 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 @@ -61,7 +61,7 @@ class WaitForDualFundingSignedStateSpec extends TestKitBaseClass with FixtureAny within(30 seconds) { alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id bob2blockchain.expectMsgType[TxPublisher.SetChannelId] // temporary channel id 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 9ee675f56e..85764379b0 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 @@ -65,7 +65,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val listener = TestProbe() within(30 seconds) { bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] 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 301609f613..e4565e2f59 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 @@ -52,7 +52,7 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, 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/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 6abc907f0a..93a31599cc 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 @@ -68,7 +68,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingSatoshis, maxExcess_opt = None, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 5e2cb53505..0557ceedb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -65,7 +65,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu alice.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(bobListener.ref, classOf[ChannelAborted]) alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushMsat, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, pushMsat, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] 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 index 12fe47d53f..bf303e81ac 100644 --- 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 @@ -72,7 +72,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val bobContribution = if (test.tags.contains("no-funding-contribution")) None else Some(TestConstants.nonInitiatorFundingSatoshis) val (initiatorPushAmount, nonInitiatorPushAmount) = if (test.tags.contains("both_push_amount")) (Some(TestConstants.initiatorPushAmount), Some(TestConstants.nonInitiatorPushAmount)) else (None, None) within(30 seconds) { - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, commitFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, bobContribution, dualFunded = true, nonInitiatorPushAmount, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] // temporary channel id bob2blockchain.expectMsgType[SetChannelId] // temporary channel id diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala index d260fc9bde..8b0d8f4e4a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala @@ -52,7 +52,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = true, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, Some(TestConstants.nonInitiatorFundingSatoshis), dualFunded = true, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2blockchain.expectMsgType[SetChannelId] // temporary channel id bob2blockchain.expectMsgType[SetChannelId] // temporary channel id 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 f111398296..88e3e5fc7d 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 @@ -61,7 +61,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed]) val commitTxFeerate = if (test.tags.contains(ChannelStateTestsTags.AnchorOutputs) || test.tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) TestConstants.anchorOutputsFeeratePerKw else TestConstants.feeratePerKw - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, commitTxFeerate, commitTxFeerate, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, commitTxFeerate, commitTxFeerate, fundingTxFeeBudget_opt = None, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] 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 4b2e0a4ad7..d32251e49c 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 @@ -75,7 +75,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[SetChannelId] 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 f4fdd37510..966d4106b9 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 @@ -180,6 +180,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, fundingAmount = fundingAmount, + maxExcess_opt = None, channelType_opt = None, pushAmount_opt = Some(pushMsat), fundingTxFeerate_opt = None, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 1fcbada4a6..27a4cd58de 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -182,7 +182,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat def openChannel(node1: MinimalNodeFixture, node2: MinimalNodeFixture, funding: Satoshi, channelType_opt: Option[SupportedChannelType] = None)(implicit system: ActorSystem): OpenChannelResponse.Created = { val sender = TestProbe("sender") - sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, channelType_opt, None, None, None, None, None)) + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, maxExcess_opt = None, channelType_opt, None, None, None, None, None)) sender.expectMsgType[OpenChannelResponse.Created] } 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 69ae7dfedc..b2bc436741 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 @@ -72,7 +72,7 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val aliceInit = Init(Alice.channelParams.initFeatures) val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, dualFunded = false, commitTxFeerate = feeratePerKw, fundingTxFeerate = feeratePerKw, fundingTxFeeBudget_opt = None, Some(1000000000 msat), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, ChannelFlags(announceChannel = false), channelConfig, channelType, replyTo = system.deadLetters) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, 2000000 sat, maxExcess_opt = None, dualFunded = false, commitTxFeerate = feeratePerKw, fundingTxFeerate = feeratePerKw, fundingTxFeeBudget_opt = None, Some(1000000000 msat), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, ChannelFlags(announceChannel = false), channelConfig, channelType, replyTo = system.deadLetters) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, Bob.channelParams, pipe, aliceInit, channelConfig, channelType) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala index 3231ce2ad2..9a8e74a406 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/OpenChannelInterceptorSpec.scala @@ -190,7 +190,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val probe = TestProbe[Any]() val fundingAmountBig = Channel.MAX_FUNDING_WITHOUT_WUMBO + 10_000.sat - openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), defaultFeatures, defaultFeatures.add(Wumbo, Optional)) + openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None, None), defaultFeatures, defaultFeatures.add(Wumbo, Optional)) assert(probe.expectMessageType[OpenChannelResponse.Rejected].reason.contains("you must enable large channels support")) } @@ -199,7 +199,7 @@ class OpenChannelInterceptorSpec extends ScalaTestWithActorTestKit(ConfigFactory val probe = TestProbe[Any]() val fundingAmountBig = Channel.MAX_FUNDING_WITHOUT_WUMBO + 10_000.sat - openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), defaultFeatures.add(Wumbo, Optional), defaultFeatures) + openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None, None), defaultFeatures.add(Wumbo, Optional), defaultFeatures) assert(probe.expectMessageType[OpenChannelResponse.Rejected].reason == s"fundingAmount=$fundingAmountBig is too big, the remote peer doesn't support wumbo") } 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 eea78d52ca..52aacd8b96 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 @@ -363,7 +363,7 @@ class PeerSpec extends FixtureSpec { connect(remoteNodeId, peer, peerConnection, switchboard) assert(peer.stateData.channels.isEmpty) - val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, None, None) + val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, None, None, None) peerConnection.send(peer, open) channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] } @@ -384,7 +384,7 @@ class PeerSpec extends FixtureSpec { // 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, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded) } @@ -427,15 +427,15 @@ class PeerSpec extends FixtureSpec { connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, 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, Some(ChannelTypes.Standard()), None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(ChannelTypes.Standard()), None, 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, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputs()) } @@ -470,7 +470,7 @@ class PeerSpec extends FixtureSpec { // We ensure the current network feerate is higher than the default anchor output feerate. nodeParams.setFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputs()) assert(!init.dualFunded) @@ -488,7 +488,7 @@ class PeerSpec extends FixtureSpec { // We ensure the current network feerate is higher than the default anchor output feerate. nodeParams.setFeerates(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2).copy(minimum = FeeratePerKw(250 sat))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) assert(!init.dualFunded) @@ -502,7 +502,7 @@ class PeerSpec extends FixtureSpec { val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.StaticRemoteKey()) assert(!init.dualFunded) @@ -519,12 +519,12 @@ class PeerSpec extends FixtureSpec { assert(peer.underlyingActor.nodeParams.channelConf.maxHtlcValueInFlightMsat == 100_000_000.msat) { - probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, None, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.localParams.maxHtlcValueInFlightMsat == 50_000_000.msat) // max-htlc-value-in-flight-percent } { - probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, None, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.localParams.maxHtlcValueInFlightMsat == 100_000_000.msat) // max-htlc-value-in-flight-msat } @@ -548,7 +548,7 @@ class PeerSpec extends FixtureSpec { import f._ intercept[IllegalArgumentException] { - Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, None, Some(ChannelFlags(announceChannel = true)), None) + Peer.OpenChannel(remoteNodeId, 24000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, None, Some(ChannelFlags(announceChannel = true)), None) } } @@ -557,7 +557,7 @@ class PeerSpec extends FixtureSpec { val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, switchboard) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, Some(100 msat), None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, Some(100 msat), None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.replyTo == probe.ref.toTyped[OpenChannelResponse]) } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index ef1bd0b36e..6993d97da3 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -52,8 +52,8 @@ trait Channel { ).map(ct => ct.toString -> ct).toMap // we use the toString method as name in the api val open: Route = postRequest("open") { implicit t => - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "fundingFeeBudgetSatoshis".as[Satoshi].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelTypeName_opt, fundingFeerateSatByte, fundingFeeBudget_opt, announceChannel_opt, openTimeout_opt) => + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "maxExcess".as[Satoshi].?, "pushMsat".as[MilliSatoshi].?, "channelType".?, "fundingFeerateSatByte".as[FeeratePerByte].?, "fundingFeeBudgetSatoshis".as[Satoshi].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { + (nodeId, fundingSatoshis, maxExcess, pushMsat, channelTypeName_opt, fundingFeerateSatByte, fundingFeeBudget_opt, announceChannel_opt, openTimeout_opt) => val (channelTypeOk, channelType_opt) = channelTypeName_opt match { case Some(channelTypeName) => supportedChannelTypes.get(channelTypeName) match { case Some(channelType) => (true, Some(channelType)) @@ -64,7 +64,7 @@ trait Channel { if (!channelTypeOk) { reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be one of ${supportedChannelTypes.keys.mkString(",")}")) } else { - complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, fundingFeeBudget_opt, announceChannel_opt, openTimeout_opt)) + complete(eclairApi.open(nodeId, fundingSatoshis, maxExcess, pushMsat, channelType_opt, fundingFeerateSatByte, fundingFeeBudget_opt, announceChannel_opt, openTimeout_opt)) } } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index aabcd8eb09..593585df2b 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 @@ -277,7 +277,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 100 sat)) + eclair.open(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 100 sat)) val mockService = new MockService(eclair) Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100002").toEntity) ~> @@ -288,7 +288,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=100 sat\"") - eclair.open(nodeId, 100002 sat, None, None, None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 100002 sat, None, None, None, None, None, None, None)(any[Timeout]).wasCalled(once) } } @@ -314,7 +314,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 0 sat)) + eclair.open(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 0 sat)) val mockService = new MockService(eclair) Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "standard").toEntity) ~> @@ -325,7 +325,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=0 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.Standard()), None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, None, Some(ChannelTypes.Standard()), None, None, None, None)(any[Timeout]).wasCalled(once) } } @@ -335,7 +335,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 1 sat)) + eclair.open(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 1 sat)) val mockService = new MockService(eclair) Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "static_remotekey").toEntity) ~> @@ -346,7 +346,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=1 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.StaticRemoteKey()), None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, None, Some(ChannelTypes.StaticRemoteKey()), None, None, None, None)(any[Timeout]).wasCalled(once) } } @@ -356,7 +356,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val fundingTxId = TxId.fromValidHex("a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4") val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 500 sat)) + eclair.open(any, any, any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 500 sat)) val mockService = new MockService(eclair) Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs").toEntity) ~> @@ -367,7 +367,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=500 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputs()), None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, None, Some(ChannelTypes.AnchorOutputs()), None, None, None, None)(any[Timeout]).wasCalled(once) } Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "channelType" -> "anchor_outputs_zero_fee_htlc_tx").toEntity) ~> @@ -378,7 +378,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e with fundingTxId=a86b3f93c1b2ea3f221159869d6f556cae1ba2622cc8c7eb71c7f4f64e0fbca4 and fees=500 sat\"") - eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None)(any[Timeout]).wasCalled(once) } } From e784ee37ce691dcc092e31e34eeb10065d4dea7b Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 22 Jul 2024 14:57:47 +0200 Subject: [PATCH 2/3] Add maxExcess and addExcessToRecipientPosition params to fundTransaction calls These parameters are only supported in a testing branch of bitcoind for now. --- .../eclair/blockchain/OnChainWallet.scala | 4 ++-- .../bitcoind/rpc/BitcoinCoreClient.scala | 18 ++++++++++-------- .../channel/fsm/ChannelOpenSingleFunded.scala | 2 +- .../channel/fund/InteractiveTxFunder.scala | 2 +- .../channel/publish/ReplaceableTxFunder.scala | 2 +- .../eclair/blockchain/DummyOnChainWallet.scala | 14 +++++++------- 6 files changed, 22 insertions(+), 20 deletions(-) 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 3544636678..12606ad9f7 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 @@ -37,7 +37,7 @@ trait OnChainChannelFunder { * Fund the provided transaction by adding inputs (and a change output if necessary). * Callers must verify that the resulting transaction isn't sending funds to unexpected addresses (malicious bitcoin node). */ - def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi], addExcessToRecipientPosition_opt: Option[Int], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] /** * Sign a PSBT. Result may be partially signed: only inputs known to our bitcoin wallet will be signed. * @@ -55,7 +55,7 @@ trait OnChainChannelFunder { def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId] /** Create a fully signed channel funding transaction with the provided pubkeyScript. */ - def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] /** * Committing *must* include publishing the transaction on the network. 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 f6a8daa965..3bb199f9ac 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 @@ -262,8 +262,8 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag }) } - def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { - fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq), feeBudget_opt = feeBudget_opt) + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None, addExcessToRecipientPosition_opt: Option[Int] = None, maxExcess_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq, add_excess_to_recipient_position = addExcessToRecipientPosition_opt, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) } private def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Option[Int] = None)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = { @@ -308,7 +308,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag } } - def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None, maxExcess_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw): Future[MakeFundingTxResponse] = { import KotlinUtils._ @@ -344,7 +344,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag // TODO: we should check that mempoolMinFee is not dangerously high feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) // we ask bitcoin core to add inputs to the funding tx, and use the specified change address - FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate), feeBudget_opt = feeBudget_opt) + FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) lockedUtxos = tx.txIn.map(_.outPoint) signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, fee, feerate)) } yield signedTx @@ -585,7 +585,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag val theirOutput = TxOut(amount, pubkeyScript) val tx = Transaction(version = 2, txIn = Nil, txOut = theirOutput :: Nil, lockTime = 0) for { - fundedTx <- fundTransaction(tx, feeratePerKw, replaceable = true) + fundedTx <- fundTransaction(tx, feeratePerKw, replaceable = true, addExcessToRecipientPosition_opt = None, maxExcess_opt = None) lockedOutputs = fundedTx.tx.txIn.map(_.outPoint) theirOutputPos = fundedTx.tx.txOut.indexOf(theirOutput) signedPsbt <- unlockIfFails(lockedOutputs)(signPsbt(new Psbt(fundedTx.tx), fundedTx.tx.txIn.indices, fundedTx.tx.txOut.indices.filterNot(_ == theirOutputPos))) @@ -704,10 +704,10 @@ object BitcoinCoreClient { def apply(outPoint: OutPoint, weight: Long): InputWeight = InputWeight(outPoint.txid.value.toHex, outPoint.index, weight) } - case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], input_weights: Option[Seq[InputWeight]]) + case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], input_weights: Option[Seq[InputWeight]], add_excess_to_recipient_position: Option[Int], max_excess: Option[Satoshi]) object FundTransactionOptions { - def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = { + def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil, add_excess_to_recipient_position: Option[Int] = None, max_excess: Option[Satoshi] = None): FundTransactionOptions = { FundTransactionOptions( BigDecimal(FeeratePerKB(feerate).toLong).bigDecimal.scaleByPowerOfTen(-8), replaceable, @@ -721,7 +721,9 @@ object BitcoinCoreClient { // potentially be double-spent. lockUnspents = true, changePosition, - if (inputWeights.isEmpty) None else Some(inputWeights) + if (inputWeights.isEmpty) None else Some(inputWeights), + add_excess_to_recipient_position, + max_excess ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 4eaf218f91..673982afa9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -186,7 +186,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath, fundingTxIndex = 0) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) - wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).pipeTo(self) + wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt, maxExcess_opt = None).pipeTo(self) val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala index 97a8b7a3d3..fc5300f1c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxFunder.scala @@ -194,7 +194,7 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response case p: PreviousTxRbf => p.feeBudget_opt case _ => None } - context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt)) { + context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt, addExcessToRecipientPosition_opt = None, maxExcess_opt = None)) { case Failure(t) => WalletFailure(t) case Success(result) => FundTransactionResult(result.tx, result.changePosition) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 4c3d1777aa..cc88d44e56 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -434,7 +434,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, // start with a dummy output and later merge that dummy output with the optional change output added by bitcoind. val txNotFunded = anchorTx.txInfo.tx.copy(txOut = TxOut(dustLimit, Script.pay2wpkh(PlaceHolderPubKey)) :: Nil) val anchorWeight = Seq(InputWeight(anchorTx.txInfo.input.outPoint, anchorInputWeight)) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight), feeBudget_opt = None).flatMap { fundTxResponse => + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight, add_excess_to_recipient_position = None, max_excess = None), feeBudget_opt = None).flatMap { fundTxResponse => // Bitcoin Core may not preserve the order of inputs, we need to make sure the anchor is the first input. val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == anchorTx.txInfo.input.outPoint) // We merge our dummy change output with the one added by Bitcoin Core, if any. 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 a3c6c04863..672747aea4 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 @@ -50,7 +50,7 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], addExcessToRecipientPosition_opt: Option[Int], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { funded += (tx.txid -> tx) Future.successful(FundTransactionResponse(tx, 0 sat, None)) } @@ -62,7 +62,7 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { Future.successful(tx.txid) } - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) funded += (tx.fundingTx.txid -> tx.fundingTx) Future.successful(tx) @@ -105,13 +105,13 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], addExcessToRecipientPosition_opt: Option[Int], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed override def signPsbt(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int])(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = Promise().future // will never be completed override def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId] = Future.successful(tx.txid) - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) @@ -152,7 +152,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi], addExcessToRecipientPosition_opt: Option[Int], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { val currentAmountIn = tx.txIn.flatMap(txIn => inputs.find(_.txid == txIn.outPoint.txid).flatMap(_.txOut.lift(txIn.outPoint.index.toInt))).map(_.amount).sum val amountOut = tx.txOut.map(_.amount).sum // We add a single input to reach the desired feerate. @@ -213,10 +213,10 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { Future.successful(ProcessPsbtResponse(signedPsbt, complete)) } - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val tx = Transaction(2, Nil, Seq(TxOut(amount, pubkeyScript)), 0) for { - fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt) + fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt, addExcessToRecipientPosition_opt = Some(0), maxExcess_opt = maxExcess_opt) signedTx <- signTransaction(fundedTx.tx) } yield MakeFundingTxResponse(signedTx.tx, 0, fundedTx.fee) } From 7b8b2ae4c52c07f83472b9519716245f1ea2aff3 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Mon, 5 Aug 2024 13:51:42 +0200 Subject: [PATCH 3/3] Update single funded channel to select inputs before sending open - makeFundingTx now just selects inputs and change outputs - (new) signFundingTx replaces the funding output script and signs --- .../eclair/blockchain/OnChainWallet.scala | 9 +- .../bitcoind/rpc/BitcoinCoreClient.scala | 40 +++-- .../fr/acinq/eclair/channel/ChannelData.scala | 19 +-- .../channel/fsm/ChannelOpenSingleFunded.scala | 74 ++++++--- .../acinq/eclair/io/PeerReadyNotifier.scala | 1 + .../acinq/eclair/json/JsonSerializers.scala | 1 + .../blockchain/DummyOnChainWallet.scala | 27 +++- .../bitcoind/BitcoinCoreClientSpec.scala | 36 +++-- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 9 +- .../a/WaitForAcceptChannelStateSpec.scala | 41 ++--- .../a/WaitForOpenChannelStateSpec.scala | 1 + .../b/WaitForFundingInternalStateSpec.scala | 17 +- ...aitForFundingSignedInternalStateSpec.scala | 145 ++++++++++++++++++ .../AnnouncementsBatchValidationSpec.scala | 6 +- 14 files changed, 332 insertions(+), 94 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.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 12606ad9f7..70bfc4ca8d 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 @@ -54,10 +54,13 @@ trait OnChainChannelFunder { */ def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId] - /** Create a fully signed channel funding transaction with the provided pubkeyScript. */ + /** Create an unsigned channel funding transaction with the provided dummy pubkeyScript. */ def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] - /** + /** Sign a funding transaction with an updated pubkeyScript. */ + def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] + + /** * Committing *must* include publishing the transaction on the network. * * We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the @@ -155,6 +158,8 @@ object OnChainWallet { final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) + final case class SignFundingTxResponse(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 } 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 3bb199f9ac..b4082a4353 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 @@ -22,11 +22,10 @@ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.{Bech32, Block, SigHash} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw} import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager -import fr.acinq.eclair.json.SatoshiSerializer import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol.ChannelAnnouncement import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates} @@ -310,7 +309,28 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None, maxExcess_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { - def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw): Future[MakeFundingTxResponse] = { + val partialFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = TxOut(amount, pubkeyScript) :: Nil, + lockTime = 0) + + for { + // TODO: we should check that mempoolMinFee is not dangerously high + feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) + // we ask bitcoin core to add inputs to the funding tx, and use the specified change address + FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) + fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match { + case Left(_) => return Future.failed(new RuntimeException("cannot find expected funding output: bitcoin core may be malicious")) + case Right(outputIndex) => outputIndex + } + makeFundingTxResponse = MakeFundingTxResponse(tx, fundingOutputIndex, fee) + } yield makeFundingTxResponse + } + + def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + + def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { import KotlinUtils._ val fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match { @@ -331,22 +351,16 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag maxFeerate = requestedFeeRate * 1.5 _ = require(actualFeerate < maxFeerate, s"actual feerate $actualFeerate is more than 50% above requested feerate $targetFeerate") _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$fundingOutputIndex fee=$fees") - } yield MakeFundingTxResponse(fundingTx, fundingOutputIndex, fees) + } yield SignFundingTxResponse(fundingTx, fundingOutputIndex, fees) } - val partialFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = TxOut(amount, pubkeyScript) :: Nil, - lockTime = 0) + val tx1 = tx.copy(txOut = tx.txOut.updated(outputIndex, tx.txOut(outputIndex).copy(publicKeyScript = pubkeyScript))) for { // TODO: we should check that mempoolMinFee is not dangerously high feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) - // we ask bitcoin core to add inputs to the funding tx, and use the specified change address - FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) - lockedUtxos = tx.txIn.map(_.outPoint) - signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, fee, feerate)) + lockedUtxos = tx1.txIn.map(_.outPoint) + signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx1, fee, feerate)) } yield signedTx } 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 dcfd390d66..46d6542e81 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful, typed} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ @@ -53,6 +54,7 @@ sealed trait ChannelState case object WAIT_FOR_INIT_INTERNAL extends ChannelState // Single-funder channel opening: case object WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_FUNDING_SIGNED_INTERNAL extends ChannelState case object WAIT_FOR_OPEN_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState @@ -522,17 +524,11 @@ sealed trait ChannelDataWithCommitments extends PersistentChannelData { 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_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel, fundingTxResponse: MakeFundingTxResponse) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams, - fundingAmount: Satoshi, - pushAmount: MilliSatoshi, - commitTxFeerate: FeeratePerKw, - remoteFundingPubKey: PublicKey, - remoteFirstPerCommitmentPoint: PublicKey, - replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { - val channelId: ByteVector32 = params.channelId +final case class DATA_WAIT_FOR_FUNDING_INTERNAL(input: INPUT_INIT_CHANNEL_INITIATOR) extends TransientChannelData { + val channelId: ByteVector32 = input.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams, fundingAmount: Satoshi, @@ -567,6 +563,11 @@ final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL 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_FUNDING_SIGNED_INTERNAL(lastFundingTx: Transaction, params: ChannelParams, fundingAmount: Satoshi, + pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey, + remoteFirstPerCommitmentPoint: PublicKey, replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { + val channelId: ByteVector32 = params.channelId +} final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32, channelParams: ChannelParams, secondRemotePerCommitmentPoint: PublicKey, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 673982afa9..e2253098d1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -20,7 +20,7 @@ import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} -import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx @@ -73,6 +73,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => + val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey + val dummyPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingPubKey, fundingPubKey))) + wallet.makeFundingTx(dummyPubkeyScript, input.fundingAmount, input.fundingTxFeerate, input.fundingTxFeeBudget_opt, maxExcess_opt = input.maxExcess_opt).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(input) + }) + + when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { + case Event(makeFundingTxResponse@MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, _), DATA_WAIT_FOR_FUNDING_INTERNAL(input)) => val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used @@ -81,7 +89,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val open = OpenChannel( chainHash = nodeParams.chainHash, temporaryChannelId = input.temporaryChannelId, - fundingSatoshis = input.fundingAmount, + fundingSatoshis = fundingTx.txOut(fundingTxOutputIndex).amount, pushMsat = input.pushAmount_opt.getOrElse(0 msat), dustLimitSatoshis = input.localParams.dustLimit, maxHtlcValueInFlightMsat = UInt64(input.localParams.maxHtlcValueInFlightMsat.toLong), @@ -101,7 +109,28 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), ChannelTlv.ChannelTypeTlv(input.channelType) )) - goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open + goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open, makeFundingTxResponse) sending open + + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + log.error(t, s"wallet returned error: ") + d.input.replyTo ! OpenChannelResponse.Rejected(s"wallet error: ${t.getMessage}") + handleLocalError(ChannelFundingError(d.channelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.Cancelled + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.Disconnected + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.TimedOut + goto(CLOSED) }) when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { @@ -162,9 +191,10 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open, fundingTxResponse)) => Helpers.validateParamsSingleFundedFunder(nodeParams, init.channelType, init.localParams.initFeatures, init.remoteInit.features, open, accept) match { case Left(t) => + wallet.rollback(fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Rejected(t.getMessage) handleLocalError(t, d, Some(accept)) case Right((channelFeatures, remoteShutdownScript)) => @@ -186,34 +216,40 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath, fundingTxIndex = 0) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) - wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt, maxExcess_opt = None).pipeTo(self) + wallet.signFundingTx(fundingTxResponse.fundingTx, fundingPubkeyScript, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.fee, init.fundingTxFeerate).pipeTo(self) val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) + goto(WAIT_FOR_FUNDING_SIGNED_INTERNAL) using DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(fundingTxResponse.fundingTx, params, open.fundingSatoshis, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, init.replyTo) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.lastSent.temporaryChannelId) case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) }) - when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, replyTo)) => + when(WAIT_FOR_FUNDING_SIGNED_INTERNAL)(handleExceptions { + case Event(SignFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(_, params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, replyTo)) => val temporaryChannelId = params.channelId // let's create the first commitment tx that spends the yet uncommitted funding tx Funding.makeFirstCommitTxs(keyManager, params, localFundingAmount = fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = pushMsat, remotePushAmount = 0 msat, commitTxFeerate, fundingTx.txid, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remoteFirstPerCommitmentPoint = remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) + case Left(ex) => + wallet.rollback(fundingTx) + handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat) @@ -233,24 +269,29 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(params1, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), fundingCreated, replyTo) sending fundingCreated } - case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) log.error(t, s"wallet returned error: ") d.replyTo ! OpenChannelResponse.Rejected(s"wallet error: ${t.getMessage}") handleLocalError(ChannelFundingError(d.channelId), d, None) // we use a generic exception and don't send the internal error to the peer - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.channelId) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) - case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) }) @@ -317,7 +358,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => - // we rollback the funding tx, it will never be published wallet.rollback(fundingTx) d.replyTo ! OpenChannelResponse.Rejected(cause.getMessage) handleLocalError(InvalidCommitmentSignature(d.channelId, fundingTx.txid, fundingTxIndex = 0, localCommitTx.tx), d, Some(msg)) @@ -349,25 +389,21 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.channelId) case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala index 81d6c71b5c..8f0af4f9a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala @@ -179,6 +179,7 @@ object PeerReadyNotifier { case channel.WAIT_FOR_OPEN_CHANNEL => true case channel.WAIT_FOR_ACCEPT_CHANNEL => true case channel.WAIT_FOR_FUNDING_INTERNAL => true + case channel.WAIT_FOR_FUNDING_SIGNED_INTERNAL => true case channel.WAIT_FOR_FUNDING_CREATED => true case channel.WAIT_FOR_FUNDING_SIGNED => true case channel.WAIT_FOR_FUNDING_CONFIRMED => true 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 90e07ca162..5074cf9ba9 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 @@ -630,6 +630,7 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_OPEN_CHANNEL], classOf[DATA_WAIT_FOR_ACCEPT_CHANNEL], classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], + classOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], 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 672747aea4..1b7f4100cf 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 @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.SignTransactionResponse import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.randomKey @@ -65,7 +65,13 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) funded += (tx.fundingTx.txid -> tx.fundingTx) - Future.successful(tx) + Future.successful(MakeFundingTxResponse(tx.fundingTx, tx.fundingTxOutputIndex, tx.fee)) + } + + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + val tx1 = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, tx.txOut(outputIndex).amount) + funded += (tx1.fundingTx.txid -> tx1.fundingTx) + Future.successful(tx1) } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = publishTransaction(tx).map(_ => true) @@ -113,6 +119,8 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = Promise().future // will never be completed + override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) override def getTransaction(txId: TxId)(implicit ec: ExecutionContext): Future[Transaction] = Promise().future // will never be completed @@ -217,8 +225,15 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { val tx = Transaction(2, Nil, Seq(TxOut(amount, pubkeyScript)), 0) for { fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt, addExcessToRecipientPosition_opt = Some(0), maxExcess_opt = maxExcess_opt) - signedTx <- signTransaction(fundedTx.tx) - } yield MakeFundingTxResponse(signedTx.tx, 0, fundedTx.fee) + } yield MakeFundingTxResponse(fundedTx.tx, 0, fundedTx.fee) + } + + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + val txOut1 = tx.txOut.updated(outputIndex, tx.txOut(outputIndex).copy(publicKeyScript = pubkeyScript)) + val fundingTx1 = tx.copy(txOut = txOut1) + for { + signedTx <- signTransaction(fundingTx1) + } yield SignFundingTxResponse(signedTx.tx, outputIndex, fee) } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) @@ -254,14 +269,14 @@ object DummyOnChainWallet { val dummyReceiveAddress: String = "bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux" val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") - def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { + def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): SignFundingTxResponse = { val fundingTx = Transaction( version = 2, txIn = TxIn(OutPoint(TxId.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101"), 42), signatureScript = Nil, sequence = SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0 ) - MakeFundingTxResponse(fundingTx, 0, 420 sat) + SignFundingTxResponse(fundingTx, 0, 420 sat) } } \ No newline at end of file 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 fd091a0803..7136591c5c 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 @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, der2compact} import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcDouble, Crypto, DeterministicWallet, MilliBtcDouble, MnemonicCode, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, addressFromPublicKeyScript, addressToPublicKeyScript, computeBIP84Address, computeP2PkhAddress, computeP2WpkhAddress} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.{BitcoinReq, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ @@ -60,6 +60,12 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A stopBitcoind() } + def makeFundingTx(bitcoinCoreClient: BitcoinCoreClient, sender: TestProbe, pubkeyScript: ByteVector, amount: Btc, feeratePerKw: FeeratePerKw): Future[SignFundingTxResponse] = { + bitcoinCoreClient.makeFundingTx(pubkeyScript, amount, feeratePerKw).pipeTo(sender.ref) + val res = sender.expectMsgType[MakeFundingTxResponse] + bitcoinCoreClient.signFundingTx(res.fundingTx, pubkeyScript, res.fundingTxOutputIndex, res.fee, feeratePerKw).pipeTo(sender.ref) + } + test("encrypt wallet") { assume(!useEclairSigner) @@ -71,7 +77,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A restartBitcoind(sender) val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) - bitcoinClient.makeFundingTx(pubkeyScript, 50 millibtc, FeeratePerKw(10000 sat)).pipeTo(sender.ref) + makeFundingTx(bitcoinClient, sender, pubkeyScript, Btc(0.1), FeeratePerKw(250 sat)) val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) @@ -382,16 +388,18 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val fundingTxs = for (_ <- 0 to 3) yield { val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) - bitcoinClient.makeFundingTx(pubkeyScript, Satoshi(500), FeeratePerKw(250 sat)).pipeTo(sender.ref) - val fundingTx = sender.expectMsgType[MakeFundingTxResponse].fundingTx + makeFundingTx(bitcoinClient, sender, pubkeyScript, Satoshi(500), FeeratePerKw(250 sat)) + + val fundingTx = sender.expectMsgType[SignFundingTxResponse].fundingTx bitcoinClient.publishTransaction(fundingTx.copy(txIn = Nil)).pipeTo(sender.ref) // try publishing an invalid version of the tx sender.expectMsgType[Failure] bitcoinClient.rollback(fundingTx).pipeTo(sender.ref) // rollback the locked outputs sender.expectMsg(true) // now fund a tx with correct feerate - bitcoinClient.makeFundingTx(pubkeyScript, 50 millibtc, FeeratePerKw(250 sat)).pipeTo(sender.ref) - sender.expectMsgType[MakeFundingTxResponse].fundingTx + makeFundingTx(bitcoinClient, sender, pubkeyScript, 50 millibtc, FeeratePerKw(250 sat)) + + sender.expectMsgType[SignFundingTxResponse].fundingTx } bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) @@ -426,8 +434,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) // 200 sat/kw is below the min-relay-fee - bitcoinClient.makeFundingTx(pubkeyScript, 5 millibtc, FeeratePerKw(200 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, pubkeyScript, 5 millibtc, FeeratePerKw(200 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] bitcoinClient.commit(fundingTx).pipeTo(sender.ref) sender.expectMsg(true) @@ -501,8 +509,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = makeBitcoinCoreClient() // create a huge tx so we make sure it has > 2 inputs - bitcoinClient.makeFundingTx(pubkeyScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, outputIndex, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, pubkeyScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, outputIndex, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.length > 2) // spend the first 2 inputs @@ -732,8 +740,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A { // test #1: unlock wallet outpoints that are actually locked // create a huge tx so we make sure it has > 1 inputs - bitcoinClient.makeFundingTx(nonWalletScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, nonWalletScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.size > 2) bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) sender.expectMsg(fundingTx.txIn.map(_.outPoint).toSet) @@ -742,8 +750,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } { // test #2: some outpoints are locked, some are unlocked - bitcoinClient.makeFundingTx(nonWalletScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, nonWalletScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.size > 2) bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) sender.expectMsg(fundingTx.txIn.map(_.outPoint).toSet) 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 3cc68fb47e..abfc9b7929 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 @@ -23,7 +23,7 @@ import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxId, TxOut} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.WatcherSpec._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.SignTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -36,6 +36,7 @@ import fr.acinq.eclair.{BlockHeight, RealShortChannelId, TestConstants, TestKitB import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike +import scodec.bits.ByteVector import java.util.UUID import java.util.concurrent.atomic.AtomicLong @@ -174,8 +175,10 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchPublished(probe.ref, publishedTx.txid) probe.expectMsg(WatchPublishedTriggered(publishedTx)) - bitcoinClient.makeFundingTx(Script.write(Script.pay2wpkh(randomKey().publicKey)), 150_000 sat, FeeratePerKw(2500 sat)).pipeTo(probe.ref) - val unpublishedTx = probe.expectMsgType[MakeFundingTxResponse].fundingTx + bitcoinClient.makeFundingTx(ByteVector.empty, 150_000 sat, FeeratePerKw(2500 sat)).pipeTo(probe.ref) + val res = probe.expectMsgType[MakeFundingTxResponse] + bitcoinClient.signFundingTx(res.fundingTx, Script.write(Script.pay2wpkh(randomKey().publicKey)), res.fundingTxOutputIndex, res.fee, FeeratePerKw(2500 sat)).pipeTo(probe.ref) + val unpublishedTx = probe.expectMsgType[SignFundingTxResponse].fundingTx watcher ! WatchPublished(probe.ref, unpublishedTx.txid) probe.expectNoMessage(100 millis) 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 6a25ac5c40..8330e5fda6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.channel.states.a import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -67,6 +68,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) val fundingAmount = if (test.tags.contains(LargeChannel)) Btc(5).toSatoshi else TestConstants.fundingSatoshis alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, maxExcess_opt = None, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(fundingAmount, ByteVector.empty)), 0), 0, 1 sat) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -82,7 +84,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.upfrontShutdownScript_opt.contains(ByteVector.empty)) assert(accept.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) aliceOpenReplyTo.expectNoMessage() } @@ -91,8 +93,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -101,8 +103,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -111,8 +113,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -123,8 +125,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but // they both activated anchor outputs so it is the default choice anyway. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -145,8 +147,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Alice asked for a standard channel whereas they both support anchor outputs. assert(accept.channelType_opt.contains(ChannelTypes.Standard())) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == DefaultCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == DefaultCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -174,14 +176,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs()) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs())) alice2bob.forward(bob, open) val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -303,7 +306,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.minimumDepth == 13) // with large channel tag we create a 5BTC channel bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) aliceOpenReplyTo.expectNoMessage() } @@ -312,8 +315,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) aliceOpenReplyTo.expectNoMessage() } @@ -323,8 +326,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) bob2alice.forward(alice, accept1) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt.isEmpty) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.remoteParams.upfrontShutdownScript_opt.isEmpty) aliceOpenReplyTo.expectNoMessage() } 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 ab0495da97..d47b4ecbb2 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 @@ -278,6 +278,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.upfrontShutdownScript_opt.get)) alice2bob.forward(bob, open) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) 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 e4565e2f59..c6edd91e0f 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 @@ -19,8 +19,9 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -30,6 +31,7 @@ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{TestConstants, TestKitBaseClass} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -47,22 +49,23 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = false) val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) withFixture(test.toNoArgTest(FixtureParam(alice, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, listener))) } } + test("recv MakeFundingTxResponse (funding success)") { f => + import f._ + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) + alice2bob.expectMsgType[OpenChannel] + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) + } + test("recv Status.Failure (wallet error)") { f => import f._ alice ! Status.Failure(new RuntimeException("insufficient funds")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala new file mode 100644 index 0000000000..961ba05336 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala @@ -0,0 +1,145 @@ +/* + * Copyright 2024 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.actor.typed.scaladsl.adapter.ClassicActorRefOps +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, SignFundingTxResponse} +import fr.acinq.eclair.blockchain.{NoOpOnChainWallet, SingleKeyOnChainWallet} +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 +import fr.acinq.eclair.io.Peer.OpenChannelResponse +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{TestConstants, TestKitBaseClass} +import org.scalatest.Outcome +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import scodec.bits.ByteVector + +import scala.concurrent.duration._ + +/** + * Created by remyers on 05/08/2024. + */ + +class WaitForFundingSignedInternalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], + bob: TestFSMRef[ChannelState, ChannelData, Channel], + wallet: NoOpOnChainWallet, + aliceOpenReplyTo: TestProbe, + alice2bob: TestProbe, + bob2alice: TestProbe, + alice2blockchain: TestProbe, + listener: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val walletA = new NoOpOnChainWallet() + val setup = init(wallet_opt = Some(walletA), tags = test.tags) + import setup._ + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags(announceChannel = false) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + val listener = TestProbe() + within(30 seconds) { + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, walletA, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, listener))) + } + } + + def makeFundingSignedResponse(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel]): SignFundingTxResponse = { + val aliceFundingPubkey = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteFundingPubKey + val bobFundingPubkey = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].remoteFundingPubKey + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(aliceFundingPubkey, bobFundingPubkey))) + val fundingTx = Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, fundingPubkeyScript)), 0) + SignFundingTxResponse(fundingTx, 0, 1 sat) + } + + test("recv SignFundingTxResponse (funding signed success)") { f => + import f._ + alice ! makeFundingSignedResponse(alice, bob) + alice2bob.expectMsgType[FundingCreated] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) + aliceOpenReplyTo.expectNoMessage(100 millis) + assert(wallet.rolledback.isEmpty) + } + + test("recv Status.Failure (wallet error)") { f => + import f._ + alice ! Status.Failure(new RuntimeException("insufficient funds")) + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] + aliceOpenReplyTo.expectNoMessage(100 millis) + awaitCond(wallet.rolledback.size == 1) + } + + test("recv Error") { f => + import f._ + alice ! Error(ByteVector32.Zeroes, "oops") + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.RemoteError] + awaitCond(wallet.rolledback.length == 1) + } + + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) + awaitCond(wallet.rolledback.length == 1) + } + + test("recv INPUT_DISCONNECTED") { f => + import f._ + alice ! INPUT_DISCONNECTED + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.Disconnected) + awaitCond(wallet.rolledback.length == 1) + } + + test("recv TickChannelOpenTimeout") { f => + import f._ + alice ! TickChannelOpenTimeout + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.TimedOut) + awaitCond(wallet.rolledback.length == 1) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index b5d7775a4f..a722abce43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -95,8 +95,10 @@ object AnnouncementsBatchValidationSpec { val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey))) val fundingTxFuture = bitcoinClient.makeFundingTx(fundingPubkeyScript, amount, FeeratePerKw(10000 sat)) val res = Await.result(fundingTxFuture, 10 seconds) - Await.result(bitcoinClient.publishTransaction(res.fundingTx), 10 seconds) - SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res.fundingTx, res.fundingTxOutputIndex) + val signedTxFuture = bitcoinClient.signFundingTx(res.fundingTx, fundingPubkeyScript, res.fundingTxOutputIndex, res.fee, FeeratePerKw(10000 sat)) + val res1 = Await.result(signedTxFuture, 10 seconds) + Await.result(bitcoinClient.publishTransaction(res1.fundingTx), 10 seconds) + SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res1.fundingTx, res1.fundingTxOutputIndex) } def makeChannelAnnouncement(c: SimulatedChannel, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): ChannelAnnouncement = {