diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 32f4447418..f2f56bb9c2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -150,6 +150,7 @@ eclair { commitment-without-htlcs = 12 // target for the commitment transaction when we have no htlcs to claim (used in force-close scenario) *do not change this unless you know what you are doing* mutual-close = 12 // target for the mutual close transaction claim-main = 12 // target for the claim main transaction (tx that spends main channel output back to wallet) + safe-utxos-threshold = 10 // when our utxos count is below this threshold, we will use more aggressive confirmation targets in force-close scenarios } feerate-tolerance { @@ -247,7 +248,7 @@ eclair { channel-capacity = 0.55 // when computing the weight for a channel, consider its CAPACITY in this proportion } locked-funds-risk = 1e-8 // msat per msat locked per block. It should be your expected interest rate per block multiplied by the probability that something goes wrong and your funds stay locked. - // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. + // 1e-8 corresponds to an interest rate of ~5% per year (1e-6 per block) and a probability of 1% that the channel will fail and our funds will be locked. // virtual fee for failed payments: how much you are willing to pay to get one less failed payment attempt failure-cost { fee-base-msat = 2000 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 b5193ac230..e931c1d5f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -319,7 +319,8 @@ object NodeParams extends Logging { commitmentBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment"), commitmentWithoutHtlcsBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment-without-htlcs"), mutualCloseBlockTarget = config.getInt("on-chain-fees.target-blocks.mutual-close"), - claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main") + claimMainBlockTarget = config.getInt("on-chain-fees.target-blocks.claim-main"), + safeUtxosThreshold = config.getInt("on-chain-fees.target-blocks.safe-utxos-threshold"), ) def getRelayFees(relayFeesConfig: Config): RelayFees = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala index 10590d8965..25f436d9fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala @@ -30,7 +30,7 @@ trait FeeEstimator { // @formatter:on } -case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, commitmentWithoutHtlcsBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int) +case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, commitmentWithoutHtlcsBlockTarget: Int, mutualCloseBlockTarget: Int, claimMainBlockTarget: Int, safeUtxosThreshold: Int) /** * @param maxExposure maximum exposure to pending dust htlcs we tolerate: we will automatically fail HTLCs when going above this threshold. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala index 0ca1c38e90..20a837a61e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisher.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.publish import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.{ActorRef, Behavior} -import fr.acinq.bitcoin.{OutPoint, Transaction} +import fr.acinq.bitcoin.{OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeratePerKw} @@ -29,7 +29,7 @@ import fr.acinq.eclair.{BlockHeight, NodeParams} import scala.concurrent.duration.{DurationInt, DurationLong} import scala.concurrent.{ExecutionContext, Future} -import scala.util.Random +import scala.util.{Failure, Random, Success} /** * Created by t-bast on 10/06/2021. @@ -50,6 +50,7 @@ object ReplaceableTxPublisher { private case class WrappedPreconditionsResult(result: ReplaceableTxPrePublisher.PreconditionsResult) extends Command private case object TimeLocksOk extends Command + private case class CheckUtxosResult(isSafe: Boolean, currentBlockHeight: BlockHeight) extends Command private case class WrappedFundingResult(result: ReplaceableTxFunder.FundingResult) extends Command private case class WrappedTxResult(result: MempoolTxMonitor.TxResult) extends Command private case class BumpFee(targetFeerate: FeeratePerKw) extends Command @@ -73,20 +74,29 @@ object ReplaceableTxPublisher { } } - def getFeerate(feeEstimator: FeeEstimator, confirmBefore: BlockHeight, currentBlockHeight: BlockHeight): FeeratePerKw = { + def getFeerate(feeEstimator: FeeEstimator, confirmBefore: BlockHeight, currentBlockHeight: BlockHeight, hasEnoughSafeUtxos: Boolean): FeeratePerKw = { val remainingBlocks = confirmBefore - currentBlockHeight - val blockTarget = remainingBlocks match { - // If our target is still very far in the future, no need to rush - case t if t >= 144 => 144 - case t if t >= 72 => 72 - case t if t >= 36 => 36 - // However, if we get closer to the target, we start being more aggressive - case t if t >= 18 => 12 - case t if t >= 12 => 6 - case t if t >= 2 => 2 - case _ => 1 + if (hasEnoughSafeUtxos) { + val blockTarget = remainingBlocks match { + // If our target is still very far in the future, no need to rush + case t if t >= 144 => 144 + case t if t >= 72 => 72 + case t if t >= 36 => 36 + // However, if we get closer to the target, we start being more aggressive + case t if t >= 18 => 12 + case t if t >= 12 => 6 + case t if t >= 2 => 2 + case _ => 1 + } + feeEstimator.getFeeratePerKw(blockTarget) + } else { + // We don't have many safe utxos so we want the transaction to confirm quickly. + if (remainingBlocks <= 1) { + feeEstimator.getFeeratePerKw(1) + } else { + feeEstimator.getFeeratePerKw(2) + } } - feeEstimator.getFeeratePerKw(blockTarget) } } @@ -125,12 +135,12 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, def checkTimeLocks(txWithWitnessData: ReplaceableTxWithWitnessData): Behavior[Command] = { txWithWitnessData match { // There are no time locks on anchor transactions, we can claim them right away. - case _: ClaimLocalAnchorWithWitnessData => fund(txWithWitnessData) + case _: ClaimLocalAnchorWithWitnessData => chooseFeerate(txWithWitnessData) case _ => val timeLocksChecker = context.spawn(TxTimeLocksMonitor(nodeParams, watcher, txPublishContext), "time-locks-monitor") timeLocksChecker ! TxTimeLocksMonitor.CheckTx(context.messageAdapter[TxTimeLocksMonitor.TimeLocksOk](_ => TimeLocksOk), cmd.txInfo.tx, cmd.desc) Behaviors.receiveMessagePartial { - case TimeLocksOk => fund(txWithWitnessData) + case TimeLocksOk => chooseFeerate(txWithWitnessData) case UpdateConfirmationTarget(target) => confirmBefore = target Behaviors.same @@ -139,8 +149,23 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } } - def fund(txWithWitnessData: ReplaceableTxWithWitnessData): Behavior[Command] = { - val targetFeerate = getFeerate(nodeParams.onChainFeeConf.feeEstimator, confirmBefore, nodeParams.currentBlockHeight) + def chooseFeerate(txWithWitnessData: ReplaceableTxWithWitnessData): Behavior[Command] = { + context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { + case Success(isSafe) => CheckUtxosResult(isSafe, nodeParams.currentBlockHeight) + case Failure(_) => CheckUtxosResult(isSafe = false, nodeParams.currentBlockHeight) // if we can't check our utxos, we assume the worst + } + Behaviors.receiveMessagePartial { + case CheckUtxosResult(isSafe, currentBlockHeight) => + val targetFeerate = getFeerate(nodeParams.onChainFeeConf.feeEstimator, confirmBefore, currentBlockHeight, isSafe) + fund(txWithWitnessData, targetFeerate) + case UpdateConfirmationTarget(target) => + confirmBefore = target + Behaviors.same + case Stop => Behaviors.stopped + } + } + + def fund(txWithWitnessData: ReplaceableTxWithWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = { val txFunder = context.spawn(ReplaceableTxFunder(nodeParams, bitcoinClient, txPublishContext), "tx-funder") txFunder ! ReplaceableTxFunder.FundTransaction(context.messageAdapter[ReplaceableTxFunder.FundingResult](WrappedFundingResult), cmd, Right(txWithWitnessData), targetFeerate) Behaviors.receiveMessagePartial { @@ -171,26 +196,32 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, case WrappedTxResult(txResult) => txResult match { case MempoolTxMonitor.TxInMempool(_, currentBlockHeight) => - // We make sure we increase the fees by at least 20% as we get closer to the confirmation target. - val bumpRatio = 1.2 - val currentFeerate = getFeerate(nodeParams.onChainFeeConf.feeEstimator, confirmBefore, currentBlockHeight) - val targetFeerate_opt = if (confirmBefore <= currentBlockHeight + 6) { - log.debug("{} confirmation target is close (in {} blocks): bumping fees", cmd.desc, confirmBefore - currentBlockHeight) - Some(currentFeerate.max(tx.feerate * bumpRatio)) - } else if (tx.feerate * bumpRatio <= currentFeerate) { - log.debug("{} confirmation target is in {} blocks: bumping fees", cmd.desc, confirmBefore - currentBlockHeight) - Some(currentFeerate) - } else { - log.debug("{} confirmation target is in {} blocks: no need to bump fees", cmd.desc, confirmBefore - currentBlockHeight) - None + context.pipeToSelf(hasEnoughSafeUtxos(nodeParams.onChainFeeConf.feeTargets.safeUtxosThreshold)) { + case Success(isSafe) => CheckUtxosResult(isSafe, currentBlockHeight) + case Failure(_) => CheckUtxosResult(isSafe = false, currentBlockHeight) // if we can't check our utxos, we assume the worst } - // We avoid a herd effect whenever we fee bump transactions. - targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) Behaviors.same case MempoolTxMonitor.TxRecentlyConfirmed(_, _) => Behaviors.same // just wait for the tx to be deeply buried case MempoolTxMonitor.TxDeeplyBuried(confirmedTx) => sendResult(TxPublisher.TxConfirmed(cmd, confirmedTx), None) case MempoolTxMonitor.TxRejected(_, reason) => sendResult(TxPublisher.TxRejected(txPublishContext.id, cmd, reason), Some(Seq(tx.signedTx))) } + case CheckUtxosResult(isSafe, currentBlockHeight) => + // We make sure we increase the fees by at least 20% as we get closer to the confirmation target. + val bumpRatio = 1.2 + val currentFeerate = getFeerate(nodeParams.onChainFeeConf.feeEstimator, confirmBefore, currentBlockHeight, isSafe) + val targetFeerate_opt = if (confirmBefore <= currentBlockHeight + 6) { + log.debug("{} confirmation target is close (in {} blocks): bumping fees", cmd.desc, confirmBefore - currentBlockHeight) + Some(currentFeerate.max(tx.feerate * bumpRatio)) + } else if (tx.feerate * bumpRatio <= currentFeerate) { + log.debug("{} confirmation target is in {} blocks: bumping fees", cmd.desc, confirmBefore - currentBlockHeight) + Some(currentFeerate) + } else { + log.debug("{} confirmation target is in {} blocks: no need to bump fees", cmd.desc, confirmBefore - currentBlockHeight) + None + } + // We avoid a herd effect whenever we fee bump transactions. + targetFeerate_opt.foreach(targetFeerate => timers.startSingleTimer(BumpFeeKey, BumpFee(targetFeerate), (1 + Random.nextLong(nodeParams.maxTxPublishRetryDelay.toMillis)).millis)) + Behaviors.same case BumpFee(targetFeerate) => fundReplacement(targetFeerate, tx) case UpdateConfirmationTarget(target) => confirmBefore = target @@ -347,5 +378,10 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams, } } + /** If we don't have a lot of safe utxos left, we will use an aggressive feerate to ensure our utxos aren't locked for too long. */ + private def hasEnoughSafeUtxos(threshold: Int): Future[Boolean] = { + bitcoinClient.listUnspent().map(_.count(utxo => utxo.safe && utxo.amount >= 10_000.sat) >= threshold) + } + } 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 391da47ab1..55f31f0716 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -102,7 +102,7 @@ object TestConstants { dustLimit = 1100 sat, maxRemoteDustLimit = 1500 sat, onChainFeeConf = OnChainFeeConf( - feeTargets = FeeTargets(6, 2, 36, 12, 18), + feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, @@ -235,7 +235,7 @@ object TestConstants { dustLimit = 1000 sat, maxRemoteDustLimit = 1500 sat, onChainFeeConf = OnChainFeeConf( - feeTargets = FeeTargets(6, 2, 36, 12, 18), + feeTargets = FeeTargets(6, 2, 36, 12, 18, 0), feeEstimator = new TestFeeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala index 13bccc9b11..138ad1c8e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/FeeEstimatorSpec.scala @@ -27,7 +27,7 @@ class FeeEstimatorSpec extends AnyFunSuite { 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(FeeTargets(1, 1, 1, 1, 1), new TestFeeEstimator(), closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 1, 1, 1, 1, 1), new TestFeeEstimator(), 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))) @@ -38,7 +38,7 @@ class FeeEstimatorSpec extends AnyFunSuite { test("get commitment feerate") { val feeEstimator = new TestFeeEstimator() val channelType = ChannelTypes.Standard - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = FeeratePerKw(5000 sat))) assert(feeConf.getCommitmentFeerate(randomKey().publicKey, channelType, 100000 sat, None) === FeeratePerKw(5000 sat)) @@ -53,7 +53,7 @@ class FeeEstimatorSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(FeeTargets(1, 2, 6, 1, 1, 1), feeEstimator, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(blocks_2 = defaultMaxCommitFeerate / 2, mempoolMinFee = FeeratePerKw(250 sat))) assert(feeConf.getCommitmentFeerate(defaultNodeId, ChannelTypes.AnchorOutputs, 100000 sat, None) === 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 012357033c..0ac2406757 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 @@ -41,7 +41,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging val feeConfNoMismatch = OnChainFeeConf( - FeeTargets(6, 2, 12, 2, 6), + FeeTargets(6, 2, 12, 2, 6, 1), new TestFeeEstimator(), closeOnOfflineMismatch = false, 1.0, 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 e12c5dd326..c5fa39956b 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 @@ -20,6 +20,7 @@ import akka.actor.typed.ActorRef import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter} import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} +import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.{BtcAmount, ByteVector32, MilliBtcDouble, OutPoint, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight @@ -35,7 +36,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher._ import fr.acinq.eclair.channel.states.{ChannelStateTestsHelperMethods, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, TestConstants, TestFeeEstimator, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -66,8 +67,10 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w publisher: ActorRef[ReplaceableTxPublisher.Command], probe: TestProbe) { - def createPublisher(): ActorRef[ReplaceableTxPublisher.Command] = { - system.spawnAnonymous(ReplaceableTxPublisher(alice.underlyingActor.nodeParams, wallet, alice2blockchain.ref, TxPublishContext(UUID.randomUUID(), randomKey().publicKey, None))) + def createPublisher(): ActorRef[ReplaceableTxPublisher.Command] = createPublisher(alice.underlyingActor.nodeParams) + + def createPublisher(nodeParams: NodeParams): ActorRef[ReplaceableTxPublisher.Command] = { + system.spawnAnonymous(ReplaceableTxPublisher(nodeParams, wallet, alice2blockchain.ref, TxPublishContext(UUID.randomUUID(), randomKey().publicKey, None))) } def aliceBlockHeight(): BlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight @@ -1010,6 +1013,28 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } + test("utxos count too low, setting short confirmation target") { + withFixture(Seq(15 millibtc, 10 millibtc, 5 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => + import f._ + + val (commitTx, htlcSuccess, _) = closeChannelWithHtlcs(f, aliceBlockHeight() + 144) + // The HTLC confirmation target is far away, but we have less safe utxos than the configured threshold. + // We will target a 1-block confirmation to get a safe utxo back as soon as possible. + val highSafeThresholdParams = alice.underlyingActor.nodeParams.modify(_.onChainFeeConf.feeTargets.safeUtxosThreshold).setTo(10) + setFeerate(FeeratePerKw(2500 sat)) + val targetFeerate = FeeratePerKw(5000 sat) + setFeerate(targetFeerate, blockTarget = 2) + + val htlcSuccessPublisher = createPublisher(highSafeThresholdParams) + htlcSuccessPublisher ! Publish(probe.ref, htlcSuccess) + val w = alice2blockchain.expectMsgType[WatchParentTxConfirmed] + w.replyTo ! WatchParentTxConfirmedTriggered(currentBlockHeight(probe), 0, commitTx) + val htlcSuccessTx = getMempoolTxs(1).head + val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx.weight.toInt) + assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.4, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") + } + } + test("unlock utxos when htlc tx cannot be published") { withFixture(Seq(500 millibtc, 200 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx) { f => import f._