diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 43d06344c7..66e1c0a24d 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -23,6 +23,21 @@ eclair.on-chain-fees.confirmation-priority { This configuration section replaces the previous `eclair.on-chain-fees.target-blocks` section. +### Add configurable maximum anchor fee + +Whenever an anchor outputs channel force-closes, we regularly bump the fees of the commitment transaction to get it to confirm. +We previously ensured that the fees paid couldn't exceed 5% of our channel balance, but that may already be extremely high for large channels. +Without package relay, our anchor transactions may not propagate to miners, and eclair may end up bumping the fees more than the actual feerate, because it cannot know why the transaction isn't confirming. + +We introduced a new parameter to control the maximum fee that will be paid during fee-bumping, that node operators may configure: + +```eclair.conf +// maximum amount of fees we will pay to bump an anchor output when we have no HTLC at risk +eclair.on-chain-fees.anchor-without-htlcs-max-fee-satoshis = 10000 +``` + +We also limit the feerate used to be at most the same order of magnitude of `fatest` feerate provided by our fee estimator. + ### Managing Bitcoin Core wallet keys You can now use Eclair to manage the private keys for on-chain funds monitored by a Bitcoin Core watch-only wallet. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 82fe9de1d5..89ae71d876 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -294,6 +294,8 @@ eclair { // it can still be manually fee-bumped using the bumpforceclose RPC // *do not change this unless you know what you are doing* spend-anchor-without-htlcs = true + // maximum amount of fees we will pay to bump an anchor output when we have no HTLC at risk + anchor-without-htlcs-max-fee-satoshis = 10000 close-on-offline-feerate-mismatch = true // do not change this unless you know what you are doing 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 90e108e96c..d77aab4687 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -528,6 +528,7 @@ object NodeParams extends Logging { feeTargets = feeTargets, safeUtxosThreshold = config.getInt("on-chain-fees.safe-utxos-threshold"), spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), + anchorWithoutHtlcsMaxFee = Satoshi(config.getLong("on-chain-fees.anchor-without-htlcs-max-fee-satoshis")), closeOnOfflineMismatch = config.getBoolean("on-chain-fees.close-on-offline-feerate-mismatch"), updateFeeMinDiffRatio = config.getDouble("on-chain-fees.update-fee-min-diff-ratio"), defaultFeerateTolerance = FeerateTolerance( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index 09bb60c884..eebac5510b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -81,7 +81,14 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax } } -case class OnChainFeeConf(feeTargets: FeeTargets, safeUtxosThreshold: Int, spendAnchorWithoutHtlcs: Boolean, closeOnOfflineMismatch: Boolean, updateFeeMinDiffRatio: Double, private val defaultFeerateTolerance: FeerateTolerance, private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { +case class OnChainFeeConf(feeTargets: FeeTargets, + safeUtxosThreshold: Int, + spendAnchorWithoutHtlcs: Boolean, + anchorWithoutHtlcsMaxFee: Satoshi, + closeOnOfflineMismatch: Boolean, + updateFeeMinDiffRatio: Double, + private val defaultFeerateTolerance: FeerateTolerance, + private val perNodeFeerateTolerance: Map[PublicKey, FeerateTolerance]) { def feerateToleranceFor(nodeId: PublicKey): FeerateTolerance = perNodeFeerateTolerance.getOrElse(nodeId, defaultFeerateTolerance) 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 bdd663d09e..5221e98c56 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 @@ -24,7 +24,7 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Script, Tr 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 +import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.FullCommitment import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext @@ -75,7 +75,7 @@ object ReplaceableTxFunder { Behaviors.withMdc(txPublishContext.mdc()) { Behaviors.receiveMessagePartial { case FundTransaction(replyTo, cmd, tx, requestedFeerate) => - val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment)) + val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment, nodeParams.currentFeerates, nodeParams.onChainFeeConf)) val txFunder = new ReplaceableTxFunder(nodeParams, replyTo, cmd, bitcoinClient, context) tx match { case Right(txWithWitnessData) => txFunder.fund(txWithWitnessData, targetFeerate) @@ -96,7 +96,7 @@ object ReplaceableTxFunder { * The on-chain feerate can be arbitrarily high, but it wouldn't make sense to pay more fees than the amount we're * trying to claim on-chain. We compute how much funds we have at risk and the feerate that matches this amount. */ - def maxFeerate(txInfo: ReplaceableTransactionWithInputInfo, commitment: FullCommitment): FeeratePerKw = { + def maxFeerate(txInfo: ReplaceableTransactionWithInputInfo, commitment: FullCommitment, currentFeerates: FeeratesPerKw, feeConf: OnChainFeeConf): FeeratePerKw = { // We don't want to pay more in fees than the amount at risk in untrimmed pending HTLCs. val maxFee = txInfo match { case tx: HtlcTx => tx.input.txOut.amount @@ -104,9 +104,10 @@ object ReplaceableTxFunder { case _: ClaimLocalAnchorOutputTx => val htlcBalance = commitment.localCommit.htlcTxsAndRemoteSigs.map(_.htlcTx.input.txOut.amount).sum val mainBalance = commitment.localCommit.spec.toLocal.truncateToSatoshi - // If there are no HTLCs or a low HTLC amount, we may still want to get back our main balance. - // In that case, we spend at most 5% of our balance in fees. - htlcBalance.max(mainBalance * 5 / 100) + // If there are no HTLCs or a low HTLC amount, we still want to get back our main balance. + // In that case, we spend at most 5% of our balance in fees, with a hard cap configured by the node operator. + val mainBalanceFee = (mainBalance * 5 / 100).min(feeConf.anchorWithoutHtlcsMaxFee) + htlcBalance.max(mainBalanceFee) } // We cannot know beforehand how many wallet inputs will be added, but an estimation should be good enough. val weight = txInfo match { @@ -118,7 +119,8 @@ object ReplaceableTxFunder { case _: ClaimHtlcTimeoutTx => Transactions.claimHtlcTimeoutWeight case _: ClaimLocalAnchorOutputTx => commitWeight(commitment) + Transactions.claimAnchorOutputMinWeight } - Transactions.fee2rate(maxFee, weight) + // It doesn't make sense to use a feerate that is much higher than the current feerate for inclusion into the next block. + Transactions.fee2rate(maxFee, weight).min(currentFeerates.fastest * 1.25) } /** 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 19ad419cf3..56ba361d16 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig import fr.acinq.eclair.payment.relay.Relayer.{AsyncPaymentsParams, RelayFees, RelayParams} import fr.acinq.eclair.router.Graph.{MessagePath, WeightRatios} import fr.acinq.eclair.router.PathFindingExperimentConf -import fr.acinq.eclair.router.Router.{MessageRouteParams, MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} +import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress, OnionRoutingPacket} import org.scalatest.Tag import scodec.bits.{ByteVector, HexStringSyntax} @@ -143,6 +143,7 @@ object TestConstants { feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, + anchorWithoutHtlcsMaxFee = 100_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.5, 8.0, anchorOutputsFeeratePerKw, DustTolerance(25_000 sat, closeOnUpdateFeeOverflow = true)), @@ -308,6 +309,7 @@ object TestConstants { feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, + anchorWithoutHtlcsMaxFee = 100_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance = FeerateTolerance(0.75, 1.5, anchorOutputsFeeratePerKw, DustTolerance(30_000 sat, closeOnUpdateFeeOverflow = true)), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala index 73eb5ac011..37e8dc470a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala @@ -16,11 +16,10 @@ package fr.acinq.eclair.blockchain.fee -import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Satoshi, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.channel.ChannelTypes import fr.acinq.eclair.randomKey -import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, UnsafeLegacyAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat} import org.scalatest.funsuite.AnyFunSuite class OnChainFeeConfSpec extends AnyFunSuite { @@ -29,7 +28,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { private val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat))) @@ -39,7 +38,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { test("get commitment feerate") { val commitmentFormat = DefaultCommitmentFormat - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(5000 sat)) assert(feeConf.getCommitmentFeerate(feerates1, randomKey().publicKey, commitmentFormat, 100000 sat) == FeeratePerKw(5000 sat)) @@ -53,7 +52,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat)) assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index 4ec9b9a026..f3cfa2940b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -45,6 +45,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, + anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = false, 1.0, FeerateTolerance(0.00001, 100000.0, TestConstants.anchorOutputsFeeratePerKw, DustTolerance(100000 sat, closeOnUpdateFeeOverflow = false)), 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 de7327fa87..568723d016 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 @@ -83,9 +83,9 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def bobBlockHeight(): BlockHeight = bob.underlyingActor.nodeParams.currentBlockHeight /** Set uniform feerate for all block targets. */ - def setFeerate(feerate: FeeratePerKw): Unit = { - alice.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(feerate)) - bob.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(feerate)) + def setFeerate(feerate: FeeratePerKw, fastest: FeeratePerKw = FeeratePerKw(100_000 sat)): Unit = { + alice.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(feerate).copy(fastest = fastest)) + bob.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(feerate).copy(fastest = fastest)) } /** Set feerate for a specific block target. */ @@ -171,7 +171,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Execute our test. val publisher = system.spawn(ReplaceableTxPublisher(aliceNodeParams, walletClient, alice2blockchain.ref, TxPublishContext(testId, TestConstants.Bob.nodeParams.nodeId, None)), testId.toString) - testFun(Fixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, walletClient, walletRpcClient, publisher, probe)) + val f = Fixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, walletClient, walletRpcClient, publisher, probe) + // We set a high fastest feerate, to ensure that by default we're not limited by this. + f.setFeerate(FeeratePerKw(100_000 sat), blockTarget = 1) + testFun(f) } def closeChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (PublishFinalTx, PublishReplaceableTx) = { @@ -493,7 +496,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsg(commitTx.tx.txid) assert(getMempool().length == 1) - val maxFeerate = ReplaceableTxFunder.maxFeerate(anchorTx.txInfo, anchorTx.commitment) + val maxFeerate = ReplaceableTxFunder.maxFeerate(anchorTx.txInfo, anchorTx.commitment, alice.underlyingActor.nodeParams.currentFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) val targetFeerate = FeeratePerKw(50_000 sat) assert(maxFeerate <= targetFeerate / 2) setFeerate(targetFeerate, blockTarget = 12) @@ -515,6 +518,31 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } + test("commit tx feerate too low, spending anchor output (fastest feerate threshold)") { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + import f._ + + val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) + wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) + probe.expectMsg(commitTx.tx.txid) + assert(getMempool().length == 1) + + val fastestFeerate = FeeratePerKw(15_000 sat) + setFeerate(fastestFeerate, blockTarget = 1) + val targetFeerate = FeeratePerKw(50_000 sat) + setFeerate(targetFeerate, blockTarget = 12) + publisher ! Publish(probe.ref, anchorTx) + // wait for the commit tx and anchor tx to be published + val mempoolTxs = getMempoolTxs(2) + assert(mempoolTxs.map(_.txid).contains(commitTx.tx.txid)) + + // We allow up to 25% more than the fastest feerate. + val targetFee = Transactions.weight2fee(fastestFeerate * 1.25, mempoolTxs.map(_.weight).sum.toInt) + val actualFee = mempoolTxs.map(_.fees).sum + assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") + } + } + test("commit tx not published, publishing it and spending anchor output") { withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ @@ -1127,12 +1155,12 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val (commitTx, htlcSuccess, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 30, outgoingHtlcAmount = 5_000_000 msat, incomingHtlcAmount = 4_000_000 msat) setFeerate(targetFeerate, blockTarget = 12) assert(htlcSuccess.txInfo.fee == 0.sat) - val htlcSuccessMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcSuccess.txInfo, htlcSuccess.commitment) + val htlcSuccessMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcSuccess.txInfo, htlcSuccess.commitment, alice.underlyingActor.nodeParams.currentFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) assert(htlcSuccessMaxFeerate < targetFeerate / 2) val htlcSuccessTx = testPublishHtlcSuccess(f, commitTx, htlcSuccess, htlcSuccessMaxFeerate) assert(htlcSuccessTx.txIn.length > 1) assert(htlcTimeout.txInfo.fee == 0.sat) - val htlcTimeoutMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcTimeout.txInfo, htlcTimeout.commitment) + val htlcTimeoutMaxFeerate = ReplaceableTxFunder.maxFeerate(htlcTimeout.txInfo, htlcTimeout.commitment, alice.underlyingActor.nodeParams.currentFeerates, alice.underlyingActor.nodeParams.onChainFeeConf) assert(htlcTimeoutMaxFeerate < targetFeerate / 2) val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, htlcTimeoutMaxFeerate) assert(htlcTimeoutTx.txIn.length > 1) @@ -1278,7 +1306,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w import f._ val feerate = FeeratePerKw(15_000 sat) - setFeerate(feerate) + setFeerate(feerate, fastest = feerate) // The confirmation target for htlc-timeout corresponds to their CLTV: we should claim them asap once the htlc has timed out. val (commitTx, _, htlcTimeout) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) @@ -1297,6 +1325,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcTimeoutTx1.txid == htlcTimeoutTxId1) // A new block is found, and we've already reached the confirmation target, so we bump the fees. + setFeerate(feerate, fastest = feerate * 1.2) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) val htlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(htlcTimeoutTx1.txid)) @@ -1673,13 +1702,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) // The Claim-HTLC-success tx will be immediately published. - setFeerate(initialFeerate) + setFeerate(initialFeerate, fastest = targetFeerate) val claimHtlcSuccessPublisher = createPublisher() claimHtlcSuccessPublisher ! Publish(probe.ref, claimHtlcSuccess) val claimHtlcSuccessTx1 = getMempoolTxs(1).head assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcSuccessTx1.txid) - setFeerate(targetFeerate) + setFeerate(targetFeerate, fastest = targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 5)) val claimHtlcSuccessTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(claimHtlcSuccessTx1.txid)) @@ -1694,7 +1723,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(finalHtlcSuccessTx.txIn.head.outPoint.txid == remoteCommitTx.txid) // The Claim-HTLC-timeout will be published after the timeout. - setFeerate(initialFeerate) + setFeerate(initialFeerate, fastest = targetFeerate) val claimHtlcTimeoutPublisher = createPublisher() claimHtlcTimeoutPublisher ! Publish(probe.ref, claimHtlcTimeout) generateBlocks(144) @@ -1703,7 +1732,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val claimHtlcTimeoutTx1 = getMempoolTxs(1).head assert(listener.expectMsgType[TransactionPublished].tx.txid == claimHtlcTimeoutTx1.txid) - setFeerate(targetFeerate) + setFeerate(targetFeerate, fastest = targetFeerate) system.eventStream.publish(CurrentBlockHeight(aliceBlockHeight() + 145)) val claimHtlcTimeoutTxId2 = listener.expectMsgType[TransactionPublished].tx.txid assert(!isInMempool(claimHtlcTimeoutTx1.txid))