Skip to content

Commit

Permalink
Use bumpforceclose RPC to also bump remote commit fees (#2744)
Browse files Browse the repository at this point in the history
We want to be able to bump a force-close regardless of which commitment
was published. If the remote commitment is in our mempool, we will use
our anchor on that commit tx to bump the fees.
  • Loading branch information
t-bast committed Sep 22, 2023
1 parent 55f9698 commit d4c502a
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,30 @@ import fr.acinq.eclair.channel.{ChannelTypes, SupportedChannelType}
import fr.acinq.eclair.transactions.Transactions

// @formatter:off
sealed trait ConfirmationPriority {
sealed trait ConfirmationPriority extends Ordered[ConfirmationPriority] {
def underlying: Int

def getFeerate(feerates: FeeratesPerKw): FeeratePerKw = this match {
case ConfirmationPriority.Slow => feerates.slow
case ConfirmationPriority.Medium => feerates.medium
case ConfirmationPriority.Fast => feerates.fast
}

override def compare(that: ConfirmationPriority): Int = this.underlying.compare(that.underlying)
}
object ConfirmationPriority {
case object Slow extends ConfirmationPriority { override def toString = "slow" }
case object Medium extends ConfirmationPriority { override def toString = "medium" }
case object Fast extends ConfirmationPriority { override def toString = "fast" }
case object Slow extends ConfirmationPriority {
override val underlying = 1
override def toString = "slow"
}
case object Medium extends ConfirmationPriority {
override val underlying = 2
override def toString = "medium"
}
case object Fast extends ConfirmationPriority {
override val underlying = 3
override def toString = "fast"
}
}
sealed trait ConfirmationTarget
object ConfirmationTarget {
Expand Down
77 changes: 54 additions & 23 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,25 @@ object Helpers {
if (isInitiator) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat
}

/**
* This function checks if the proposed confirmation target is more aggressive than whatever confirmation target
* we previously had. Note that absolute targets are always considered more aggressive than relative targets.
*/
private def shouldUpdateAnchorTxs(anchorTxs: List[ClaimAnchorOutputTx], confirmationTarget: ConfirmationTarget): Boolean = {
anchorTxs
.collect { case tx: ClaimLocalAnchorOutputTx => tx.confirmationTarget }
.forall {
case ConfirmationTarget.Absolute(current) => confirmationTarget match {
case ConfirmationTarget.Absolute(proposed) => proposed < current
case _: ConfirmationTarget.Priority => false
}
case ConfirmationTarget.Priority(current) => confirmationTarget match {
case _: ConfirmationTarget.Absolute => true
case ConfirmationTarget.Priority(proposed) => current < proposed
}
}
}

object LocalClose {

/**
Expand Down Expand Up @@ -763,16 +782,20 @@ object Helpers {
}

def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, lcp: LocalCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): LocalCommitPublished = {
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
val claimAnchorTxs = List(
withTxGenerationLog("local-anchor") {
Transactions.makeClaimLocalAnchorOutputTx(lcp.commitTx, localFundingPubKey, confirmationTarget)
},
withTxGenerationLog("remote-anchor") {
Transactions.makeClaimRemoteAnchorOutputTx(lcp.commitTx, commitment.remoteFundingPubKey)
}
).flatten
lcp.copy(claimAnchorTxs = claimAnchorTxs)
if (shouldUpdateAnchorTxs(lcp.claimAnchorTxs, confirmationTarget)) {
val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
val claimAnchorTxs = List(
withTxGenerationLog("local-anchor") {
Transactions.makeClaimLocalAnchorOutputTx(lcp.commitTx, localFundingPubKey, confirmationTarget)
},
withTxGenerationLog("remote-anchor") {
Transactions.makeClaimRemoteAnchorOutputTx(lcp.commitTx, commitment.remoteFundingPubKey)
}
).flatten
lcp.copy(claimAnchorTxs = claimAnchorTxs)
} else {
lcp
}
}

/**
Expand Down Expand Up @@ -862,30 +885,38 @@ object Helpers {

val htlcTxs: Map[OutPoint, Option[ClaimHtlcTx]] = claimHtlcOutputs(keyManager, commitment, remoteCommit, feerates, finalScriptPubKey)

val rcp = RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = claimMainOutput(keyManager, commitment.params, remoteCommit.remotePerCommitmentPoint, tx, feerates, onChainFeeConf, finalScriptPubKey),
claimHtlcTxs = htlcTxs,
claimAnchorTxs = Nil,
irrevocablySpent = Map.empty
)
val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
val claimAnchorTxs: List[ClaimAnchorOutputTx] = if (spendAnchors) {
if (spendAnchors) {
// If we don't have pending HTLCs, we don't have funds at risk, so we use the normal closing priority.
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => htlcTx.confirmationTarget).minByOption(_.confirmBefore).getOrElse(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
claimAnchors(keyManager, commitment, rcp, confirmCommitBefore)
} else {
rcp
}
}

def claimAnchors(keyManager: ChannelKeyManager, commitment: FullCommitment, rcp: RemoteCommitPublished, confirmationTarget: ConfirmationTarget)(implicit log: LoggingAdapter): RemoteCommitPublished = {
if (shouldUpdateAnchorTxs(rcp.claimAnchorTxs, confirmationTarget)) {
val localFundingPubkey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex).publicKey
List(
val claimAnchorTxs = List(
withTxGenerationLog("local-anchor") {
Transactions.makeClaimLocalAnchorOutputTx(tx, localFundingPubkey, confirmCommitBefore)
Transactions.makeClaimLocalAnchorOutputTx(rcp.commitTx, localFundingPubkey, confirmationTarget)
},
withTxGenerationLog("remote-anchor") {
Transactions.makeClaimRemoteAnchorOutputTx(tx, commitment.remoteFundingPubKey)
Transactions.makeClaimRemoteAnchorOutputTx(rcp.commitTx, commitment.remoteFundingPubKey)
}
).flatten
rcp.copy(claimAnchorTxs = claimAnchorTxs)
} else {
Nil
rcp
}

RemoteCommitPublished(
commitTx = tx,
claimMainOutputTx = claimMainOutput(keyManager, commitment.params, remoteCommit.remotePerCommitmentPoint, tx, feerates, onChainFeeConf, finalScriptPubKey),
claimHtlcTxs = htlcTxs,
claimAnchorTxs = claimAnchorTxs,
irrevocablySpent = Map.empty
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1707,20 +1707,23 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case Event(c: CMD_CLOSE, d: DATA_CLOSING) => handleCommandError(ClosingAlreadyInProgress(d.channelId), c)

case Event(c: CMD_BUMP_FORCE_CLOSE_FEE, d: DATA_CLOSING) =>
d.localCommitPublished match {
case Some(lcp) => d.commitments.params.commitmentFormat match {
case _: Transactions.AnchorOutputsCommitmentFormat =>
val lcp1 = Closing.LocalClose.claimAnchors(keyManager, d.commitments.latest, lcp, c.confirmationTarget)
lcp1.claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => txPublisher ! PublishReplaceableTx(tx, d.commitments.latest) }
d.commitments.params.commitmentFormat match {
case _: Transactions.AnchorOutputsCommitmentFormat =>
val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.claimAnchors(keyManager, d.commitments.latest, lcp, c.confirmationTarget))
val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, rcp, c.confirmationTarget))
val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.claimAnchors(keyManager, d.commitments.latest, nrcp, c.confirmationTarget))
val claimAnchorTxs = lcp1.toSeq.flatMap(_.claimAnchorTxs) ++ rcp1.toSeq.flatMap(_.claimAnchorTxs) ++ nrcp1.toSeq.flatMap(_.claimAnchorTxs)
claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => txPublisher ! PublishReplaceableTx(tx, d.commitments.latest) }
if (claimAnchorTxs.nonEmpty) {
c.replyTo ! RES_SUCCESS(c, d.channelId)
stay() using d.copy(localCommitPublished = Some(lcp1))
case Transactions.DefaultCommitmentFormat =>
log.warning("cannot bump force-close fees, channel is not using anchor outputs")
stay() using d.copy(localCommitPublished = lcp1, remoteCommitPublished = rcp1, nextRemoteCommitPublished = nrcp1) storing()
} else {
log.warning("cannot bump force-close fees, local or remote commit not published")
c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName))
stay()
}
case None =>
log.warning("cannot bump force-close fees, local commit hasn't been published")
}
case _ =>
log.warning("cannot bump force-close fees, channel is not using anchor outputs")
c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName))
stay()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,9 @@ trait ErrorHandlers extends CommonHandlers {
def doPublish(remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment): Unit = {
import remoteCommitPublished._

val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitment) }
val redeemableHtlcTxs = claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitment))
val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs
val publishQueue = claimLocalAnchor ++ claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ redeemableHtlcTxs
publishIfNeeded(publishQueue, irrevocablySpent)

// we watch:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,25 @@ private class ReplaceableTxPrePublisher(nodeParams: NodeParams,
// We verify that:
// - our commit is not confirmed (if it is, no need to claim our anchor)
// - their commit is not confirmed (if it is, no need to claim our anchor either)
// - our commit tx is in the mempool (otherwise we can't claim our anchor)
val commitTx = cmd.commitment.fullySignedLocalCommitTx(nodeParams.channelKeyManager).tx
// - the local or remote commit tx is in the mempool (otherwise we can't claim our anchor)
val fundingOutpoint = cmd.commitment.commitInput.outPoint
context.pipeToSelf(bitcoinClient.getTxConfirmations(fundingOutpoint.txid).flatMap {
case Some(_) =>
// The funding transaction was found, let's see if we can still spend it.
bitcoinClient.isTransactionOutputSpendable(fundingOutpoint.txid, fundingOutpoint.index.toInt, includeMempool = false).flatMap {
case false => Future.failed(CommitTxAlreadyConfirmed)
case true =>
// We must ensure our local commit tx is in the mempool before publishing the anchor transaction.
// If it's already published, this call will be a no-op.
bitcoinClient.publishTransaction(commitTx)
val remoteCommits = Set(Some(cmd.commitment.remoteCommit.txid), cmd.commitment.nextRemoteCommit_opt.map(_.commit.txid)).flatten
if (remoteCommits.contains(localAnchorTx.input.outPoint.txid)) {
// We're trying to bump the remote commit tx: we must make sure it is in our mempool first.
// If it isn't, we will publish our local commit tx instead.
bitcoinClient.getMempoolTx(localAnchorTx.input.outPoint.txid).map(_.txid)
} else {
// We must ensure our local commit tx is in the mempool before publishing the anchor transaction.
// If it's already published, this call will be a no-op.
val commitTx = cmd.commitment.fullySignedLocalCommitTx(nodeParams.channelKeyManager).tx
bitcoinClient.publishTransaction(commitTx)
}
}
case None =>
// If the funding transaction cannot be found (e.g. when using 0-conf), we should retry later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact
case (_: ConfirmationTarget.Priority, ConfirmationTarget.Absolute(_)) =>
// Switch from relative priority mode to absolute blockheight mode
updateConfirmationTarget()
case (ConfirmationTarget.Priority(current), ConfirmationTarget.Priority(proposed)) if current < proposed =>
// Switch to a higher relative priority.
updateConfirmationTarget()
case _ =>
log.debug("not publishing replaceable {} spending {}:{} with confirmation target={}, publishing is already in progress with confirmation target={}", cmd.desc, cmd.input.txid, cmd.input.index, proposedConfirmationTarget, currentConfirmationTarget)
Behaviors.same
Expand Down
Loading

0 comments on commit d4c502a

Please sign in to comment.