@@ -20,7 +20,7 @@ import akka.actor.typed.eventstream.EventStream
20
20
import akka .actor .typed .scaladsl .{ActorContext , Behaviors }
21
21
import akka .actor .typed .{ActorRef , Behavior }
22
22
import fr .acinq .bitcoin .psbt .Psbt
23
- import fr .acinq .bitcoin .scalacompat .{ByteVector32 , Satoshi , Script , Transaction , TxOut }
23
+ import fr .acinq .bitcoin .scalacompat .{ByteVector32 , Satoshi , Transaction , TxOut }
24
24
import fr .acinq .eclair .NotificationsLogger .NotifyNodeOperator
25
25
import fr .acinq .eclair .blockchain .bitcoind .rpc .BitcoinCoreClient
26
26
import fr .acinq .eclair .blockchain .fee .{FeeratePerKw , FeeratesPerKw , OnChainFeeConf }
@@ -74,7 +74,7 @@ object ReplaceableTxFunder {
74
74
Behaviors .withMdc(txPublishContext.mdc()) {
75
75
Behaviors .receiveMessagePartial {
76
76
case FundTransaction (replyTo, cmd, tx, requestedFeerate) =>
77
- val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf))
77
+ val targetFeerate = requestedFeerate.min(maxFeerate(cmd.txInfo, cmd.commitment, cmd.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf))
78
78
val txFunder = new ReplaceableTxFunder (nodeParams, replyTo, cmd, bitcoinClient, context)
79
79
tx match {
80
80
case Right (txWithWitnessData) => txFunder.fund(txWithWitnessData, targetFeerate)
@@ -85,17 +85,11 @@ object ReplaceableTxFunder {
85
85
}
86
86
}
87
87
88
- private def commitWeight (commitment : FullCommitment ): Int = {
89
- val unsignedCommitTx = commitment.localCommit.commitTxAndRemoteSig.commitTx
90
- val dummySignedCommitTx = addSigs(unsignedCommitTx, PlaceHolderPubKey , PlaceHolderPubKey , PlaceHolderSig , PlaceHolderSig )
91
- dummySignedCommitTx.tx.weight()
92
- }
93
-
94
88
/**
95
89
* The on-chain feerate can be arbitrarily high, but it wouldn't make sense to pay more fees than the amount we're
96
90
* trying to claim on-chain. We compute how much funds we have at risk and the feerate that matches this amount.
97
91
*/
98
- def maxFeerate (txInfo : ReplaceableTransactionWithInputInfo , commitment : FullCommitment , currentFeerates : FeeratesPerKw , feeConf : OnChainFeeConf ): FeeratePerKw = {
92
+ def maxFeerate (txInfo : ReplaceableTransactionWithInputInfo , commitment : FullCommitment , commitTx : Transaction , currentFeerates : FeeratesPerKw , feeConf : OnChainFeeConf ): FeeratePerKw = {
99
93
// We don't want to pay more in fees than the amount at risk in untrimmed pending HTLCs.
100
94
val maxFee = txInfo match {
101
95
case tx : HtlcTx => tx.input.txOut.amount
@@ -116,7 +110,7 @@ object ReplaceableTxFunder {
116
110
case _ : ClaimHtlcSuccessTx => Transactions .claimHtlcSuccessWeight
117
111
case _ : LegacyClaimHtlcSuccessTx => Transactions .claimHtlcSuccessWeight
118
112
case _ : ClaimHtlcTimeoutTx => Transactions .claimHtlcTimeoutWeight
119
- case _ : ClaimLocalAnchorOutputTx => commitWeight(commitment ) + Transactions .claimAnchorOutputMinWeight
113
+ case _ : ClaimLocalAnchorOutputTx => commitTx.weight( ) + Transactions .claimAnchorOutputMinWeight
120
114
}
121
115
// It doesn't make sense to use a feerate that is much higher than the current feerate for inclusion into the next block.
122
116
Transactions .fee2rate(maxFee, weight).min(currentFeerates.fastest * 1.25 )
@@ -161,12 +155,12 @@ object ReplaceableTxFunder {
161
155
* Adjust the outputs of a transaction that was previously published at a lower feerate.
162
156
* If the current set of inputs doesn't let us to reach the target feerate, we should request new wallet inputs from bitcoind.
163
157
*/
164
- def adjustPreviousTxOutput (previousTx : FundedTx , targetFeerate : FeeratePerKw , commitment : FullCommitment ): AdjustPreviousTxOutputResult = {
158
+ def adjustPreviousTxOutput (previousTx : FundedTx , targetFeerate : FeeratePerKw , commitment : FullCommitment , commitTx : Transaction ): AdjustPreviousTxOutputResult = {
165
159
val dustLimit = commitment.localParams.dustLimit
166
160
val targetFee = previousTx.signedTxWithWitnessData match {
167
161
case _ : ClaimLocalAnchorWithWitnessData =>
168
162
val commitFee = commitment.localCommit.commitTxAndRemoteSig.commitTx.fee
169
- val totalWeight = previousTx.signedTx.weight() + commitWeight(commitment )
163
+ val totalWeight = previousTx.signedTx.weight() + commitTx.weight( )
170
164
weight2fee(targetFeerate, totalWeight) - commitFee
171
165
case _ =>
172
166
weight2fee(targetFeerate, previousTx.signedTx.weight())
@@ -273,7 +267,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
273
267
274
268
private def bump (previousTx : FundedTx , targetFeerate : FeeratePerKw ): Behavior [Command ] = {
275
269
log.info(" bumping {} tx (targetFeerate={})" , previousTx.signedTxWithWitnessData.txInfo.desc, targetFeerate)
276
- adjustPreviousTxOutput(previousTx, targetFeerate, cmd.commitment) match {
270
+ adjustPreviousTxOutput(previousTx, targetFeerate, cmd.commitment, cmd.commitTx ) match {
277
271
case AdjustPreviousTxOutputResult .Skip (reason) =>
278
272
log.warn(" skipping {} fee bumping: {} (feerate={})" , cmd.desc, reason, targetFeerate)
279
273
replyTo ! FundingFailed (TxPublisher .TxRejectedReason .TxSkipped (retryNextBlock = true ))
@@ -392,7 +386,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
392
386
case Right (signedTx) =>
393
387
val actualFees = kmp2scala(processPsbtResponse.psbt.computeFees())
394
388
val actualWeight = locallySignedTx match {
395
- case _ : ClaimLocalAnchorWithWitnessData => signedTx.weight() + commitWeight( cmd.commitment )
389
+ case _ : ClaimLocalAnchorWithWitnessData => signedTx.weight() + cmd.commitTx.weight( )
396
390
case _ => signedTx.weight()
397
391
}
398
392
val actualFeerate = Transactions .fee2rate(actualFees, actualWeight)
@@ -439,30 +433,34 @@ private class ReplaceableTxFunder(nodeParams: NodeParams,
439
433
}
440
434
441
435
private def addInputs (anchorTx : ClaimLocalAnchorWithWitnessData , targetFeerate : FeeratePerKw , commitment : FullCommitment ): Future [(ClaimLocalAnchorWithWitnessData , Satoshi )] = {
442
- val dustLimit = commitment.localParams.dustLimit
443
- // NB: fundrawtransaction requires at least one output, and may add at most one additional change output.
444
- // Since the purpose of this transaction is just to do a CPFP, the resulting tx should have a single change output
445
- // (note that bitcoind doesn't let us publish a transaction with no outputs). To work around these limitations, we
446
- // start with a dummy output and later merge that dummy output with the optional change output added by bitcoind.
447
- val txNotFunded = anchorTx.txInfo.tx.copy(txOut = TxOut (dustLimit, Script .pay2wpkh(PlaceHolderPubKey )) :: Nil )
448
- val anchorWeight = Map (anchorTx.txInfo.input.outPoint -> anchorInputWeight.toLong)
449
- // We only use confirmed inputs for anchor transactions to be able to leverage 1-parent-1-child package relay.
450
- bitcoinClient.fundTransaction(txNotFunded, targetFeerate, externalInputsWeight = anchorWeight, minInputConfirmations_opt = Some (1 )).flatMap { fundTxResponse =>
451
- // Bitcoin Core may not preserve the order of inputs, we need to make sure the anchor is the first input.
452
- val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn.filterNot(_.outPoint == anchorTx.txInfo.input.outPoint)
453
- // We merge our dummy change output with the one added by Bitcoin Core, if any.
454
- fundTxResponse.changePosition match {
455
- case Some (changePos) =>
456
- val changeOutput = fundTxResponse.tx.txOut(changePos).copy(amount = fundTxResponse.tx.txOut.map(_.amount).sum)
457
- val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq (changeOutput))
458
- Future .successful(anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn)
459
- case None =>
460
- bitcoinClient.getChangePublicKeyScript().map(changeScript => {
461
- // We must have a change output, otherwise the transaction is invalid: we replace the PlaceHolderPubKey with a real wallet key.
462
- val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq (TxOut (dustLimit, changeScript)))
463
- (anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn)
464
- })
436
+ // We want to pay the commit fees using CPFP. Since the commit tx may not be in the mempool yet (its feerate may be
437
+ // below the minimum acceptable mempool feerate), we cannot ask bitcoind to fund a transaction that spends that
438
+ // commit tx: it would fail because it cannot find the input in the utxo set. So we instead ask bitcoind to fund an
439
+ // empty transaction that pays the fees we must add to the transaction package, and we then add the input spending
440
+ // the commit tx and adjust the change output.
441
+ val expectedCommitFee = Transactions .weight2fee(targetFeerate, cmd.commitTx.weight())
442
+ val actualCommitFee = commitment.commitInput.txOut.amount - cmd.commitTx.txOut.map(_.amount).sum
443
+ val anchorInputFee = Transactions .weight2fee(targetFeerate, anchorInputWeight)
444
+ val missingFee = expectedCommitFee - actualCommitFee + anchorInputFee
445
+ for {
446
+ changeScript <- bitcoinClient.getChangePublicKeyScript()
447
+ txNotFunded = Transaction (2 , Nil , TxOut (commitment.localParams.dustLimit + missingFee, changeScript) :: Nil , 0 )
448
+ // We only use confirmed inputs for anchor transactions to be able to leverage 1-parent-1-child package relay.
449
+ fundTxResponse <- bitcoinClient.fundTransaction(txNotFunded, targetFeerate, minInputConfirmations_opt = Some (1 ))
450
+ } yield {
451
+ // We merge our dummy change output with the one added by Bitcoin Core, if any, and adjust the change amount to
452
+ // pay the expected package feerate.
453
+ val txIn = anchorTx.txInfo.tx.txIn ++ fundTxResponse.tx.txIn
454
+ val packageWeight = cmd.commitTx.weight() + anchorInputWeight + fundTxResponse.tx.weight()
455
+ val expectedFee = Transactions .weight2fee(targetFeerate, packageWeight)
456
+ val currentFee = actualCommitFee + fundTxResponse.fee
457
+ val changeAmount = (fundTxResponse.tx.txOut.map(_.amount).sum - expectedFee + currentFee).max(commitment.localParams.dustLimit)
458
+ val changeOutput = fundTxResponse.changePosition match {
459
+ case Some (changePos) => fundTxResponse.tx.txOut(changePos).copy(amount = changeAmount)
460
+ case None => TxOut (changeAmount, changeScript)
465
461
}
462
+ val txSingleOutput = fundTxResponse.tx.copy(txIn = txIn, txOut = Seq (changeOutput))
463
+ (anchorTx.updateTx(txSingleOutput), fundTxResponse.amountIn)
466
464
}
467
465
}
468
466
0 commit comments