From 568e04d813cf791a185cb541bb301b1fd99261be Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 5 Jan 2024 18:30:03 +0100 Subject: [PATCH 1/4] add a funding fee budget This is based on #2780, with a slightly different implementation. The fee is checked right after calling `fundrawtransaction`, which makes it work for both the single and dual funding cases. Also we use the `feeBudget` terminology instead of `maxFee`. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 11 ++- .../eclair/blockchain/OnChainWallet.scala | 4 +- .../bitcoind/rpc/BitcoinCoreClient.scala | 18 +++-- .../fr/acinq/eclair/channel/ChannelData.scala | 1 + .../channel/fsm/ChannelOpenDualFunded.scala | 4 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 2 +- .../channel/fund/InteractiveTxFunder.scala | 6 +- .../channel/publish/ReplaceableTxFunder.scala | 4 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 3 +- .../fr/acinq/eclair/EclairImplSpec.scala | 12 ++-- .../blockchain/DummyOnChainWallet.scala | 14 ++-- .../bitcoind/BitcoinCoreClientSpec.scala | 70 ++++++++++--------- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 2 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../channel/InteractiveTxBuilderSpec.scala | 4 +- .../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 ++--- 38 files changed, 136 insertions(+), 112 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 e015c773ac..d3295d9a79 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -87,7 +87,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], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] + 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 rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] @@ -205,9 +205,13 @@ 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], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[OpenChannelResponse] = { + 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] = { // 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)) + val fundingFeeBudget = fundingFeeBudget_opt.getOrElse{ + val proportionalFee = Helpers.getRelayFees(appKit.nodeParams, nodeId, announceChannel_opt.getOrElse(appKit.nodeParams.channelConf.channelFlags.announceChannel)).feeProportionalMillionths.toDouble / 1e6 + fundingAmount * proportionalFee + } for { _ <- Future.successful(0) open = Peer.OpenChannel( @@ -215,7 +219,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { fundingAmount = fundingAmount, channelType_opt = channelType_opt, pushAmount_opt = pushAmount_opt, - fundingTxFeerate_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), + fundingTxFeerate_opt = fundingFeerate_opt.map(FeeratePerKw(_)), + fundingTxFeeBudget_opt = Some(fundingFeeBudget), channelFlags_opt = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)), timeout_opt = Some(openTimeout)) res <- (appKit.switchboard ? open).mapTo[OpenChannelResponse] 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 dc9c1a0f6f..45068ec1e1 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)(implicit ec: ExecutionContext): Future[FundTransactionResponse] + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_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)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_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 376a160e72..71134d9abd 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 @@ -26,6 +26,7 @@ import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFu 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} @@ -233,7 +234,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag //------------------------- FUNDING -------------------------// - def fundTransaction(tx: Transaction, options: FundTransactionOptions)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + /** + * @param feeBudget max allowed fee, if the transaction returned by bitcoin core has a higher fee a funding error is returned. + */ + def fundTransaction(tx: Transaction, options: FundTransactionOptions, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { rpcClient.invoke("fundrawtransaction", tx.toString(), options).flatMap(json => { val JString(hex) = json \ "hex" val JInt(changePos) = json \ "changepos" @@ -243,12 +247,14 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag val walletInputs = fundedTx.txIn.map(_.outPoint).toSet -- tx.txIn.map(_.outPoint).toSet val addedOutputs = fundedTx.txOut.size - tx.txOut.size + val feeSat = toSatoshi(fee) Try { require(addedOutputs <= 1, "more than one change output added") require(addedOutputs == 0 || changePos >= 0, "change output added, but position not returned") require(options.changePosition.isEmpty || changePos_opt.isEmpty || changePos_opt == options.changePosition, "change output added at wrong position") + feeBudget_opt.foreach(feeBudget => require(feeSat <= feeBudget, s"mining fee is higher than budget ($feeSat > $feeBudget)")) - FundTransactionResponse(fundedTx, toSatoshi(fee), changePos_opt) + FundTransactionResponse(fundedTx, feeSat, changePos_opt) } match { case Success(response) => Future.successful(response) case Failure(error) => unlockOutpoints(walletInputs.toSeq).flatMap(_ => Future.failed(error)) @@ -256,8 +262,8 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag }) } - def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { - fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq)) + 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) } private def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Option[Int] = None)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = { @@ -302,7 +308,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag } } - def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw): Future[MakeFundingTxResponse] = { import KotlinUtils._ @@ -338,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)) + FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate), feeBudget_opt = feeBudget_opt) lockedUtxos = tx.txIn.map(_.outPoint) signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, 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 18163fce05..bf296a9b69 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 @@ -96,6 +96,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32, dualFunded: Boolean, commitTxFeerate: FeeratePerKw, fundingTxFeerate: FeeratePerKw, + fundingTxFeeBudget_opt: Option[Satoshi], pushAmount_opt: Option[MilliSatoshi], requireConfirmedInputs: Boolean, localParams: LocalParams, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 833089a610..9ed1e21615 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -212,7 +212,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { targetFeerate = open.fundingFeerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = open.requireConfirmedInputs, forRemote = accept.requireConfirmedInputs) ) - val purpose = InteractiveTxBuilder.FundingTx(open.commitmentFeerate, open.firstPerCommitmentPoint) + val purpose = InteractiveTxBuilder.FundingTx(open.commitmentFeerate, open.firstPerCommitmentPoint, feeBudget_opt = None) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, @@ -275,7 +275,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { targetFeerate = d.lastSent.fundingFeerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = accept.requireConfirmedInputs, forRemote = d.lastSent.requireConfirmedInputs) ) - val purpose = InteractiveTxBuilder.FundingTx(d.lastSent.commitmentFeerate, accept.firstPerCommitmentPoint) + val purpose = InteractiveTxBuilder.FundingTx(d.lastSent.commitmentFeerate, accept.firstPerCommitmentPoint, feeBudget_opt = d.init.fundingTxFeeBudget_opt) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( randomBytes32(), nodeParams, fundingParams, 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 e9990229ef..0740907597 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).pipeTo(self) + wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt).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/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 955800aceb..f634ef4e99 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -169,7 +169,7 @@ object InteractiveTxBuilder { def localHtlcs: Set[DirectedHtlc] def htlcBalance: MilliSatoshi = localHtlcs.toSeq.map(_.add.amountMsat).sum } - case class FundingTx(commitTxFeerate: FeeratePerKw, remotePerCommitmentPoint: PublicKey) extends Purpose { + case class FundingTx(commitTxFeerate: FeeratePerKw, remotePerCommitmentPoint: PublicKey, feeBudget_opt: Option[Satoshi]) extends Purpose { override val previousLocalBalance: MilliSatoshi = 0 msat override val previousRemoteBalance: MilliSatoshi = 0 msat override val previousFundingAmount: Satoshi = 0 sat 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 ef063dcd9b..6571879164 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 @@ -189,7 +189,11 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response */ private def fund(txNotFunded: Transaction, currentInputs: Seq[OutgoingInput], unusableInputs: Set[UnusableInput]): Behavior[Command] = { val sharedInputWeight = fundingParams.sharedInput_opt.toSeq.map(i => i.info.outPoint -> i.weight.toLong).toMap - context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight)) { + val feeBudget_opt = purpose match { + case p: FundingTx => p.feeBudget_opt + case _ => None + } + context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt)) { 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 b606495414..bdd663d09e 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 @@ -438,7 +438,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, // The anchor transaction is paying for the weight of the commitment transaction. // We remove the weight of the artificially added change output, because we will remove that output after funding. val anchorWeight = Seq(InputWeight(anchorTx.txInfo.input.outPoint, anchorInputWeight + commitTxWeight - KotlinUtils.scala2kmp(dummyChangeOutput).weight())) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight)).flatMap { fundTxResponse => + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight), 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) // The commitment transaction was already paying some fees that we're paying again in the anchor transaction since @@ -464,7 +464,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessInputWeight case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutInputWeight }) - bitcoinClient.fundTransaction(htlcTx.txInfo.tx, FundTransactionOptions(targetFeerate, changePosition = Some(1), inputWeights = Seq(htlcInputWeight))).map(fundTxResponse => { + bitcoinClient.fundTransaction(htlcTx.txInfo.tx, FundTransactionOptions(targetFeerate, changePosition = Some(1), inputWeights = Seq(htlcInputWeight)), feeBudget_opt = None).map(fundTxResponse => { // Bitcoin Core may not preserve the order of inputs, we need to make sure the htlc is the first input. val fundedTx = fundTxResponse.tx.copy(txIn = htlcTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == htlcTx.txInfo.input.outPoint)) (htlcTx.updateTx(fundedTx), fundTxResponse.amountIn) 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 63993e5291..4a885b1ddc 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 @@ -164,7 +164,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnchainP 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.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, 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) => @@ -505,6 +505,7 @@ object Peer { channelType_opt: Option[SupportedChannelType], pushAmount_opt: Option[MilliSatoshi], fundingTxFeerate_opt: Option[FeeratePerKw], + fundingTxFeeBudget_opt: Option[Satoshi], channelFlags_opt: Option[ChannelFlags], timeout_opt: Option[Timeout], requireConfirmedInputsOverride_opt: Option[Boolean] = None, 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 57acb2891a..dd82272995 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, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), announceChannel_opt = None, openTimeout_opt = None) + 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) 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()), fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), announceChannel_opt = None, openTimeout_opt = None) + 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) 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, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, 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, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, 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, announceChannel_opt = Some(true), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, 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, announceChannel_opt = Some(false), None).pipeTo(sender.ref) + eclair.open(randomKey().publicKey, 123456 sat, 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/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 4e02548273..908f26ce3d 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 @@ -49,7 +49,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])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { funded += (tx.txid -> tx) Future.successful(FundTransactionResponse(tx, 0 sat, None)) } @@ -61,7 +61,7 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { Future.successful(tx.txid) } - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) funded += (tx.fundingTx.txid -> tx.fundingTx) Future.successful(tx) @@ -96,13 +96,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])(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])(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)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed + 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 commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) @@ -134,7 +134,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])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_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. @@ -189,10 +189,10 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { Future.successful(ProcessPsbtResponse(signedPsbt, complete)) } - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { + override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_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) + fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt) signedTx <- signTransaction(fundedTx.tx) } yield MakeFundingTxResponse(signedTx.tx, 0, fundedTx.fee) } 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 2ad2874949..2fd27b9703 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -22,7 +22,7 @@ import akka.testkit.TestProbe import fr.acinq.bitcoin import fr.acinq.bitcoin.psbt.{Psbt, UpdateFailure} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcDouble, ByteVector32, 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.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} @@ -86,7 +86,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txToRemote = { val txNotFunded = Transaction(2, Nil, TxOut(150000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.changePosition.nonEmpty) assert(fundTxResponse.fee > 0.sat) @@ -112,22 +112,28 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A { // txs with no outputs are not supported. val emptyTx = Transaction(2, Nil, Nil, 0) - bitcoinClient.fundTransaction(emptyTx, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(emptyTx, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) sender.expectMsgType[Failure] } { // bitcoind requires that "all existing inputs must have their previous output transaction be in the wallet". val txNonWalletInputs = Transaction(2, Seq(TxIn(OutPoint(txToRemote, 0), Nil, 0), TxIn(OutPoint(txToRemote, 1), Nil, 0)), Seq(TxOut(100000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - bitcoinClient.fundTransaction(txNonWalletInputs, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNonWalletInputs, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) + sender.expectMsgType[Failure] + } + { + // mining fee must be below budget + val txNotFunded = Transaction(2, Nil, TxOut(150000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = Some(100.sat)).pipeTo(sender.ref) sender.expectMsgType[Failure] } { // we can increase the feerate. - bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse1 = sender.expectMsgType[FundTransactionResponse] bitcoinClient.rollback(fundTxResponse1.tx).pipeTo(sender.ref) sender.expectMsg(true) - bitcoinClient.fundTransaction(fundTxResponse1.tx, FundTransactionOptions(TestConstants.feeratePerKw * 2)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(fundTxResponse1.tx, FundTransactionOptions(TestConstants.feeratePerKw * 2), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse2 = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse1.tx != fundTxResponse2.tx) assert(fundTxResponse1.fee < fundTxResponse2.fee) @@ -137,7 +143,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A { // we can control where the change output is inserted and opt-out of RBF. val txManyOutputs = Transaction(2, Nil, TxOut(410000 sat, Script.pay2wpkh(randomKey().publicKey)) :: TxOut(230000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) - bitcoinClient.fundTransaction(txManyOutputs, FundTransactionOptions(TestConstants.feeratePerKw, replaceable = false, changePosition = Some(1))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txManyOutputs, FundTransactionOptions(TestConstants.feeratePerKw, replaceable = false, changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.tx.txOut.size == 3) assert(fundTxResponse.changePosition.contains(1)) @@ -170,19 +176,19 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A { // bitcoin core doesn't specify change position. val evilBitcoinClient = makeEvilBitcoinClient(_ => -1, tx => tx) - evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) sender.expectMsgType[Failure] } { // bitcoin core tries to send twice the amount we wanted by duplicating the output. val evilBitcoinClient = makeEvilBitcoinClient(pos => pos, tx => tx.copy(txOut = tx.txOut ++ txNotFunded.txOut)) - evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) sender.expectMsgType[Failure] } { // bitcoin core ignores our specified change position. val evilBitcoinClient = makeEvilBitcoinClient(_ => 1, tx => tx.copy(txOut = tx.txOut.reverse)) - evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw, changePosition = Some(0))).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw, changePosition = Some(0)), feeBudget_opt = None).pipeTo(sender.ref) sender.expectMsgType[Failure] } } @@ -208,7 +214,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val (outpoint1, inputScript1, txOut1) = { val script = Script.createMultiSigMofN(1, Seq(alicePriv.publicKey, bobPriv.publicKey)) val txNotFunded = Transaction(2, Nil, Seq(TxOut(250_000 sat, Script.pay2wsh(script))), 0) - defaultWallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(2500 sat), changePosition = Some(1))).pipeTo(sender.ref) + defaultWallet.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(2500 sat), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx defaultWallet.signPsbt(new Psbt(fundedTx), fundedTx.txIn.indices, Nil).pipeTo(sender.ref) val signedTx = sender.expectMsgType[ProcessPsbtResponse].finalTx_opt.toOption.get @@ -227,7 +233,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txNotFunded = Transaction(2, Seq(TxIn(outpoint1, Nil, 0)), Seq(TxOut(300_000 sat, Script.pay2wsh(outputScript))), 0) val smallExternalInputWeight = 200 assert(smallExternalInputWeight < externalInputWeight) - walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(outpoint1, smallExternalInputWeight)), changePosition = Some(1))).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(outpoint1, smallExternalInputWeight)), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx1 = sender.expectMsgType[FundTransactionResponse] assert(fundedTx1.tx.txIn.length >= 2) val amountIn1 = fundedTx1.tx.txIn.map(txIn => { @@ -236,7 +242,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A }).sum assert(amountIn1 == fundedTx1.amountIn) // If we specify a bigger weight, bitcoind uses a bigger fee. - walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(outpoint1, externalInputWeight)), changePosition = Some(1))).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(outpoint1, externalInputWeight)), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx2 = sender.expectMsgType[FundTransactionResponse] assert(fundedTx2.tx.txIn.length >= 2) assert(fundedTx1.fee < fundedTx2.fee) @@ -272,7 +278,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val targetFeerate = FeeratePerKw(10_000 sat) val externalOutpoint = OutPoint(tx2, 0) val txNotFunded = Transaction(2, Seq(TxIn(externalOutpoint, Nil, 0)), Seq(TxOut(300_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(externalOutpoint, externalInputWeight)), changePosition = Some(1))).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = Seq(InputWeight(externalOutpoint, externalInputWeight)), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse] assert(fundedTx.tx.txIn.length >= 2) // bitcoind signs the wallet input. @@ -308,7 +314,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val weight = txNotFunded.weight() - txNotFunded.copy(txIn = txNotFunded.txIn.filterNot(_.outPoint == txIn.outPoint)).weight() InputWeight(txIn.outPoint, weight) }) - walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = inputWeights, changePosition = Some(1))).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = inputWeights, changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse] assert(fundedTx.tx.txIn.length >= 2) assert(fundedTx.tx.txOut.length == 2) @@ -360,7 +366,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.onChainBalance().pipeTo(sender.ref) assert(sender.expectMsgType[OnChainBalance] == OnChainBalance(Satoshi(satoshi), Satoshi(satoshi))) - bitcoinClient.fundTransaction(txIn, FundTransactionOptions(FeeratePerKw(250 sat))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txIn, FundTransactionOptions(FeeratePerKw(250 sat)), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.fee == Satoshi(satoshi)) } @@ -466,7 +472,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val tx1 = { val fundedTxs = (1 to 3).map(_ => { val txNotFunded = Transaction(2, Nil, TxOut(15000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) sender.expectMsgType[FundTransactionResponse].tx }) val fundedTx = Transaction(2, fundedTxs.flatMap(_.txIn), fundedTxs.flatMap(_.txOut), 0) @@ -489,7 +495,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // create a second transaction that double-spends one of the inputs of the first transaction val tx2 = { val txNotFunded = tx1.copy(txIn = tx1.txIn.take(1)) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw * 2)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw * 2), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx assert(fundedTx.txIn.length >= 2) // we added at least one new input @@ -637,7 +643,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val nonWalletKey = randomKey() val opts = FundTransactionOptions(TestConstants.feeratePerKw, changePosition = Some(1)) - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx bitcoinClient.signPsbt(new Psbt(fundedTx), fundedTx.txIn.indices, Nil).pipeTo(sender.ref) val txToRemote = sender.expectMsgType[ProcessPsbtResponse].finalTx_opt.toOption.get @@ -646,7 +652,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A generateBlocks(1) { - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(400000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(400000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] val txWithNonWalletInput = fundTxResponse.tx.copy(txIn = TxIn(OutPoint(txToRemote, 0), ByteVector.empty, 0) +: fundTxResponse.tx.txIn) val walletInputTxs = txWithNonWalletInput.txIn.tail.map(txIn => { @@ -674,7 +680,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } { // bitcoind lets us double-spend ourselves. - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(75000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(75000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] bitcoinClient.signPsbt(new Psbt(fundTxResponse.tx), fundTxResponse.tx.txIn.indices, Nil).pipeTo(sender.ref) assert(sender.expectMsgType[ProcessPsbtResponse].complete) @@ -683,14 +689,14 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } { // create an unconfirmed utxo to a non-wallet address. - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(125000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(125000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx bitcoinClient.signPsbt(new Psbt(fundedTx), fundedTx.txIn.indices, Nil).pipeTo(sender.ref) val unconfirmedTx = sender.expectMsgType[ProcessPsbtResponse].finalTx_opt.toOption.get bitcoinClient.publishTransaction(unconfirmedTx).pipeTo(sender.ref) sender.expectMsg(unconfirmedTx.txid) // bitcoind lets us use this unconfirmed non-wallet input. - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(350000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(350000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] val txWithUnconfirmedInput = fundTxResponse.tx.copy(txIn = TxIn(OutPoint(unconfirmedTx, 0), ByteVector.empty, 0) +: fundTxResponse.tx.txIn) val nonWalletSig = Transaction.signInput(txWithUnconfirmedInput, 0, Script.pay2pkh(nonWalletKey.publicKey), bitcoin.SigHash.SIGHASH_ALL, unconfirmedTx.txOut.head.amount, bitcoin.SigVersion.SIGVERSION_WITNESS_V0, nonWalletKey) @@ -712,7 +718,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val priv = randomKey() val noInputTx = Transaction(2, Nil, TxOut(6.btc.toSatoshi, Script.pay2wpkh(priv.publicKey)) :: Nil, 0) - bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] val changePos = fundTxResponse.changePosition.get bitcoinClient.signPsbt(new Psbt(fundTxResponse.tx), fundTxResponse.tx.txIn.indices, Nil).pipeTo(sender.ref) @@ -768,7 +774,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A bitcoinClient.getTxConfirmations(txWithUnknownInputs.txid).pipeTo(sender.ref) sender.expectMsg(None) - bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(100000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), FundTransactionOptions(TestConstants.feeratePerKw)).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(100000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) val txUnsignedInputs = sender.expectMsgType[FundTransactionResponse].tx bitcoinClient.publishTransaction(txUnsignedInputs).pipeTo(sender.ref) sender.expectMsgType[Failure] @@ -878,7 +884,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // Broadcast a wallet transaction. val opts = FundTransactionOptions(TestConstants.feeratePerKw, changePosition = Some(1)) - bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), opts, feeBudget_opt = None).pipeTo(sender.ref) val fundedTx1 = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, fundedTx1).pipeTo(sender.ref) val signedTx1 = sender.expectMsgType[SignTransactionResponse].tx @@ -947,7 +953,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val fundingScript = Scripts.multiSig2of2(remoteFundingPrivKey.publicKey, walletFundingPrivKey.publicKey) val fundingTx = { val txNotFunded = Transaction(2, Nil, TxOut(250_000 sat, Script.pay2wsh(fundingScript)) :: Nil, 0) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(fundingFeerate, changePosition = Some(1))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(fundingFeerate, changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.changePosition.contains(1)) signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) @@ -1017,7 +1023,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } val txOut = dest.map { case (pubKey, amount) => TxOut(amount, Script.pay2wpkh(pubKey)) } val txNotFunded = Transaction(2, txIn, txOut, 0) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(currentFeerate, changePosition = Some(txOut.length))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(currentFeerate, changePosition = Some(txOut.length)), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] @@ -1126,7 +1132,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val sender = TestProbe() val bitcoinClient = makeBitcoinCoreClient() val txNotFunded = Transaction(2, Nil, TxOut(50_000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1000 sat), changePosition = Some(1))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(1000 sat), changePosition = Some(1)), feeBudget_opt = None).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] @@ -1154,7 +1160,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A // first let's create a tx val noInputTx1 = Transaction(2, Nil, Seq(TxOut(500_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) - bitcoinClient.fundTransaction(noInputTx1, FundTransactionOptions(FeeratePerKw(2500 sat))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx1, FundTransactionOptions(FeeratePerKw(2500 sat)), feeBudget_opt = None).pipeTo(sender.ref) val unsignedTx1 = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, unsignedTx1).pipeTo(sender.ref) val tx1 = sender.expectMsgType[SignTransactionResponse].tx @@ -1195,7 +1201,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val (confirmedParentTx, unconfirmedParentTx) = { val txs = Seq(400_000 sat, 500_000 sat).map(amount => { val noInputTx = Transaction(2, Nil, Seq(TxOut(amount, Script.pay2wpkh(priv.publicKey))), 0) - bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(FeeratePerKw(2500 sat))).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx, FundTransactionOptions(FeeratePerKw(2500 sat)), feeBudget_opt = None).pipeTo(sender.ref) val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, unsignedTx).pipeTo(sender.ref) sender.expectMsgType[SignTransactionResponse].tx @@ -1358,7 +1364,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val outputsWithLargeScript = Seq.fill(largeInputsCount)(TxOut(1_000 sat, Script.pay2wsh(bigInputScript))) val outputs = mainOutput +: outputsWithLargeScript val txNotFunded = Transaction(2, Nil, mainOutput +: outputsWithLargeScript, 0) - miner.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(500 sat), changePosition = Some(outputs.length))).pipeTo(sender.ref) + miner.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(500 sat), changePosition = Some(outputs.length)), feeBudget_opt = None).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx signTransaction(miner, fundedTx).pipeTo(sender.ref) val signedTx = sender.expectMsgType[SignTransactionResponse].tx 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 ff13d282a1..3cc68fb47e 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 @@ -305,7 +305,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // create a chain of transactions that we don't broadcast yet val priv = randomKey() val tx1 = { - bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(150000 sat, Script.pay2wpkh(priv.publicKey)) :: Nil, 0), FundTransactionOptions(FeeratePerKw(250 sat))).pipeTo(probe.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(150000 sat, Script.pay2wpkh(priv.publicKey)) :: Nil, 0), FundTransactionOptions(FeeratePerKw(250 sat)), feeBudget_opt = None).pipeTo(probe.ref) val funded = probe.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, funded).pipeTo(probe.ref) probe.expectMsgType[SignTransactionResponse].tx 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 faeec2ed30..f4aa5683ae 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 @@ -78,7 +78,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, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, channelFlags = ChannelFlags.Private, ChannelConfig.standard, ChannelTypes.Standard(), replyTo = system.deadLetters) + 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 = ChannelFlags.Private, 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/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 2573fa7d96..3d0d287d26 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -125,7 +125,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderAlice(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsA): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsA, fundingParams, channelParamsA, - FundingTx(commitFeerate, firstPerCommitmentPointB), + FundingTx(commitFeerate, firstPerCommitmentPointB, feeBudget_opt = None), 0 msat, 0 msat, wallet)) @@ -153,7 +153,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsB, fundingParams, channelParamsB, - FundingTx(commitFeerate, firstPerCommitmentPointA), + FundingTx(commitFeerate, firstPerCommitmentPointA, feeBudget_opt = None), 0 msat, 0 msat, wallet)) 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 142e4a2a45..1d235413bc 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 @@ -250,7 +250,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, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) 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 600b3a2b45..9421473ea6 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, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -171,7 +171,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, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), ChannelFlags.Private, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) + 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.Private, 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 82a346fd4f..dc89dcfe0b 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, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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.Private, 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 1793f43b59..3d20ac58e0 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, 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, 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 038892db9e..1aaeb9a997 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, pushAmount, requireConfirmedInputs, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Private, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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.Private, 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 ee7b162571..d51fbb9d82 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, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) 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 de8b70043f..bdf6867649 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 @@ -56,7 +56,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, TestConstants.feeratePerKw, TestConstants.feeratePerKw, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, 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 a5013b41ef..53f7392589 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, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) 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 8a734e703d..1b8006ab48 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, 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, 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 6f0e739723..5df46bf911 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -63,7 +63,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, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) 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 18bde8e355..e8f08c0a0c 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 @@ -60,7 +60,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, TestConstants.feeratePerKw, TestConstants.feeratePerKw, pushMsat, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, 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 de59a118b6..5b05a3c9de 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 @@ -67,7 +67,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, TestConstants.feeratePerKw, TestConstants.feeratePerKw, initiatorPushAmount, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = true, TestConstants.feeratePerKw, 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 0aa007a1a6..d260fc9bde 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, None, requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + 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) 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 6f4f177e65..a604be4711 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 @@ -60,7 +60,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed]) bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelClosed]) - alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(pushMsat), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.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) 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 0f8fc3c752..307ad3f8e6 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 @@ -73,7 +73,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, 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, 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 402783bf2d..8028532726 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 channelType_opt = None, pushAmount_opt = Some(pushMsat), fundingTxFeerate_opt = None, + fundingTxFeeBudget_opt = None, channelFlags_opt = None, timeout_opt = None)) sender.expectMsgType[OpenChannelResponse.Created](10 seconds) 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 accf5fc6b5..9dc831eb6d 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)) + sender.send(node1.switchboard, Peer.OpenChannel(node2.nodeParams.nodeId, funding, 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 354427abbd..8d511c634c 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, Some(1000000000 msat), requireConfirmedInputs = false, Alice.channelParams, pipe, bobInit, ChannelFlags.Private, channelConfig, channelType, replyTo = system.deadLetters) + 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.Private, 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 1d4550f970..6149722688 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 @@ -156,7 +156,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), Features.empty, initFeatures().add(Wumbo, Optional)) + openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), Features.empty, initFeatures().add(Wumbo, Optional)) assert(probe.expectMessageType[OpenChannelResponse.Rejected].reason.contains("you must enable large channels support")) } @@ -165,7 +165,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), initFeatures().add(Wumbo, Optional), Features.empty) + openChannelInterceptor ! OpenChannelInitiator(probe.ref, remoteNodeId, Peer.OpenChannel(remoteNodeId, fundingAmountBig, None, None, None, None, None, None), initFeatures().add(Wumbo, Optional), Features.empty) 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 d1545d2621..722f141d74 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 @@ -360,7 +360,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) + val open = Peer.OpenChannel(remoteNodeId, 10000 sat, None, None, None, None, None, None) peerConnection.send(peer, open) channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] } @@ -381,7 +381,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 25000 sat, None, None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].dualFunded) } @@ -424,15 +424,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, Some(ChannelTypes.AnchorOutputs()), None, None, None, None, None)) assert(channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR].channelType == ChannelTypes.AnchorOutputs()) } @@ -467,7 +467,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputs()) assert(!init.dualFunded) @@ -485,7 +485,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) assert(!init.dualFunded) @@ -499,7 +499,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, None, None, None, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_CHANNEL_INITIATOR] assert(init.channelType == ChannelTypes.StaticRemoteKey()) assert(!init.dualFunded) @@ -516,12 +516,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 200_000 sat, 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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 500_000 sat, 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 } @@ -545,7 +545,7 @@ class PeerSpec extends FixtureSpec { import f._ intercept[IllegalArgumentException] { - Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, Some(ChannelFlags(announceChannel = true)), None) + Peer.OpenChannel(remoteNodeId, 24000 sat, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true, zeroConf = true)), None, None, None, Some(ChannelFlags(announceChannel = true)), None) } } @@ -554,7 +554,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)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 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 c4a5e0b3a7..414a0771ca 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].?, "announceChannel".as[Boolean].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, channelTypeName_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) => + 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) => 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, announceChannel_opt, openTimeout_opt)) + complete(eclairApi.open(nodeId, fundingSatoshis, 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 a0abe0744c..e69a06c798 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[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 100 sat)) + eclair.open(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)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 100002 sat, 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[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 0 sat)) + eclair.open(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)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, 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[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 1 sat)) + eclair.open(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)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, 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[Timeout]) returns Future.successful(OpenChannelResponse.Created(channelId, fundingTxId, 500 sat)) + eclair.open(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)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, 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)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 25000 sat, None, Some(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()), None, None, None, None)(any[Timeout]).wasCalled(once) } } From 5534091bc775e0d59a7a4d6341ed716a8125fa33 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 10 Jan 2024 13:33:01 +0100 Subject: [PATCH 2/4] use 0.1% of funding amount as default fee budget --- docs/release-notes/eclair-vnext.md | 1 + eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala | 9 +++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 56ac5d4413..fff1416fc0 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -55,6 +55,7 @@ This feature leaks a bit of information about the balance when the channel is al ### API changes - `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743) +- `open` now takes an optional parameter `--fundingFeeBudgetSatoshis` to define the maximum acceptable value for the mining fee of the funding transaction. This mining fee can sometimes be unexpectedly high depending on available UTXOs in the wallet. Default value is 0.1% of the funding amount (#2808) ### Miscellaneous improvements and bug fixes 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 d3295d9a79..ff3584c7d5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -30,9 +30,8 @@ import fr.acinq.eclair.balance.CheckBalance.GlobalBalance import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.WalletTx -import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{Descriptors, WalletTx} +import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} @@ -208,10 +207,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { 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] = { // 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)) - val fundingFeeBudget = fundingFeeBudget_opt.getOrElse{ - val proportionalFee = Helpers.getRelayFees(appKit.nodeParams, nodeId, announceChannel_opt.getOrElse(appKit.nodeParams.channelConf.channelFlags.announceChannel)).feeProportionalMillionths.toDouble / 1e6 - fundingAmount * proportionalFee - } + // 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 + val fundingFeeBudget = fundingFeeBudget_opt.getOrElse(fundingAmount * 0.001) for { _ <- Future.successful(0) open = Peer.OpenChannel( From 338cdc3f249e5404b19cb5bbad0317fb5faae581 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 10 Jan 2024 14:55:06 +0100 Subject: [PATCH 3/4] add a mandatory fee budget for rbfopen --- docs/release-notes/eclair-vnext.md | 1 + .../main/scala/fr/acinq/eclair/Eclair.scala | 6 +-- .../fr/acinq/eclair/channel/ChannelData.scala | 2 +- .../channel/fsm/ChannelOpenDualFunded.scala | 4 +- .../channel/fund/InteractiveTxBuilder.scala | 2 +- .../channel/fund/InteractiveTxFunder.scala | 1 + .../blockchain/DummyOnChainWallet.scala | 15 ++++--- .../channel/InteractiveTxBuilderSpec.scala | 8 ++-- ...WaitForDualFundingConfirmedStateSpec.scala | 41 ++++++++++++++++--- .../channel/version4/ChannelCodecs4Spec.scala | 2 +- .../acinq/eclair/api/handlers/Channel.scala | 4 +- 11 files changed, 61 insertions(+), 25 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index fff1416fc0..43d06344c7 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -56,6 +56,7 @@ This feature leaks a bit of information about the balance when the channel is al - `bumpforceclose` can be used to make a force-close confirm faster, by spending the anchor output (#2743) - `open` now takes an optional parameter `--fundingFeeBudgetSatoshis` to define the maximum acceptable value for the mining fee of the funding transaction. This mining fee can sometimes be unexpectedly high depending on available UTXOs in the wallet. Default value is 0.1% of the funding amount (#2808) +- `rbfopen` now takes a mandatory parameter `--fundingFeeBudgetSatoshis`, with the same semantics as for `open` (#2808) ### Miscellaneous improvements and bug fixes 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 ff3584c7d5..28f5eadb46 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -88,7 +88,7 @@ trait Eclair { 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 rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] + def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] @@ -224,9 +224,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } yield res } - override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = { + override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = { sendToChannelTyped(channel = Left(channelId), - cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong))) + cmdBuilder = CMD_BUMP_FUNDING_FEE(_, targetFeerate, fundingFeeBudget, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong))) } override def spliceIn(channelId: ByteVector32, amountIn: Satoshi, pushAmount_opt: Option[MilliSatoshi])(implicit timeout: Timeout): Future[CommandResponse[CMD_SPLICE]] = { 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 bf296a9b69..ebc8c7aa5d 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 @@ -208,7 +208,7 @@ final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand final case class CMD_BUMP_FORCE_CLOSE_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FORCE_CLOSE_FEE]], confirmationTarget: ConfirmationTarget) extends Command -final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, lockTime: Long) extends Command +final case class CMD_BUMP_FUNDING_FEE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_BUMP_FUNDING_FEE]], targetFeerate: FeeratePerKw, fundingFeeBudget: Satoshi, lockTime: Long) extends Command case class SpliceIn(additionalLocalFunding: Satoshi, pushAmount: MilliSatoshi = 0 msat) case class SpliceOut(amount: Satoshi, scriptPubKey: ByteVector) final case class CMD_SPLICE(replyTo: akka.actor.typed.ActorRef[CommandResponse[CMD_SPLICE]], spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]) extends Command { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 9ed1e21615..335cf163ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -547,7 +547,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { randomBytes32(), nodeParams, fundingParams, channelParams = d.commitments.params, - purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx)), + purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None), localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) @@ -585,7 +585,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { randomBytes32(), nodeParams, fundingParams, channelParams = d.commitments.params, - purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx)), + purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)), localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index f634ef4e99..2d4afcbc94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -194,7 +194,7 @@ object InteractiveTxBuilder { * only one of them ends up confirming. We guarantee this by having the latest transaction * always double-spend all its predecessors. */ - case class PreviousTxRbf(replacedCommitment: Commitment, previousLocalBalance: MilliSatoshi, previousRemoteBalance: MilliSatoshi, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction]) extends Purpose { + case class PreviousTxRbf(replacedCommitment: Commitment, previousLocalBalance: MilliSatoshi, previousRemoteBalance: MilliSatoshi, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], feeBudget_opt: Option[Satoshi]) extends Purpose { // Note that the truncation is a no-op: the sum of balances in a channel must be a satoshi amount. override val previousFundingAmount: Satoshi = (previousLocalBalance + previousRemoteBalance).truncateToSatoshi override val localCommitIndex: Long = replacedCommitment.localCommit.index 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 6571879164..97a8b7a3d3 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 @@ -191,6 +191,7 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response val sharedInputWeight = fundingParams.sharedInput_opt.toSeq.map(i => i.info.outPoint -> i.weight.toLong).toMap val feeBudget_opt = purpose match { case p: FundingTx => p.feeBudget_opt + case p: PreviousTxRbf => p.feeBudget_opt case _ => None } context.pipeToSelf(wallet.fundTransaction(txNotFunded, fundingParams.targetFeerate, replaceable = true, externalInputsWeight = sharedInputWeight, feeBudget_opt = feeBudget_opt)) { 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 908f26ce3d..d96844535e 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 @@ -147,11 +147,16 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { txOut = tx.txOut :+ TxOut(inputAmount, Script.pay2wpkh(pubkey)), ) val fee = Transactions.weight2fee(feeRate, dummySignedTx.weight() + externalInputsWeight.values.sum.toInt) - val fundedTx = tx.copy( - txIn = tx.txIn :+ TxIn(OutPoint(inputTx, 0), Nil, 0), - txOut = tx.txOut :+ TxOut(inputAmount + currentAmountIn - amountOut - fee, Script.pay2wpkh(pubkey)), - ) - Future.successful(FundTransactionResponse(fundedTx, fee, Some(tx.txOut.length))) + feeBudget_opt match { + case Some(feeBudget) if fee > feeBudget => + Future.failed(new RuntimeException(s"mining fee is higher than budget ($fee > $feeBudget)")) + case _ => + val fundedTx = tx.copy( + txIn = tx.txIn :+ TxIn(OutPoint(inputTx, 0), Nil, 0), + txOut = tx.txOut :+ TxOut(inputAmount + currentAmountIn - amountOut - fee, Script.pay2wpkh(pubkey)), + ) + Future.successful(FundTransactionResponse(fundedTx, fee, Some(tx.txOut.length))) + } } private def signTransaction(tx: Transaction): Future[SignTransactionResponse] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index 3d0d287d26..6f0cecfa52 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -132,7 +132,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderRbfAlice(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsA, fundingParams, channelParamsA, - PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions), + PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) @@ -146,7 +146,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderSpliceRbfAlice(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsA, fundingParams, channelParamsA, - PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions), + PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) @@ -160,7 +160,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderRbfBob(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsB, fundingParams, channelParamsB, - PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions), + PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) @@ -174,7 +174,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit def spawnTxBuilderSpliceRbfBob(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( ByteVector32.Zeroes, nodeParamsB, fundingParams, channelParamsB, - PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions), + PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) 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 5b05a3c9de..9d0cbf3b39 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 @@ -236,7 +236,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] @@ -309,7 +309,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, fundingFeeBudget = 10_000.sat, 0) assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) @@ -373,7 +373,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) @@ -400,6 +400,35 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) } + test("recv CMD_BUMP_FUNDING_FEE (over fee budget)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] + val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100.sat, 0) + assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) + bob2alice.forward(alice) + + // Alice and Bob build a new version of the funding transaction. + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + awaitAssert(assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfStatus == RbfStatus.RbfAborted)) + bob2alice.expectMsgType[TxAbort] // bob acks alice's tx_abort + bob2alice.forward(alice) + alice2bob.expectNoMessage(100 millis) + + // Alice and Bob clear RBF data from their state. + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfStatus == RbfStatus.NoRbf) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx == fundingTxAlice) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfStatus == RbfStatus.NoRbf) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx == fundingTxBob) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + } + test("recv TxInitRbf (exhausted RBF attempts)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.RejectRbfAttempts)) { f => import f._ @@ -429,7 +458,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv TxAckRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f => import f._ - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, fundingFeeBudget = 10_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] val fundingBelowPushAmount = 99_000.sat alice ! TxAckRbf(channelId(alice), fundingBelowPushAmount) @@ -719,7 +748,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] @@ -778,7 +807,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index be3317c7a4..5ff534ec64 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -144,7 +144,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { ) val testCases = Map( RbfStatus.NoRbf -> RbfStatus.NoRbf, - RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), 0)) -> RbfStatus.NoRbf, + RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 10_000.sat, 0)) -> RbfStatus.NoRbf, RbfStatus.RbfInProgress(None, null, None) -> RbfStatus.NoRbf, RbfStatus.RbfWaitingForSigs(waitingForSigs) -> RbfStatus.RbfWaitingForSigs(waitingForSigs), RbfStatus.RbfAborted -> RbfStatus.NoRbf, 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 414a0771ca..9c8c0413ae 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 @@ -70,8 +70,8 @@ trait Channel { } val rbfOpen: Route = postRequest("rbfopen") { implicit f => - formFields(channelIdFormParam, "targetFeerateSatByte".as[FeeratePerByte], "lockTime".as[Long].?) { - (channelId, targetFeerateSatByte, lockTime_opt) => complete(eclairApi.rbfOpen(channelId, FeeratePerKw(targetFeerateSatByte), lockTime_opt)) + formFields(channelIdFormParam, "targetFeerateSatByte".as[FeeratePerByte], "fundingFeeBudgetSatoshis".as[Satoshi], "lockTime".as[Long].?) { + (channelId, targetFeerateSatByte, fundingFeeBudget, lockTime_opt) => complete(eclairApi.rbfOpen(channelId, FeeratePerKw(targetFeerateSatByte), fundingFeeBudget, lockTime_opt)) } } From a6f6c037ae11b35a961ebcccbbd4f20113afa72b Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 10 Jan 2024 15:15:57 +0100 Subject: [PATCH 4/4] fixup! add a mandatory fee budget for rbfopen --- .../c/WaitForDualFundingConfirmedStateSpec.scala | 12 ++++++------ .../channel/version4/ChannelCodecs4Spec.scala | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) 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 9d0cbf3b39..8ba26e7be8 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 @@ -236,7 +236,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] @@ -309,7 +309,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, fundingFeeBudget = 100_000.sat, 0) assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) @@ -373,7 +373,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val probe = TestProbe() val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction] - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0) assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) @@ -458,7 +458,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv TxAckRbf (invalid push amount)", Tag(ChannelStateTestsTags.DualFunding), Tag("both_push_amount")) { f => import f._ - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.25, fundingFeeBudget = 100_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] val fundingBelowPushAmount = 99_000.sat alice ! TxAckRbf(channelId(alice), fundingBelowPushAmount) @@ -748,7 +748,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv INPUT_DISCONNECTED (unsigned rbf attempt)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] @@ -807,7 +807,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val currentFundingTxId = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.txId - alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 10_000.sat, 0) + alice ! CMD_BUMP_FUNDING_FEE(TestProbe().ref, TestConstants.feeratePerKw * 1.1, fundingFeeBudget = 100_000.sat, 0) alice2bob.expectMsgType[TxInitRbf] alice2bob.forward(bob) bob2alice.expectMsgType[TxAckRbf] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index 5ff534ec64..0eeb1bd1cc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -144,7 +144,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { ) val testCases = Map( RbfStatus.NoRbf -> RbfStatus.NoRbf, - RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 10_000.sat, 0)) -> RbfStatus.NoRbf, + RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 100_000.sat, 0)) -> RbfStatus.NoRbf, RbfStatus.RbfInProgress(None, null, None) -> RbfStatus.NoRbf, RbfStatus.RbfWaitingForSigs(waitingForSigs) -> RbfStatus.RbfWaitingForSigs(waitingForSigs), RbfStatus.RbfAborted -> RbfStatus.NoRbf,