Skip to content

Commit fdc2077

Browse files
authored
Refactor replaceable transactions (#3075)
We previously included confirmation targets directly in HTLC and anchor transactions, which didn't really belong there. We now instead set confirmation targets at publication time, based on the current state of the force-close attempt. We also extract witness data for HTLCs (preimage and remote signature) when creating the command to publish the transaction, instead of doing it inside the publisher actors. We improve the checks done in the pre-publisher actors, which now take into account the min-depth for commit txs before aborting anchor txs. The `ReplaceableTxFunder` is also simpler now, thanks to a better encapsulation of the transaction's contents. We didn't watch our anchor output on the remote commit, which means we weren't recording in the `AuditDB` the fees we paid to get a remote commit tx confirmed by spending our anchor output. We only had that code for the local commit, but we've recently started more aggressively trying to get the *remote* commitment confirmed, so it's important to correctly record those fees.
1 parent f14b92d commit fdc2077

31 files changed

+962
-1051
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package fr.acinq.eclair.channel
1919
import akka.actor.{ActorRef, PossiblyHarmful, typed}
2020
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
2121
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut}
22-
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
22+
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, OnChainFeeConf}
2323
import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx
2424
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
2525
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
@@ -313,7 +313,7 @@ sealed trait CommitPublished {
313313
def commitTx: Transaction
314314
/** Map of relevant outpoints that have been spent and the confirmed transaction that spends them. */
315315
def irrevocablySpent: Map[OutPoint, Transaction]
316-
316+
/** Returns true if the commitment transaction is confirmed. */
317317
def isConfirmed: Boolean = {
318318
// NB: if multiple transactions end up in the same block, the first confirmation we receive may not be the commit tx.
319319
// However if the confirmed tx spends from the commit tx, we know that the commit tx is already confirmed and we know
@@ -329,11 +329,26 @@ sealed trait CommitPublished {
329329
* @param htlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be
330330
* None only for incoming HTLCs for which we don't have the preimage (we can't claim them yet).
331331
* @param claimHtlcDelayedTxs 3rd-stage txs (spending the output of HTLC txs).
332-
* @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable).
333-
* We currently only claim our local anchor, but it would be nice to claim both when it
334-
* is economical to do so to avoid polluting the utxo set.
332+
* @param claimAnchorTxs txs spending our anchor output to bump the feerate of the commitment tx (if applicable).
335333
*/
336334
case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[ClaimLocalDelayedOutputTx], htlcTxs: Map[OutPoint, Option[HtlcTx]], claimHtlcDelayedTxs: List[HtlcDelayedTx], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished {
335+
// We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates.
336+
val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption
337+
338+
/** Compute the confirmation target that should be used to get the [[commitTx]] confirmed. */
339+
def confirmationTarget(onChainFeeConf: OnChainFeeConf): Option[ConfirmationTarget] = {
340+
if (isConfirmed) {
341+
None
342+
} else {
343+
htlcTxs.values.flatten.map(_.htlcExpiry.blockHeight).minOption match {
344+
// If there are pending HTLCs, we must get the commit tx confirmed before they timeout.
345+
case Some(htlcExpiry) => Some(ConfirmationTarget.Absolute(htlcExpiry))
346+
// Otherwise, we don't have funds at risk, so we can aim for a slower confirmation.
347+
case None => Some(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
348+
}
349+
}
350+
}
351+
337352
/**
338353
* A local commit is considered done when:
339354
* - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours)
@@ -363,11 +378,26 @@ case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx:
363378
* @param claimMainOutputTx tx claiming our main output (if we have one).
364379
* @param claimHtlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be None
365380
* only for incoming HTLCs for which we don't have the preimage (we can't claim them yet).
366-
* @param claimAnchorTxs txs spending anchor outputs to bump the feerate of the commitment tx (if applicable).
367-
* We currently only claim our local anchor, but it would be nice to claim both when it is
368-
* economical to do so to avoid polluting the utxo set.
381+
* @param claimAnchorTxs txs spending our anchor output to bump the feerate of the commitment tx (if applicable).
369382
*/
370383
case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], claimHtlcTxs: Map[OutPoint, Option[ClaimHtlcTx]], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished {
384+
// We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates.
385+
val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption
386+
387+
/** Compute the confirmation target that should be used to get the [[commitTx]] confirmed. */
388+
def confirmationTarget(onChainFeeConf: OnChainFeeConf): Option[ConfirmationTarget] = {
389+
if (isConfirmed) {
390+
None
391+
} else {
392+
claimHtlcTxs.values.flatten.map(_.htlcExpiry.blockHeight).minOption match {
393+
// If there are pending HTLCs, we must get the commit tx confirmed before they timeout.
394+
case Some(htlcExpiry) => Some(ConfirmationTarget.Absolute(htlcExpiry))
395+
// Otherwise, we don't have funds at risk, so we can aim for a slower confirmation.
396+
case None => Some(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
397+
}
398+
}
399+
}
400+
371401
/**
372402
* A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed
373403
* (even if the spending tx was not ours).

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,19 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePer
237237
/** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */
238238
case class NextRemoteCommit(sig: CommitSig, commit: RemoteCommit)
239239

240+
/**
241+
* If we ignore revoked commitments, there can be at most three concurrent commitment transactions during a force-close:
242+
* - the local commitment
243+
* - the remote commitment
244+
* - the next remote commitment, if we sent commit_sig but haven't yet received revoke_and_ack
245+
*/
246+
case class CommitTxIds(localCommitTxId: TxId, remoteCommitTxId: TxId, nextRemoteCommitTxId_opt: Option[TxId]) {
247+
val txIds: Set[TxId] = nextRemoteCommitTxId_opt match {
248+
case Some(nextRemoteCommitTxId) => Set(localCommitTxId, remoteCommitTxId, nextRemoteCommitTxId)
249+
case None => Set(localCommitTxId, remoteCommitTxId)
250+
}
251+
}
252+
240253
/**
241254
* A minimal commitment for a given funding tx.
242255
*
@@ -255,6 +268,7 @@ case class Commitment(fundingTxIndex: Long,
255268
localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) {
256269
val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input
257270
val fundingTxId: TxId = commitInput.outPoint.txid
271+
val commitTxIds: CommitTxIds = CommitTxIds(localCommit.commitTxAndRemoteSig.commitTx.tx.txid, remoteCommit.txid, nextRemoteCommit_opt.map(_.commit.txid))
258272
val capacity: Satoshi = commitInput.txOut.amount
259273
/** Once the funding transaction is confirmed, short_channel_id matching this transaction. */
260274
val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match {
@@ -735,6 +749,7 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges,
735749
val remoteParams: RemoteParams = params.remoteParams
736750
val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input
737751
val fundingTxId: TxId = commitInput.outPoint.txid
752+
val commitTxIds: CommitTxIds = CommitTxIds(localCommit.commitTxAndRemoteSig.commitTx.tx.txid, remoteCommit.txid, nextRemoteCommit_opt.map(_.commit.txid))
738753
val capacity: Satoshi = commitInput.txOut.amount
739754
val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, nextRemoteCommit_opt)
740755

eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -850,25 +850,6 @@ object Helpers {
850850
if (localPaysCommitTxFees) commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum else 0 sat
851851
}
852852

853-
/**
854-
* This function checks if the proposed confirmation target is more aggressive than whatever confirmation target
855-
* we previously had. Note that absolute targets are always considered more aggressive than relative targets.
856-
*/
857-
private def shouldUpdateAnchorTxs(anchorTxs: List[ClaimAnchorOutputTx], confirmationTarget: ConfirmationTarget): Boolean = {
858-
anchorTxs
859-
.collect { case tx: ClaimAnchorOutputTx => tx.confirmationTarget }
860-
.forall {
861-
case ConfirmationTarget.Absolute(current) => confirmationTarget match {
862-
case ConfirmationTarget.Absolute(proposed) => proposed < current
863-
case _: ConfirmationTarget.Priority => false
864-
}
865-
case ConfirmationTarget.Priority(current) => confirmationTarget match {
866-
case _: ConfirmationTarget.Absolute => true
867-
case ConfirmationTarget.Priority(proposed) => current < proposed
868-
}
869-
}
870-
}
871-
872853
object LocalClose {
873854

874855
/**
@@ -900,23 +881,17 @@ object Helpers {
900881
)
901882
val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
902883
if (spendAnchors) {
903-
// If we don't have pending HTLCs, we don't have funds at risk, so we can aim for a slower confirmation.
904-
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => ConfirmationTarget.Absolute(htlcTx.htlcExpiry.blockHeight)).minByOption(_.confirmBefore).getOrElse(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
905-
claimAnchors(fundingKey, commitmentKeys, lcp, confirmCommitBefore, commitment.params.commitmentFormat)
884+
claimAnchors(fundingKey, commitmentKeys, lcp, commitment.params.commitmentFormat)
906885
} else {
907886
lcp
908887
}
909888
}
910889

911-
def claimAnchors(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, lcp: LocalCommitPublished, confirmationTarget: ConfirmationTarget, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): LocalCommitPublished = {
912-
if (shouldUpdateAnchorTxs(lcp.claimAnchorTxs, confirmationTarget)) {
913-
val claimAnchorTx = withTxGenerationLog("local-anchor") {
914-
ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, lcp.commitTx, confirmationTarget, commitmentFormat)
915-
}
916-
lcp.copy(claimAnchorTxs = claimAnchorTx.toList)
917-
} else {
918-
lcp
890+
def claimAnchors(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, lcp: LocalCommitPublished, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): LocalCommitPublished = {
891+
val claimAnchorTx = withTxGenerationLog("local-anchor") {
892+
ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, lcp.commitTx, commitmentFormat)
919893
}
894+
lcp.copy(claimAnchorTxs = claimAnchorTx.toList)
920895
}
921896

922897
/**
@@ -1015,23 +990,17 @@ object Helpers {
1015990
)
1016991
val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs
1017992
if (spendAnchors) {
1018-
// If we don't have pending HTLCs, we don't have funds at risk, so we use the normal closing priority.
1019-
val confirmCommitBefore = htlcTxs.values.flatten.map(htlcTx => ConfirmationTarget.Absolute(htlcTx.htlcExpiry.blockHeight)).minByOption(_.confirmBefore).getOrElse(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
1020-
claimAnchors(fundingKey, commitKeys, rcp, confirmCommitBefore, commitment.params.commitmentFormat)
993+
claimAnchors(fundingKey, commitKeys, rcp, commitment.params.commitmentFormat)
1021994
} else {
1022995
rcp
1023996
}
1024997
}
1025998

1026-
def claimAnchors(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, rcp: RemoteCommitPublished, confirmationTarget: ConfirmationTarget, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): RemoteCommitPublished = {
1027-
if (shouldUpdateAnchorTxs(rcp.claimAnchorTxs, confirmationTarget)) {
1028-
val claimAnchorTx = withTxGenerationLog("remote-anchor") {
1029-
ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, rcp.commitTx, confirmationTarget, commitmentFormat)
1030-
}
1031-
rcp.copy(claimAnchorTxs = claimAnchorTx.toList)
1032-
} else {
1033-
rcp
999+
def claimAnchors(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, rcp: RemoteCommitPublished, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): RemoteCommitPublished = {
1000+
val claimAnchorTx = withTxGenerationLog("remote-anchor") {
1001+
ClaimAnchorOutputTx.createUnsignedTx(fundingKey, commitKeys.publicKeys, rcp.commitTx, commitmentFormat)
10341002
}
1003+
rcp.copy(claimAnchorTxs = claimAnchorTx.toList)
10351004
}
10361005

10371006
/**

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags}
3838
import fr.acinq.eclair.channel._
3939
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
4040
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxFunder, InteractiveTxSigningSession}
41-
import fr.acinq.eclair.channel.publish.TxPublisher
4241
import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId}
42+
import fr.acinq.eclair.channel.publish.{ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor, TxPublisher}
4343
import fr.acinq.eclair.crypto.keymanager.ChannelKeys
4444
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType
4545
import fr.acinq.eclair.db.PendingCommandsDb
@@ -2168,27 +2168,36 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
21682168
case commitmentFormat: Transactions.AnchorOutputsCommitmentFormat =>
21692169
val commitment = d.commitments.latest
21702170
val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
2171-
val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.claimAnchors(fundingKey, commitment.localKeys(channelKeys), lcp, c.confirmationTarget, commitmentFormat))
2172-
val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.claimAnchors(fundingKey, commitment.remoteKeys(channelKeys, commitment.remoteCommit.remotePerCommitmentPoint), rcp, c.confirmationTarget, commitmentFormat))
2173-
val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.claimAnchors(fundingKey, commitment.remoteKeys(channelKeys, commitment.nextRemoteCommit_opt.get.commit.remotePerCommitmentPoint), nrcp, c.confirmationTarget, commitmentFormat))
2171+
val localAnchor_opt = for {
2172+
lcp <- d.localCommitPublished
2173+
commitKeys = commitment.localKeys(channelKeys)
2174+
anchorTx <- Closing.LocalClose.claimAnchors(fundingKey, commitKeys, lcp, commitmentFormat).claimAnchorTx_opt
2175+
} yield PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, lcp.commitTx, commitment), c.confirmationTarget)
2176+
val remoteAnchor_opt = for {
2177+
rcp <- d.remoteCommitPublished
2178+
commitKeys = commitment.remoteKeys(channelKeys, commitment.remoteCommit.remotePerCommitmentPoint)
2179+
anchorTx <- Closing.RemoteClose.claimAnchors(fundingKey, commitKeys, rcp, commitmentFormat).claimAnchorTx_opt
2180+
} yield PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, rcp.commitTx, commitment), c.confirmationTarget)
2181+
val nextRemoteAnchor_opt = for {
2182+
nrcp <- d.nextRemoteCommitPublished
2183+
commitKeys = commitment.remoteKeys(channelKeys, commitment.nextRemoteCommit_opt.get.commit.remotePerCommitmentPoint)
2184+
anchorTx <- Closing.RemoteClose.claimAnchors(fundingKey, commitKeys, nrcp, commitmentFormat).claimAnchorTx_opt
2185+
} yield PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, nrcp.commitTx, commitment), c.confirmationTarget)
21742186
// We favor the remote commitment(s) because they're more interesting than the local commitment (no CSV delays).
2175-
if (rcp1.nonEmpty) {
2176-
rcp1.foreach(rcp => rcp.claimAnchorTxs.foreach { tx => txPublisher ! PublishReplaceableTx(tx, channelKeys, d.commitments.latest, rcp.commitTx, tx.confirmationTarget) })
2187+
if (remoteAnchor_opt.nonEmpty) {
2188+
remoteAnchor_opt.foreach { publishTx => txPublisher ! publishTx }
21772189
c.replyTo ! RES_SUCCESS(c, d.channelId)
2178-
stay() using d.copy(remoteCommitPublished = rcp1) storing()
2179-
} else if (nrcp1.nonEmpty) {
2180-
nrcp1.foreach(rcp => rcp.claimAnchorTxs.foreach { tx => txPublisher ! PublishReplaceableTx(tx, channelKeys, d.commitments.latest, rcp.commitTx, tx.confirmationTarget) })
2190+
} else if (nextRemoteAnchor_opt.nonEmpty) {
2191+
nextRemoteAnchor_opt.foreach { publishTx => txPublisher ! publishTx }
21812192
c.replyTo ! RES_SUCCESS(c, d.channelId)
2182-
stay() using d.copy(nextRemoteCommitPublished = nrcp1) storing()
2183-
} else if (lcp1.nonEmpty) {
2184-
lcp1.foreach(lcp => lcp.claimAnchorTxs.foreach { tx => txPublisher ! PublishReplaceableTx(tx, channelKeys, d.commitments.latest, lcp.commitTx, tx.confirmationTarget) })
2193+
} else if (localAnchor_opt.nonEmpty) {
2194+
localAnchor_opt.foreach { publishTx => txPublisher ! publishTx }
21852195
c.replyTo ! RES_SUCCESS(c, d.channelId)
2186-
stay() using d.copy(localCommitPublished = lcp1) storing()
21872196
} else {
21882197
log.warning("cannot bump force-close fees, local or remote commit not published")
21892198
c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName))
2190-
stay()
21912199
}
2200+
stay()
21922201
case _ =>
21932202
log.warning("cannot bump force-close fees, channel is not using anchor outputs")
21942203
c.replyTo ! RES_FAILURE(c, CommandUnavailableInThisState(d.channelId, "rbf-force-close", stateName))

0 commit comments

Comments
 (0)