From 835b33b2b8ab6e39ff87fff652b880b19a5a2c8f Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Thu, 25 May 2023 14:53:26 +0200 Subject: [PATCH] Add upper bound on fees paid during force-close (#2668) The on-chain feerate can be arbitrarily high, but it wouldn't make sense to pay more fees than the amount we have at risk and need to claim on-chain. We compute an upper bound on the fees we'll pay and make sure we don't exceed it, even when trying to RBF htlc transactions that get close to their deadline. --- .../channel/publish/ReplaceableTxFunder.scala | 37 ++++++++++- .../publish/ReplaceableTxPublisherSpec.scala | 64 +++++++++++++++++-- .../transactions/TransactionsSpec.scala | 14 ++-- 3 files changed, 101 insertions(+), 14 deletions(-) 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 3a331df8b6..37e1ee24d8 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 @@ -27,6 +27,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.FullCommitment import fr.acinq.eclair.channel.publish.ReplaceableTxPrePublisher._ import fr.acinq.eclair.channel.publish.TxPublisher.TxPublishContext +import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{NodeParams, NotificationsLogger} @@ -72,7 +73,8 @@ object ReplaceableTxFunder { Behaviors.setup { context => Behaviors.withMdc(txPublishContext.mdc()) { Behaviors.receiveMessagePartial { - case FundTransaction(replyTo, cmd, tx, targetFeerate) => + case FundTransaction(replyTo, cmd, tx, requestedFeerate) => + val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment)) val txFunder = new ReplaceableTxFunder(nodeParams, replyTo, cmd, bitcoinClient, context) tx match { case Right(txWithWitnessData) => txFunder.fund(txWithWitnessData, targetFeerate) @@ -88,6 +90,35 @@ object ReplaceableTxFunder { addSigs(unsignedCommitTx, PlaceHolderPubKey, PlaceHolderPubKey, PlaceHolderSig, PlaceHolderSig) } + /** + * 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 = { + // 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 + case tx: ClaimHtlcTx => tx.input.txOut.amount + 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) + } + // We cannot know beforehand how many wallet inputs will be added, but an estimation should be good enough. + val weight = txInfo match { + // For HTLC transactions, we add a p2wpkh input and a p2wpkh change output. + case _: HtlcSuccessTx => commitment.params.commitmentFormat.htlcSuccessWeight + Transactions.claimP2WPKHOutputWeight + case _: HtlcTimeoutTx => commitment.params.commitmentFormat.htlcTimeoutWeight + Transactions.claimP2WPKHOutputWeight + case _: ClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight + case _: LegacyClaimHtlcSuccessTx => Transactions.claimHtlcSuccessWeight + case _: ClaimHtlcTimeoutTx => Transactions.claimHtlcTimeoutWeight + case _: ClaimLocalAnchorOutputTx => dummySignedCommitTx(commitment).tx.weight() + Transactions.claimAnchorOutputMinWeight + } + Transactions.fee2rate(maxFee, weight) + } + /** * Adjust the main output of a claim-htlc tx to match our target feerate. * If the resulting output is too small, we skip the transaction. @@ -203,6 +234,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, private val log = context.log def fund(txWithWitnessData: ReplaceableTxWithWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = { + log.info("funding {} tx (targetFeerate={})", txWithWitnessData.txInfo.desc, targetFeerate) txWithWitnessData match { case claimLocalAnchor: ClaimLocalAnchorWithWitnessData => val commitFeerate = cmd.commitment.localCommit.spec.commitTxFeerate @@ -237,6 +269,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } private def bump(previousTx: FundedTx, targetFeerate: FeeratePerKw): Behavior[Command] = { + log.info("bumping {} tx (targetFeerate={})", previousTx.signedTxWithWitnessData.txInfo.desc, targetFeerate) adjustPreviousTxOutput(previousTx, targetFeerate, cmd.commitment) match { case AdjustPreviousTxOutputResult.Skip(reason) => log.warn("skipping {} fee bumping: {} (feerate={})", cmd.desc, reason, targetFeerate) @@ -260,7 +293,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } Behaviors.receiveMessagePartial { case AddInputsOk(fundedTx, totalAmountIn) => - log.info("added {} wallet input(s) and {} wallet output(s) to {}", fundedTx.txInfo.tx.txIn.length - 1, fundedTx.txInfo.tx.txOut.length - 1, cmd.desc) + log.debug("added {} wallet input(s) and {} wallet output(s) to {}", fundedTx.txInfo.tx.txIn.length - 1, fundedTx.txInfo.tx.txOut.length - 1, cmd.desc) sign(fundedTx, targetFeerate, totalAmountIn) case AddInputsFailed(reason) => if (reason.getMessage.contains("Insufficient funds")) { 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 6891c77c56..8bf3643632 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 @@ -39,7 +39,7 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck} -import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestFeeEstimator, TestKitBaseClass, randomKey} +import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestFeeEstimator, TestKitBaseClass, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike @@ -380,6 +380,37 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } + test("commit tx feerate too low, spending anchor output (feerate upper bound reached)") { + 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 maxFeerate = ReplaceableTxFunder.maxFeerate(anchorTx.txInfo, anchorTx.commitment) + val targetFeerate = FeeratePerKw(50_000 sat) + assert(maxFeerate <= targetFeerate / 2) + 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)) + + val targetFee = Transactions.weight2fee(maxFeerate, 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") + + generateBlocks(5) + system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) + val result = probe.expectMsgType[TxConfirmed] + assert(result.cmd == anchorTx) + assert(result.tx.txIn.map(_.outPoint.txid).contains(commitTx.tx.txid)) + assert(mempoolTxs.map(_.txid).contains(result.tx.txid)) + } + } + test("commit tx not published, publishing it and spending anchor output") { withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ @@ -517,7 +548,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w system.eventStream.subscribe(listener.ref, classOf[TransactionPublished]) // The feerate is (much) higher for higher block targets - val targetFeerate = FeeratePerKw(75_000 sat) + val targetFeerate = FeeratePerKw(20_000 sat) setFeerate(FeeratePerKw(3000 sat)) setFeerate(targetFeerate, blockTarget = 6) publisher ! Publish(probe.ref, anchorTx) @@ -801,13 +832,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } - def closeChannelWithHtlcs(f: Fixture, overrideHtlcTarget: BlockHeight): (Transaction, PublishReplaceableTx, PublishReplaceableTx) = { + def closeChannelWithHtlcs(f: Fixture, overrideHtlcTarget: BlockHeight, outgoingHtlcAmount: MilliSatoshi = 120_000_000 msat, incomingHtlcAmount: MilliSatoshi = 100_000_000 msat): (Transaction, PublishReplaceableTx, PublishReplaceableTx) = { import f._ // Add htlcs in both directions and ensure that preimages are available. - addHtlc(5_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(outgoingHtlcAmount, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val (r, htlc) = addHtlc(4_000_000 msat, bob, alice, bob2alice, alice2bob) + val (r, htlc) = addHtlc(incomingHtlcAmount, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) probe.send(alice, CMD_FULFILL_HTLC(htlc.id, r, replyTo_opt = Some(probe.ref))) probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] @@ -871,6 +902,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcSuccessTx = getMempoolTxs(1).head val htlcSuccessTargetFee = Transactions.weight2fee(targetFeerate, htlcSuccessTx.weight.toInt) assert(htlcSuccessTargetFee * 0.9 <= htlcSuccessTx.fees && htlcSuccessTx.fees <= htlcSuccessTargetFee * 1.2, s"actualFee=${htlcSuccessTx.fees} targetFee=$htlcSuccessTargetFee") + assert(htlcSuccessTx.fees <= htlcSuccess.txInfo.input.txOut.amount) generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -900,6 +932,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val htlcTimeoutTx = getMempoolTxs(1).head val htlcTimeoutTargetFee = Transactions.weight2fee(targetFeerate, htlcTimeoutTx.weight.toInt) assert(htlcTimeoutTargetFee * 0.9 <= htlcTimeoutTx.fees && htlcTimeoutTx.fees <= htlcTimeoutTargetFee * 1.2, s"actualFee=${htlcTimeoutTx.fees} targetFee=$htlcTimeoutTargetFee") + assert(htlcTimeoutTx.fees <= htlcTimeout.txInfo.input.txOut.amount) generateBlocks(4) system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe))) @@ -975,6 +1008,27 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } } + test("htlc tx feerate zero, adding wallet inputs (feerate upper bound reached)") { + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + import f._ + + val targetFeerate = FeeratePerKw(15_000 sat) + // HTLC amount is small, so we should cap the feerate to avoid paying more in fees than what we're claiming. + 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) + 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) + assert(htlcTimeoutMaxFeerate < targetFeerate / 2) + val htlcTimeoutTx = testPublishHtlcTimeout(f, commitTx, htlcTimeout, htlcTimeoutMaxFeerate) + assert(htlcTimeoutTx.txIn.length > 1) + } + } + test("htlc tx feerate too low, adding multiple wallet inputs") { val utxos = Seq( // channel funding diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 7380aa0f01..3d77c73bd6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -120,7 +120,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // ClaimP2WPKHOutputTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimP2WPKHOutputTx val pubKeyScript = write(pay2wpkh(localPaymentPriv.publicKey)) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx, localDustLimit, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimP2WPKHOutputTx, localPaymentPriv.publicKey, PlaceHolderSig).tx) @@ -131,7 +131,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // HtlcDelayedTx // first we create a fake htlcSuccessOrTimeoutTx tx, containing only the output that will be spent by the 3rd-stage tx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val htlcSuccessOrTimeoutTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) + val htlcSuccessOrTimeoutTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val Right(htlcDelayedTx) = makeHtlcDelayedTx(htlcSuccessOrTimeoutTx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(htlcDelayedTx, PlaceHolderSig).tx) @@ -142,7 +142,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // MainPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the MainPenaltyTx val pubKeyScript = write(pay2wsh(toLocalDelayed(localRevocationPriv.publicKey, toLocalDelay, localPaymentPriv.publicKey))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(20000 sat, pubKeyScript) :: Nil, lockTime = 0) val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localPaymentPriv.publicKey, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(mainPenaltyTx, PlaceHolderSig).tx) @@ -156,7 +156,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000) msat, sha256(paymentPreimage), CltvExpiryDelta(144).toCltvExpiry(blockHeight), TestConstants.emptyOnionPacket, None) val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(redeemScript)) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx, 0, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(htlcPenaltyTx, PlaceHolderSig, localRevocationPriv.publicKey).tx) @@ -171,7 +171,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val spec = CommitmentSpec(Set(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), DefaultCommitmentFormat))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimHtlcSuccessTx, PlaceHolderSig, paymentPreimage).tx) @@ -186,7 +186,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val spec = CommitmentSpec(Set(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0 msat, toRemote = 0 msat) val outputs = makeCommitTxOutputs(localIsInitiator = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry, DefaultCommitmentFormat))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0) val Right(claimClaimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) // we use dummy signatures to compute the weight val weight = Transaction.weight(addSigs(claimClaimHtlcTimeoutTx, PlaceHolderSig).tx) @@ -197,7 +197,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // ClaimAnchorOutputTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimAnchorOutputTx val pubKeyScript = write(pay2wsh(anchor(localFundingPriv.publicKey))) - val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(anchorAmount, pubKeyScript) :: Nil, lockTime = 0) + val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(anchorAmount, pubKeyScript) :: Nil, lockTime = 0) val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx, localFundingPriv.publicKey, BlockHeight(1105)) assert(claimAnchorOutputTx.tx.txOut.isEmpty) assert(claimAnchorOutputTx.confirmBefore == BlockHeight(1105))