diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index adec4332c4..aecf2c25cc 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -109,6 +109,9 @@ eclair { max-attempts = 5 // maximum number of RBF attempts our peer is allowed to make attempt-delta-blocks = 6 // minimum number of blocks between RBF attempts } + // Duration after which we abort a channel creation. If our peer seems unresponsive and doesn't complete the + // funding protocol in time, they're likely buggy or malicious. + timeout = 60 seconds } dust-limit-satoshis = 546 @@ -343,6 +346,21 @@ eclair { // This doesn't involve trust from the buyer or the seller. "from_channel_balance" ] + // When selling your liquidity, malicious nodes can attempt a liquidity griefing attack against your node where + // they initiate a purchase and stop responding before finalizing the funding transaction. By default, the utxos + // used for this funding transaction will be locked to avoid accidentally double-spending ourselves. The funding + // attempt will automatically be cancelled after a timeout and utxos unlocked, but the attacker will have succeeded + // in "locking" some of our liquidity for a short duration. By continually repeating this process, they may prevent + // your node from successfully selling liquidity to honest nodes. More details can be found here: + // https://delvingbitcoin.org/t/liquidity-griefing-in-multi-party-transaction-protocols/264 + // + // The only way to fully prevent this attack is to set the following parameter to false, which means that utxos + // will never be locked during funding attempts, and can thus immediately be reused until an honest node completes + // a liquidity purchase. However, there is a drawback: it means that all funding transactions may now be double + // spent accidentally, even funding transactions with honest nodes, which requires retrying the purchase when it + // fails and isn't a very good UX. It is up to the node operator to decide which trade-off they're comfortable with + // and to set this flag accordingly. + lock-utxos-during-funding = true } // On-the-fly funding leverages liquidity ads to fund channels with wallet peers based on their payment patterns. 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 0ed5fefd61..207fce90e9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -792,7 +792,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } override def enableFromFutureHtlc(): Future[EnableFromFutureHtlcResponse] = { - appKit.nodeParams.willFundRates_opt match { + appKit.nodeParams.liquidityAdsConfig.rates_opt match { case Some(willFundRates) if willFundRates.paymentTypes.contains(LiquidityAds.PaymentType.FromFutureHtlc) => appKit.nodeParams.onTheFlyFundingConfig.enableFromFutureHtlc() Future.successful(EnableFromFutureHtlcResponse(appKit.nodeParams.onTheFlyFundingConfig.isFromFutureHtlcAllowed, None)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 024dc3555e..cc9a84ad28 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -90,7 +90,7 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, onionMessageConfig: OnionMessageConfig, purgeInvoicesInterval: Option[FiniteDuration], revokedHtlcInfoCleanerConfig: RevokedHtlcInfoCleaner.Config, - willFundRates_opt: Option[LiquidityAds.WillFundRates], + liquidityAdsConfig: LiquidityAds.Config, peerWakeUpConfig: PeerReadyNotifier.WakeUpConfig, onTheFlyFundingConfig: OnTheFlyFunding.Config, peerStorageConfig: PeerStorageConfig) { @@ -591,6 +591,7 @@ object NodeParams extends Logging { channelOpenerWhitelist = channelOpenerWhitelist, maxPendingChannelsPerPeer = maxPendingChannelsPerPeer, maxTotalPendingChannelsPrivateNodes = maxTotalPendingChannelsPrivateNodes, + channelFundingTimeout = FiniteDuration(config.getDuration("channel.funding.timeout").getSeconds, TimeUnit.SECONDS), remoteRbfLimits = Channel.RemoteRbfLimits(config.getInt("channel.funding.remote-rbf-limits.max-attempts"), config.getInt("channel.funding.remote-rbf-limits.attempt-delta-blocks")), quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS), balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq, @@ -683,7 +684,7 @@ object NodeParams extends Logging { batchSize = config.getInt("db.revoked-htlc-info-cleaner.batch-size"), interval = FiniteDuration(config.getDuration("db.revoked-htlc-info-cleaner.interval").getSeconds, TimeUnit.SECONDS) ), - willFundRates_opt = willFundRates_opt, + liquidityAdsConfig = LiquidityAds.Config(rates_opt = willFundRates_opt, lockUtxos = config.getBoolean("liquidity-ads.lock-utxos-during-funding")), peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig( enabled = config.getBoolean("peer-wake-up.enabled"), timeout = FiniteDuration(config.getDuration("peer-wake-up.timeout").getSeconds, TimeUnit.SECONDS), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index e27a8472dd..73585376c4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -274,7 +274,7 @@ class Setup(val datadir: File, // This is how you would create a new bitcoin wallet whose private keys are managed by Eclair. // 3) there is an `eclair-signer.conf` file in Eclair's data directory, and the name of the wallet set in `eclair-signer.conf` matches the `eclair.bitcoind.wallet` setting in `eclair.conf`. // Eclair will assume that this is a watch-only bitcoin wallet that has been created from descriptors generated by Eclair, and will manage its private keys, and here we pass the onchain key manager to our bitcoin client. - bitcoinClient = new BitcoinCoreClient(bitcoin, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnchainPubkeyCache { + bitcoinClient = new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnchainPubkeyCache { val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(this, finalPubkey, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") override def getP2wpkhPubkey(renew: Boolean): PublicKey = { @@ -325,9 +325,9 @@ class Setup(val datadir: File, system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), ZMQActor.Topics.HashBlock, Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), ZMQActor.Topics.RawTx, Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) val watcherBitcoinClient = if (config.getBoolean("bitcoind.batch-watcher-requests")) { - new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin)) + new BitcoinCoreClient(new BatchingBitcoinJsonRPCClient(bitcoin), nodeParams.liquidityAdsConfig.lockUtxos) } else { - new BitcoinCoreClient(bitcoin) + new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos) } system.spawn(Behaviors.supervise(ZmqWatcher(nodeParams, blockHeight, watcherBitcoinClient)).onFailure(typed.SupervisorStrategy.resume), "watcher") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index 3544636678..95bc598e2a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -37,7 +37,7 @@ trait OnChainChannelFunder { * Fund the provided transaction by adding inputs (and a change output if necessary). * Callers must verify that the resulting transaction isn't sending funds to unexpected addresses (malicious bitcoin node). */ - def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, 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. * 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 721e0179e3..8dac3f8bb2 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,7 +26,6 @@ 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} @@ -51,7 +50,7 @@ import scala.util.{Failure, Success, Try} * @param onChainKeyManager_opt optional on-chain key manager. If provided it will be used to sign transactions (it is assumed that bitcoin * core uses a watch-only wallet with descriptors generated by Eclair with this on-chain key manager) */ -class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManager_opt: Option[OnChainKeyManager] = None) extends OnChainWallet with Logging { +class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Boolean = true, val onChainKeyManager_opt: Option[OnChainKeyManager] = None) extends OnChainWallet with Logging { import BitcoinCoreClient._ @@ -262,8 +261,23 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag }) } - def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { - fundTransaction(tx, FundTransactionOptions(feeRate, replaceable, inputWeights = externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq), feeBudget_opt = feeBudget_opt) + def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, externalInputsWeight: Map[OutPoint, Long] = Map.empty, feeBudget_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + val options = FundTransactionOptions( + BigDecimal(FeeratePerKB(feeRate).toLong).bigDecimal.scaleByPowerOfTen(-8), + replaceable, + // We must either *always* lock inputs selected for funding or *never* lock them, otherwise locking wouldn't work + // at all, as the following scenario highlights: + // - we fund a transaction for which we don't lock utxos + // - we fund another unrelated transaction for which we lock utxos + // - the second transaction ends up using the same utxos as the first one + // - but the first transaction confirms, invalidating the second one + // This would break the assumptions of the second transaction: its inputs are locked, so it doesn't expect to + // potentially be double-spent. + lockUtxos, + changePosition, + if (externalInputsWeight.isEmpty) None else Some(externalInputsWeight.map { case (outpoint, weight) => InputWeight(outpoint, weight) }.toSeq) + ) + fundTransaction(tx, options, feeBudget_opt = feeBudget_opt) } private def processPsbt(psbt: Psbt, sign: Boolean = true, sighashType: Option[Int] = None)(implicit ec: ExecutionContext): Future[ProcessPsbtResponse] = { @@ -344,7 +358,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag // TODO: we should check that mempoolMinFee is not dangerously high feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) // we ask bitcoin core to add inputs to the funding tx, and use the specified change address - FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate), feeBudget_opt = feeBudget_opt) + FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, feerate, feeBudget_opt = feeBudget_opt) lockedUtxos = tx.txIn.map(_.outPoint) signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, fee, feerate)) } yield signedTx @@ -706,26 +720,6 @@ object BitcoinCoreClient { case class FundTransactionOptions(feeRate: BigDecimal, replaceable: Boolean, lockUnspents: Boolean, changePosition: Option[Int], input_weights: Option[Seq[InputWeight]]) - object FundTransactionOptions { - def apply(feerate: FeeratePerKw, replaceable: Boolean = true, changePosition: Option[Int] = None, inputWeights: Seq[InputWeight] = Nil): FundTransactionOptions = { - FundTransactionOptions( - BigDecimal(FeeratePerKB(feerate).toLong).bigDecimal.scaleByPowerOfTen(-8), - replaceable, - // We must *always* lock inputs selected for funding, otherwise locking wouldn't work at all, as the following - // scenario highlights: - // - we fund a transaction for which we don't lock utxos - // - we fund another unrelated transaction for which we lock utxos - // - the second transaction ends up using the same utxos as the first one - // - but the first transaction confirms, invalidating the second one - // This would break the assumptions of the second transaction: its inputs are locked, so it doesn't expect to - // potentially be double-spent. - lockUnspents = true, - changePosition, - if (inputWeights.isEmpty) None else Some(inputWeights) - ) - } - } - /** * Information about a transaction currently in the mempool. * diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index 216170d918..6c78fb792b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -102,6 +102,7 @@ object Channel { channelOpenerWhitelist: Set[PublicKey], maxPendingChannelsPerPeer: Int, maxTotalPendingChannelsPrivateNodes: Int, + channelFundingTimeout: FiniteDuration, remoteRbfLimits: RemoteRbfLimits, quiescenceTimeout: FiniteDuration, balanceThresholds: Seq[BalanceThreshold], @@ -1000,7 +1001,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val parentCommitment = d.commitments.latest.commitment val localFundingPubKey = nodeParams.channelKeyManager.fundingPublicKey(d.commitments.params.localParams.fundingKeyPath, parentCommitment.fundingTxIndex + 1).publicKey val fundingScript = Funding.makeFundingPubKeyScript(localFundingPubKey, msg.fundingPubKey) - LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.willFundRates_opt, msg.useFeeCredit_opt) match { + LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, msg.useFeeCredit_opt) match { case Left(t) => log.warning("rejecting splice request with invalid liquidity ads: {}", t.getMessage) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage) @@ -1122,7 +1123,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, rbf.latestFundingTx.createdAt, rbf.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) case Right(rbf) => val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript - LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.willFundRates_opt, feeCreditUsed_opt = None) match { + LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, feeCreditUsed_opt = None) match { case Left(t) => log.warning("rejecting rbf request with invalid liquidity ads: {}", t.getMessage) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, t.getMessage) 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 a32a13c049..e9de1ba929 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 @@ -556,7 +556,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, d.latestFundingTx.createdAt, d.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) } else { val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript - LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.willFundRates_opt, None) match { + LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, None) match { case Left(t) => log.warning("rejecting rbf attempt: invalid liquidity ads request ({})", t.getMessage) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, t.getMessage) 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 02bfb485dd..33b8aaf23c 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 @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Script, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient -import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{FundTransactionOptions, InputWeight} import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.FullCommitment import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ @@ -446,8 +445,8 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, // (note that bitcoind doesn't let us publish a transaction with no outputs). To work around these limitations, we // start with a dummy output and later merge that dummy output with the optional change output added by bitcoind. val txNotFunded = anchorTx.txInfo.tx.copy(txOut = TxOut(dustLimit, Script.pay2wpkh(PlaceHolderPubKey)) :: Nil) - val anchorWeight = Seq(InputWeight(anchorTx.txInfo.input.outPoint, anchorInputWeight)) - bitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(targetFeerate, inputWeights = anchorWeight), feeBudget_opt = None).flatMap { fundTxResponse => + val anchorWeight = Map(anchorTx.txInfo.input.outPoint -> anchorInputWeight.toLong) + bitcoinClient.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = anchorWeight).flatMap { fundTxResponse => // Bitcoin Core may not preserve the order of inputs, we need to make sure the anchor is the first input. val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == anchorTx.txInfo.input.outPoint) // We merge our dummy change output with the one added by Bitcoin Core, if any. @@ -467,11 +466,11 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } private def addInputs(htlcTx: HtlcWithWitnessData, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(HtlcWithWitnessData, Satoshi)] = { - val htlcInputWeight = InputWeight(htlcTx.txInfo.input.outPoint, htlcTx.txInfo match { - 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)), feeBudget_opt = None).map(fundTxResponse => { + val htlcInputWeight = htlcTx.txInfo match { + case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessInputWeight.toLong + case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutInputWeight.toLong + } + bitcoinClient.fundTransaction(htlcTx.txInfo.tx, targetFeerate, changePosition = Some(1), externalInputsWeight = Map(htlcTx.txInfo.input.outPoint -> htlcInputWeight)).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/OpenChannelInterceptor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala index 8714ac9b5a..32cc36ec6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala @@ -209,7 +209,7 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], case None => request.open.fold(_ => None, _.requestFunding_opt) match { case Some(requestFunding) if Features.canUseFeature(request.localFeatures, request.remoteFeatures, Features.OnTheFlyFunding) && localParams.paysCommitTxFees => - val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.willFundRates_opt) + val addFunding = LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt) val accept = SpawnChannelNonInitiator(request.open, ChannelConfig.standard, channelType, Some(addFunding), localParams, request.peerConnection.toClassic) checkNoExistingChannel(request, accept) case _ => 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 ddb8f5d5d1..8b1b1d135d 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 @@ -198,7 +198,7 @@ class Peer(val nodeParams: NodeParams, case Event(SpawnChannelInitiator(replyTo, c, channelConfig, channelType, localParams), d: ConnectedData) => val channel = spawnChannel() - c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher)) + context.system.scheduler.scheduleOnce(c.timeout_opt.map(_.duration).getOrElse(nodeParams.channelConf.channelFundingTimeout), channel, Channel.TickChannelOpenTimeout)(context.dispatcher) val dualFunded = Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) val requireConfirmedInputs = c.requireConfirmedInputsOverride_opt.getOrElse(nodeParams.channelConf.requireConfirmedInputsForDualFunding) val temporaryChannelId = if (dualFunded) { @@ -252,6 +252,7 @@ class Peer(val nodeParams: NodeParams, stay() case accept: OnTheFlyFunding.ValidationResult.Accept => val channel = spawnChannel() + context.system.scheduler.scheduleOnce(nodeParams.channelConf.channelFundingTimeout, channel, Channel.TickChannelOpenTimeout)(context.dispatcher) log.info(s"accepting a new channel with type=$channelType temporaryChannelId=$temporaryChannelId localParams=$localParams") open match { case Left(open) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 24582f90b9..14f4a20b46 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -112,7 +112,7 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) val hasChannels = peersWithChannels.contains(authenticated.remoteNodeId) // if the peer is whitelisted, we sync with them, otherwise we only sync with peers with whom we have at least one channel val doSync = nodeParams.syncWhitelist.contains(authenticated.remoteNodeId) || (nodeParams.syncWhitelist.isEmpty && hasChannels) - authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync, nodeParams.willFundRates_opt) + authenticated.peerConnection ! PeerConnection.InitializeConnection(peer, nodeParams.chainHash, features, doSync, nodeParams.liquidityAdsConfig.rates_opt) if (!hasChannels && !authenticated.outgoing) { incomingConnectionsTracker ! TrackIncomingConnection(authenticated.remoteNodeId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index d9362f4a53..efff3896c6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -100,7 +100,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.willFundRates_opt) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.liquidityAdsConfig.rates_opt) self ! nodeAnn log.info("initialization completed, ready to process messages") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index fc36c0cf6f..858caf25fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -262,7 +262,7 @@ object Validation { // in case this was our first local channel, we make a node announcement if (!d.nodes.contains(nodeParams.nodeId) && isRelatedTo(ann, nodeParams.nodeId)) { log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.willFundRates_opt) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses, nodeParams.features.nodeAnnouncementFeatures(), fundingRates_opt = nodeParams.liquidityAdsConfig.rates_opt) handleNodeAnnouncement(d1, nodeParams.db.network, Set(LocalGossip), nodeAnn) } else d1 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala index cab42821fa..fbf983bad9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LiquidityAds.scala @@ -40,6 +40,12 @@ import scodec.codecs._ */ object LiquidityAds { + /** + * @param rates_opt funding rates if offering to sell liquidity. + * @param lockUtxos whether utxos should be locked during funding, which may expose to liquidity griefing. + */ + case class Config(rates_opt: Option[WillFundRates], lockUtxos: Boolean) + /** * @param miningFee we refund the liquidity provider for some of the fee they paid to miners for the underlying on-chain transaction. * @param serviceFee fee paid to the liquidity provider for the inbound liquidity. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index b10e297188..d9ac1ad8f4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -145,6 +145,7 @@ object TestConstants { channelOpenerWhitelist = Set.empty, maxPendingChannelsPerPeer = 3, maxTotalPendingChannelsPrivateNodes = 99, + channelFundingTimeout = 30 seconds, remoteRbfLimits = RemoteRbfLimits(5, 0), quiescenceTimeout = 2 minutes, balanceThresholds = Nil, @@ -239,7 +240,7 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - willFundRates_opt = Some(defaultLiquidityRates), + liquidityAdsConfig = LiquidityAds.Config(Some(defaultLiquidityRates), lockUtxos = true), peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(enabled = false, timeout = 30 seconds), onTheFlyFundingConfig = OnTheFlyFunding.Config(proposalTimeout = 90 seconds), peerStorageConfig = PeerStorageConfig(writeDelay = 5 seconds, removalDelay = 10 seconds, cleanUpFrequency = 1 hour) @@ -323,6 +324,7 @@ object TestConstants { channelOpenerWhitelist = Set.empty, maxPendingChannelsPerPeer = 3, maxTotalPendingChannelsPrivateNodes = 99, + channelFundingTimeout = 30 seconds, remoteRbfLimits = RemoteRbfLimits(10, 0), quiescenceTimeout = 2 minutes, balanceThresholds = Nil, @@ -417,7 +419,7 @@ object TestConstants { ), purgeInvoicesInterval = None, revokedHtlcInfoCleanerConfig = RevokedHtlcInfoCleaner.Config(10, 100 millis), - willFundRates_opt = Some(defaultLiquidityRates), + liquidityAdsConfig = LiquidityAds.Config(Some(defaultLiquidityRates), lockUtxos = true), peerWakeUpConfig = PeerReadyNotifier.WakeUpConfig(enabled = false, timeout = 30 seconds), onTheFlyFundingConfig = OnTheFlyFunding.Config(proposalTimeout = 90 seconds), peerStorageConfig = PeerStorageConfig(writeDelay = 5 seconds, removalDelay = 10 seconds, cleanUpFrequency = 1 hour) 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 88089fa25a..81e000f58c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -50,7 +50,7 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = { funded += (tx.txid -> tx) Future.successful(FundTransactionResponse(tx, 0 sat, None)) } @@ -105,7 +105,7 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = Promise().future // will never be completed + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], 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 @@ -152,7 +152,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey) - override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, externalInputsWeight: Map[OutPoint, Long], feeBudget_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[FundTransactionResponse] = synchronized { + override def fundTransaction(tx: Transaction, feeRate: FeeratePerKw, replaceable: Boolean, changePosition: Option[Int], 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. @@ -225,7 +225,7 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { 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, feeBudget_opt = feeBudget_opt) + fundedTx <- fundTransaction(tx, feeRatePerKw, 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 dfde6aa86e..7e437a2a57 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 @@ -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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.changePosition.nonEmpty) assert(fundTxResponse.fee > 0.sat) @@ -112,28 +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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(emptyTx, TestConstants.feeratePerKw).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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNonWalletInputs, TestConstants.feeratePerKw).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) + bitcoinClient.fundTransaction(txNotFunded, 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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), TestConstants.feeratePerKw).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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(fundTxResponse1.tx, TestConstants.feeratePerKw * 2).pipeTo(sender.ref) val fundTxResponse2 = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse1.tx != fundTxResponse2.tx) assert(fundTxResponse1.fee < fundTxResponse2.fee) @@ -143,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txManyOutputs, TestConstants.feeratePerKw, replaceable = false, changePosition = Some(1)).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.tx.txOut.size == 3) assert(fundTxResponse.changePosition.contains(1)) @@ -170,25 +170,25 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A case _ => bitcoinClient.rpcClient.invoke(method, params: _*)(ec) } } - new BitcoinCoreClient(badRpcClient, if (useEclairSigner) Some(onChainKeyManager) else None) + new BitcoinCoreClient(badRpcClient, onChainKeyManager_opt = if (useEclairSigner) Some(onChainKeyManager) else None) } { // bitcoin core doesn't specify change position. val evilBitcoinClient = makeEvilBitcoinClient(_ => -1, tx => tx) - evilBitcoinClient.fundTransaction(txNotFunded, FundTransactionOptions(TestConstants.feeratePerKw), feeBudget_opt = None).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw).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), feeBudget_opt = None).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw).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)), feeBudget_opt = None).pipeTo(sender.ref) + evilBitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw, changePosition = Some(0)).pipeTo(sender.ref) sender.expectMsgType[Failure] } } @@ -214,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)), feeBudget_opt = None).pipeTo(sender.ref) + defaultWallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), changePosition = Some(1)).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 @@ -233,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)), feeBudget_opt = None).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = Map(outpoint1 -> smallExternalInputWeight), changePosition = Some(1)).pipeTo(sender.ref) val fundedTx1 = sender.expectMsgType[FundTransactionResponse] assert(fundedTx1.tx.txIn.length >= 2) val amountIn1 = fundedTx1.tx.txIn.map(txIn => { @@ -242,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)), feeBudget_opt = None).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = Map(outpoint1 -> externalInputWeight), changePosition = Some(1)).pipeTo(sender.ref) val fundedTx2 = sender.expectMsgType[FundTransactionResponse] assert(fundedTx2.tx.txIn.length >= 2) assert(fundedTx1.fee < fundedTx2.fee) @@ -278,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)), feeBudget_opt = None).pipeTo(sender.ref) + walletExternalFunds.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = Map(externalOutpoint -> externalInputWeight), changePosition = Some(1)).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse] assert(fundedTx.tx.txIn.length >= 2) // We sign our external input. @@ -310,9 +310,9 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val txNotFunded = tx3.copy(txOut = tx3.txOut.take(1)) val inputWeights = txNotFunded.txIn.map(txIn => { 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)), feeBudget_opt = None).pipeTo(sender.ref) + txIn.outPoint -> weight.toLong + }).toMap + walletExternalFunds.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = inputWeights, changePosition = Some(1)).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse] assert(fundedTx.tx.txIn.length >= 2) assert(fundedTx.tx.txOut.length == 2) @@ -363,7 +363,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txIn, FeeratePerKw(250 sat)).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.fee == Satoshi(satoshi)) } @@ -444,7 +444,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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw).pipeTo(sender.ref) sender.expectMsgType[FundTransactionResponse].tx }) val fundedTx = Transaction(2, fundedTxs.flatMap(_.txIn), fundedTxs.flatMap(_.txOut), 0) @@ -467,7 +467,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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, TestConstants.feeratePerKw * 2).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx assert(fundedTx.txIn.length >= 2) // we added at least one new input @@ -574,7 +574,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val Seq(anchorTx1, anchorTx2) = Seq(FeeratePerKw(1000 sat), FeeratePerKw(2000 sat)).map { feerate => val externalInput = Map(commitOutpoint -> Transactions.claimP2WPKHOutputWeight.toLong) val txNotFunded = Transaction(2, Seq(TxIn(commitOutpoint, Nil, 0)), Seq(TxOut(200_000 sat, Script.pay2wpkh(priv.publicKey))), 0) - wallet.fundTransaction(txNotFunded, feerate, replaceable = true, externalInput).pipeTo(sender.ref) + wallet.fundTransaction(txNotFunded, feerate, externalInputsWeight = externalInput).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) val partiallySignedTx = sender.expectMsgType[SignTransactionResponse].tx assert(partiallySignedTx.txIn.size == 2) // a single wallet input should have been added @@ -648,7 +648,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val Seq(htlcSuccessTx, htlcTimeoutTx) = Seq((wallet1, FeeratePerKw(1000 sat)), (wallet2, FeeratePerKw(2000 sat))).map { case (wallet, feerate) => val externalInput = Map(commitOutpoint -> Transactions.claimP2WPKHOutputWeight.toLong) val txNotFunded = Transaction(2, Seq(TxIn(commitOutpoint, Nil, 0)), Seq(TxOut(200_000 sat, Script.pay2wpkh(priv.publicKey))), 0) - wallet.fundTransaction(txNotFunded, feerate, replaceable = true, externalInput).pipeTo(sender.ref) + wallet.fundTransaction(txNotFunded, feerate, externalInputsWeight = externalInput).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) val partiallySignedTx = sender.expectMsgType[SignTransactionResponse].tx assert(partiallySignedTx.txIn.size == 2) // a single wallet input should have been added @@ -787,8 +787,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = makeBitcoinCoreClient() 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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).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 @@ -797,7 +796,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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(400000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).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 => { @@ -825,7 +824,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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(75000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).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) @@ -834,14 +833,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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(125000 sat, Script.pay2wpkh(nonWalletKey.publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(350000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).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) @@ -863,7 +862,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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx, TestConstants.feeratePerKw).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) @@ -919,7 +918,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), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(100000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0), TestConstants.feeratePerKw).pipeTo(sender.ref) val txUnsignedInputs = sender.expectMsgType[FundTransactionResponse].tx bitcoinClient.publishTransaction(txUnsignedInputs).pipeTo(sender.ref) sender.expectMsgType[Failure] @@ -1028,8 +1027,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = makeBitcoinCoreClient() // 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, feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, Seq(TxOut(250000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), TestConstants.feeratePerKw, changePosition = Some(1)).pipeTo(sender.ref) val fundedTx1 = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, fundedTx1).pipeTo(sender.ref) val signedTx1 = sender.expectMsgType[SignTransactionResponse].tx @@ -1098,7 +1096,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, fundingFeerate, changePosition = Some(1)).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] assert(fundTxResponse.changePosition.contains(1)) signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) @@ -1168,7 +1166,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, currentFeerate, changePosition = Some(txOut.length)).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] @@ -1277,7 +1275,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(txNotFunded, FeeratePerKw(1000 sat), changePosition = Some(1)).pipeTo(sender.ref) val fundTxResponse = sender.expectMsgType[FundTransactionResponse] signTransaction(bitcoinClient, fundTxResponse.tx).pipeTo(sender.ref) val signTxResponse = sender.expectMsgType[SignTransactionResponse] @@ -1305,7 +1303,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx1, FeeratePerKw(2500 sat)).pipeTo(sender.ref) val unsignedTx1 = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, unsignedTx1).pipeTo(sender.ref) val tx1 = sender.expectMsgType[SignTransactionResponse].tx @@ -1346,7 +1344,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)), feeBudget_opt = None).pipeTo(sender.ref) + bitcoinClient.fundTransaction(noInputTx, FeeratePerKw(2500 sat)).pipeTo(sender.ref) val unsignedTx = sender.expectMsgType[FundTransactionResponse].tx signTransaction(bitcoinClient, unsignedTx).pipeTo(sender.ref) sender.expectMsgType[SignTransactionResponse].tx @@ -1504,7 +1502,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val parentTxs = (1 to 15).map { _ => val outputs = Seq.fill(largeInputsCount)(TxOut(50_000 sat, Script.pay2wsh(bigInputScript))) val txNotFunded = Transaction(2, Nil, outputs, 0) - miner.fundTransaction(txNotFunded, FundTransactionOptions(FeeratePerKw(500 sat), changePosition = Some(outputs.length)), feeBudget_opt = None).pipeTo(sender.ref) + miner.fundTransaction(txNotFunded, FeeratePerKw(500 sat), changePosition = Some(outputs.length)).pipeTo(sender.ref) val fundedTx = sender.expectMsgType[FundTransactionResponse].tx signTransaction(miner, fundedTx).pipeTo(sender.ref) val signedTx = sender.expectMsgType[SignTransactionResponse].tx @@ -1580,7 +1578,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val localSpliceTx = { val txNotFunded = Transaction(2, Seq(TxIn(OutPoint(remoteSpliceTx2, 0), Nil, 0)), Seq(TxOut(900_000 sat, fundingScript)), 0) val externalWeight = Map(txNotFunded.txIn.head.outPoint -> 390L) - wallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), replaceable = true, externalWeight).pipeTo(sender.ref) + wallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), externalInputsWeight = externalWeight).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) val partiallySignedTx = sender.expectMsgType[SignTransactionResponse].tx val fundingIndex = partiallySignedTx.txIn.indexWhere(_.outPoint == OutPoint(remoteSpliceTx2, 0)) @@ -1611,7 +1609,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val localAnchorTx = { val txNotFunded = Transaction(2, Seq(TxIn(OutPoint(localCommitTx, 1), Nil, 0)), Seq(TxOut(300_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0) val externalWeight = Map(txNotFunded.txIn.head.outPoint -> Transactions.anchorInputWeight.toLong) - wallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), replaceable = true, externalWeight).pipeTo(sender.ref) + wallet.fundTransaction(txNotFunded, FeeratePerKw(2500 sat), externalInputsWeight = externalWeight).pipeTo(sender.ref) signTransaction(wallet, sender.expectMsgType[FundTransactionResponse].tx).pipeTo(sender.ref) val partiallySignedTx = sender.expectMsgType[SignTransactionResponse].tx val anchorIndex = partiallySignedTx.txIn.indexWhere(_.outPoint == OutPoint(localCommitTx, 1)) @@ -1717,7 +1715,7 @@ class BitcoinCoreClientWithEclairSignerSpec extends BitcoinCoreClientSpec { val name = s"eclair_${seed.toHex.take(16)}" val onChainKeyManager = new LocalOnChainKeyManager(name, seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) val jsonRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(name)) - (new BitcoinCoreClient(jsonRpcClient, Some(onChainKeyManager)), onChainKeyManager) + (new BitcoinCoreClient(jsonRpcClient, onChainKeyManager_opt = Some(onChainKeyManager)), onChainKeyManager) } test("wallets managed by eclair implement BIP84") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 8f87d409ba..641b27a543 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -133,7 +133,7 @@ trait BitcoindService extends Logging { })) } - def makeBitcoinCoreClient(): BitcoinCoreClient = new BitcoinCoreClient(bitcoinrpcclient, if (useEclairSigner) Some(onChainKeyManager) else None) + def makeBitcoinCoreClient(): BitcoinCoreClient = new BitcoinCoreClient(bitcoinrpcclient, onChainKeyManager_opt = if (useEclairSigner) Some(onChainKeyManager) else None) def stopBitcoind(): Unit = { // gracefully stopping bitcoin will make it store its state cleanly to disk, which is good for later debugging 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 18adb2d1e0..3b94944cf0 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 @@ -405,7 +405,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)), feeBudget_opt = None).pipeTo(probe.ref) + bitcoinClient.fundTransaction(Transaction(2, Nil, TxOut(150000 sat, Script.pay2wpkh(priv.publicKey)) :: Nil, 0), FeeratePerKw(250 sat)).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/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 84e161dfd5..e2e18dcb06 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -1802,7 +1802,7 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS val seed = MnemonicCode.toSeed(MnemonicCode.toMnemonics(entropy), walletName) val keyManager = new LocalOnChainKeyManager(walletName, seed, TimestampSecond.now(), Block.RegtestGenesisBlock.hash) val walletRpcClient = new BasicBitcoinJsonRPCClient(rpcAuthMethod = bitcoinrpcauthmethod, host = "localhost", port = bitcoindRpcPort, wallet = Some(walletName)) - val walletClient = new BitcoinCoreClient(walletRpcClient, Some(keyManager)) with OnchainPubkeyCache { + val walletClient = new BitcoinCoreClient(walletRpcClient, onChainKeyManager_opt = Some(keyManager)) with OnchainPubkeyCache { lazy val pubkey = { getP2wpkhPubkey().pipeTo(probe.ref) probe.expectMsgType[PublicKey] 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 ac64eb146a..0c1fc8e032 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 @@ -28,6 +28,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentFeerates, DummyOnChainWallet} import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.states.ChannelStateTestsTags import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.message.OnionMessages.{Recipient, buildMessage} @@ -76,6 +77,7 @@ class PeerSpec extends FixtureSpec { .modify(_.features).setToIf(testData.tags.contains(ChannelStateTestsTags.DualFunding))(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional)) .modify(_.channelConf.maxHtlcValueInFlightMsat).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(100_000_000 msat) .modify(_.channelConf.maxHtlcValueInFlightPercent).setToIf(testData.tags.contains("max-htlc-value-in-flight-percent"))(25) + .modify(_.channelConf.channelFundingTimeout).setToIf(testData.tags.contains("channel_funding_timeout"))(100 millis) .modify(_.autoReconnect).setToIf(testData.tags.contains("auto_reconnect"))(true) if (testData.tags.contains("with_node_announcement")) { @@ -718,6 +720,17 @@ class PeerSpec extends FixtureSpec { assert(channelAborted.channelId == open.temporaryChannelId) } + test("abort incoming channel after funding timeout", Tag(ChannelStateTestsTags.DualFunding), Tag("channel_funding_timeout")) { f => + import f._ + + connect(remoteNodeId, peer, peerConnection, switchboard, remoteInit = protocol.Init(Features(StaticRemoteKey -> Optional, AnchorOutputsZeroFeeHtlcTx -> Optional, DualFunding -> Optional))) + val open = createOpenDualFundedChannelMessage() + peerConnection.send(peer, open) + assert(channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR].dualFunded) + channel.expectMsg(open) + channel.expectMsg(Channel.TickChannelOpenTimeout) + } + test("kill peer with no channels if connection dies before receiving `ConnectionReady`") { f => import f._ val probe = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala index c37f1fd1a3..3f14abb27f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala @@ -564,7 +564,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] assert(!init.localParams.isChannelOpener) assert(init.localParams.paysCommitTxFees) - assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.willFundRates_opt))) + assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt))) // The preimage was provided, so we fulfill upstream HTLCs. verifyFulfilledUpstream(upstream, preimage) @@ -617,7 +617,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike { val init = channel.expectMsgType[INPUT_INIT_CHANNEL_NON_INITIATOR] assert(!init.localParams.isChannelOpener) assert(init.localParams.paysCommitTxFees) - assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.willFundRates_opt))) + assert(init.fundingContribution_opt.contains(LiquidityAds.AddFunding(requestFunding.requestedAmount, nodeParams.liquidityAdsConfig.rates_opt))) assert(channel.expectMsgType[OpenDualFundedChannel].useFeeCredit_opt.contains(3_000_000 msat)) // Once the funding transaction is signed, we remove the fee credit consumed.