Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel
import akka.actor.{ActorRef, PossiblyHarmful, typed}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut}
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw, OnChainFeeConf}
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession}
Expand Down Expand Up @@ -335,20 +335,6 @@ case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx:
// We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates.
val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption

/** Compute the confirmation target that should be used to get the [[commitTx]] confirmed. */
def confirmationTarget(onChainFeeConf: OnChainFeeConf): Option[ConfirmationTarget] = {
if (isConfirmed) {
None
} else {
htlcTxs.values.flatten.map(_.htlcExpiry.blockHeight).minOption match {
// If there are pending HTLCs, we must get the commit tx confirmed before they timeout.
case Some(htlcExpiry) => Some(ConfirmationTarget.Absolute(htlcExpiry))
// Otherwise, we don't have funds at risk, so we can aim for a slower confirmation.
case None => Some(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
}
}
}

/**
* A local commit is considered done when:
* - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours)
Expand Down Expand Up @@ -384,20 +370,6 @@ case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Optio
// We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates.
val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption

/** Compute the confirmation target that should be used to get the [[commitTx]] confirmed. */
def confirmationTarget(onChainFeeConf: OnChainFeeConf): Option[ConfirmationTarget] = {
if (isConfirmed) {
None
} else {
claimHtlcTxs.values.flatten.map(_.htlcExpiry.blockHeight).minOption match {
// If there are pending HTLCs, we must get the commit tx confirmed before they timeout.
case Some(htlcExpiry) => Some(ConfirmationTarget.Absolute(htlcExpiry))
// Otherwise, we don't have funds at risk, so we can aim for a slower confirmation.
case None => Some(ConfirmationTarget.Priority(onChainFeeConf.feeTargets.closing))
}
}
}

/**
* A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed
* (even if the spending tx was not ours).
Expand Down
202 changes: 103 additions & 99 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2030,34 +2030,25 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}

case Event(WatchOutputSpentTriggered(_, tx), d: DATA_CLOSING) =>
// one of the outputs of the local/remote/revoked commit was spent
// we just put a watch to be notified when it is confirmed
// One of the outputs of the local/remote/revoked commit transaction or of an HTLC transaction was spent.
// We put a watch to be notified when the transaction confirms: it may double-spend one of our transactions.
blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth)
// when a remote or local commitment tx containing outgoing htlcs is published on the network,
// we watch it in order to extract payment preimage if funds are pulled by the counterparty
// we can then use these preimages to fulfill origin htlcs
log.debug(s"processing bitcoin output spent by txid={} tx={}", tx.txid, tx)
// If this is an HTLC transaction, it may reveal preimages that we haven't received yet.
// If we successfully extract those preimages, we can forward them upstream.
log.debug("processing bitcoin output spent by txid={} tx={}", tx.txid, tx)
val extracted = Closing.extractPreimages(d.commitments.latest, tx)
extracted.foreach { case (htlc, preimage) =>
d.commitments.originChannels.get(htlc.id) match {
case Some(origin) =>
log.info("fulfilling htlc #{} paymentHash={} origin={}", htlc.id, htlc.paymentHash, origin)
relayer ! RES_ADD_SETTLED(origin, htlc, HtlcResult.OnChainFulfill(preimage))
case None =>
// if we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal.
// this can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it
// If we don't have the origin, it means that we already have forwarded the fulfill so that's not a big deal.
// This can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it.
log.warning("cannot fulfill htlc #{} paymentHash={} (origin not found)", htlc.id, htlc.paymentHash)
}
}
val revokedCommitPublished1 = d.revokedCommitPublished.map { rev =>
// this transaction may be an HTLC transaction spending a revoked commitment
// in that case, we immediately publish an HTLC-penalty transaction spending its output(s)
val (rev1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.params, channelKeys, d.commitments.remotePerCommitmentSecrets, rev, tx, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey)
penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None))
penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, claimTx.amountIn, hints = Set(claimTx.tx.txid)))
rev1
}
stay() using d.copy(revokedCommitPublished = revokedCommitPublished1) storing()
stay()

case Event(WatchTxConfirmedTriggered(blockHeight, _, tx), d: DATA_CLOSING) =>
log.info("txid={} has reached mindepth, updating closing state", tx.txid)
Expand All @@ -2076,7 +2067,14 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)),
nextRemoteCommitPublished = d.nextRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)),
futureRemoteCommitPublished = d.futureRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)),
revokedCommitPublished = d.revokedCommitPublished.map(Closing.updateRevokedCommitPublished(_, tx))
revokedCommitPublished = d.revokedCommitPublished.map(rvk => {
// If the tx is one of our peer's HTLC txs, they were able to claim the output before us.
// In that case, we immediately publish a penalty transaction spending their HTLC tx to steal their funds.
val (rvk1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.params, channelKeys, d.commitments.remotePerCommitmentSecrets, rvk, tx, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey)
penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None))
penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, claimTx.amountIn, hints = Set(claimTx.tx.txid)))
Closing.updateRevokedCommitPublished(rvk1, tx)
})
)
// if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund
if (d1.localCommitPublished.exists(_.commitTx.txid == tx.txid)) {
Expand Down Expand Up @@ -2109,8 +2107,8 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
}
// we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold
val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match {
case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.localCommit, c.localCommitPublished, d.commitments.params.localParams.dustLimit, tx)
case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.remoteCommit, c.remoteCommitPublished, d.commitments.params.remoteParams.dustLimit, tx)
case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.localCommit, d.commitments.params.localParams.dustLimit, tx)
case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.remoteCommit, tx)
case Some(_: Closing.RevokedClose) => Set.empty[UpdateAddHtlc] // revoked commitments are handled using [[overriddenOutgoingHtlcs]] below
case Some(_: Closing.RecoveryClose) => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarios (future remote commit)
case Some(_: Closing.MutualClose) => Set.empty[UpdateAddHtlc]
Expand Down Expand Up @@ -2171,17 +2169,17 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
val localAnchor_opt = for {
lcp <- d.localCommitPublished
commitKeys = commitment.localKeys(channelKeys)
anchorTx <- Closing.LocalClose.claimAnchors(fundingKey, commitKeys, lcp, commitmentFormat).claimAnchorTx_opt
anchorTx <- Closing.LocalClose.claimAnchor(fundingKey, commitKeys, lcp.commitTx, commitmentFormat)
} yield PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, lcp.commitTx, commitment), c.confirmationTarget)
val remoteAnchor_opt = for {
rcp <- d.remoteCommitPublished
commitKeys = commitment.remoteKeys(channelKeys, commitment.remoteCommit.remotePerCommitmentPoint)
anchorTx <- Closing.RemoteClose.claimAnchors(fundingKey, commitKeys, rcp, commitmentFormat).claimAnchorTx_opt
anchorTx <- Closing.RemoteClose.claimAnchor(fundingKey, commitKeys, rcp.commitTx, commitmentFormat)
} yield PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, rcp.commitTx, commitment), c.confirmationTarget)
val nextRemoteAnchor_opt = for {
nrcp <- d.nextRemoteCommitPublished
commitKeys = commitment.remoteKeys(channelKeys, commitment.nextRemoteCommit_opt.get.commit.remotePerCommitmentPoint)
anchorTx <- Closing.RemoteClose.claimAnchors(fundingKey, commitKeys, nrcp, commitmentFormat).claimAnchorTx_opt
anchorTx <- Closing.RemoteClose.claimAnchor(fundingKey, commitKeys, nrcp.commitTx, commitmentFormat)
} yield PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, nrcp.commitTx, commitment), c.confirmationTarget)
// We favor the remote commitment(s) because they're more interesting than the local commitment (no CSV delays).
if (remoteAnchor_opt.nonEmpty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,19 +233,17 @@ trait ErrorHandlers extends CommonHandlers {

val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
val commitKeys = commitment.localKeys(channelKeys)
val localPaysCommitTxFees = commitment.localParams.paysCommitTxFees
val publishQueue = commitment.params.commitmentFormat match {
case Transactions.DefaultCommitmentFormat =>
val htlcTxs = redeemableHtlcTxs(commitTx, commitKeys, commitment)
List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ htlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)))
case _: Transactions.AnchorOutputsCommitmentFormat =>
val claimAnchor = for {
confirmationTarget <- localCommitPublished.confirmationTarget(nodeParams.onChainFeeConf)
anchorTx <- claimAnchorTx_opt
} yield PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget)
val htlcTxs = redeemableHtlcTxs(commitTx, commitKeys, commitment)
List(PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, localPaysCommitTxFees), None)) ++ claimAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ htlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))
val publishCommitTx = PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, commitment.localParams.paysCommitTxFees), None)
val publishAnchorTx_opt = claimAnchorTx_opt match {
case Some(anchorTx) if !localCommitPublished.isConfirmed =>
val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localParams.dustLimit, commitment.params.commitmentFormat, nodeParams.onChainFeeConf)
Some(PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget))
case _ => None
}
val publishMainDelayedTx_opt = claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None))
val publishHtlcTxs = redeemableHtlcTxs(commitTx, commitKeys, commitment)
val publishHtlcDelayedTxs = claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))
val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs ++ publishHtlcDelayedTxs
publishIfNeeded(publishQueue, irrevocablySpent)

// We watch:
Expand All @@ -259,7 +257,7 @@ trait ErrorHandlers extends CommonHandlers {
// We watch outputs of the commitment tx that both parties may spend.
// We also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees
// in the audit DB, even if we restart before confirmation.
val watchSpentQueue = htlcTxs.keys.toSeq ++ claimAnchorTx_opt.collect { case tx if !localCommitPublished.isConfirmed => tx.input.outPoint }
val watchSpentQueue = htlcTxs.keys.toSeq ++ publishAnchorTx_opt.map(_.input)
watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent)
}

Expand Down Expand Up @@ -331,17 +329,20 @@ trait ErrorHandlers extends CommonHandlers {
import remoteCommitPublished._

val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex)
val remotePerCommitmentPoint = commitment.nextRemoteCommit_opt match {
case Some(c) if remoteCommitPublished.commitTx.txid == c.commit.txid => c.commit.remotePerCommitmentPoint
case _ => commitment.remoteCommit.remotePerCommitmentPoint
val remoteCommit = commitment.nextRemoteCommit_opt match {
case Some(c) if remoteCommitPublished.commitTx.txid == c.commit.txid => c.commit
case _ => commitment.remoteCommit
}
val commitKeys = commitment.remoteKeys(channelKeys, remotePerCommitmentPoint)
val claimAnchor = for {
confirmationTarget <- remoteCommitPublished.confirmationTarget(nodeParams.onChainFeeConf)
anchorTx <- claimAnchorTx_opt
} yield PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget)
val htlcTxs = redeemableClaimHtlcTxs(remoteCommitPublished, commitKeys, commitment)
val publishQueue = claimAnchor ++ claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ htlcTxs
val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint)
val publishAnchorTx_opt = claimAnchorTx_opt match {
case Some(anchorTx) if !remoteCommitPublished.isConfirmed =>
val confirmationTarget = Closing.confirmationTarget(remoteCommit, commitment.remoteParams.dustLimit, commitment.params.commitmentFormat, nodeParams.onChainFeeConf)
Some(PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget))
case _ => None
}
val publishMainTx_opt = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None))
val publishHtlcTxs = redeemableClaimHtlcTxs(remoteCommitPublished, commitKeys, commitment)
val publishQueue = publishAnchorTx_opt ++ publishMainTx_opt ++ publishHtlcTxs
publishIfNeeded(publishQueue, irrevocablySpent)

// We watch:
Expand All @@ -353,7 +354,7 @@ trait ErrorHandlers extends CommonHandlers {
// We watch outputs of the commitment tx that both parties may spend.
// We also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees
// in the audit DB, even if we restart before confirmation.
val watchSpentQueue = claimHtlcTxs.keys.toSeq ++ claimAnchorTx_opt.collect { case tx if !remoteCommitPublished.isConfirmed => tx.input.outPoint }
val watchSpentQueue = claimHtlcTxs.keys.toSeq ++ publishAnchorTx_opt.map(_.input)
watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent)
}

Expand Down
Loading