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
24 changes: 12 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -267,20 +267,20 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
case Some(c: Closing.LocalClose) =>
doPublish(c.localCommitPublished, closing.commitments)
case Some(c: Closing.RemoteClose) =>
doPublish(c.remoteCommitPublished)
doPublish(c.remoteCommitPublished, closing.commitments)
case Some(c: Closing.RecoveryClose) =>
doPublish(c.remoteCommitPublished)
doPublish(c.remoteCommitPublished, closing.commitments)
case Some(c: Closing.RevokedClose) =>
doPublish(c.revokedCommitPublished)
case None =>
// in all other cases we need to be ready for any type of closing
watchFundingTx(data.commitments, closing.spendingTxs.map(_.txid).toSet)
closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isFunder))
closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments))
closing.remoteCommitPublished.foreach(doPublish)
closing.nextRemoteCommitPublished.foreach(doPublish)
closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments))
closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments))
closing.revokedCommitPublished.foreach(doPublish)
closing.futureRemoteCommitPublished.foreach(doPublish)
closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments))

// if commitment number is zero, we also need to make sure that the funding tx has been published
if (closing.commitments.localCommit.index == 0 && closing.commitments.remoteCommit.index == 0) {
Expand Down Expand Up @@ -1415,8 +1415,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo

def republish(): Unit = {
localCommitPublished1.foreach(lcp => doPublish(lcp, commitments1))
remoteCommitPublished1.foreach(doPublish)
nextRemoteCommitPublished1.foreach(doPublish)
remoteCommitPublished1.foreach(rcp => doPublish(rcp, commitments1))
nextRemoteCommitPublished1.foreach(rcp => doPublish(rcp, commitments1))
}

handleCommandSuccess(c, d.copy(commitments = commitments1, localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1)) storing() calling republish()
Expand Down Expand Up @@ -2498,7 +2498,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
case waitForFundingConfirmed: DATA_WAIT_FOR_FUNDING_CONFIRMED => DATA_CLOSING(d.commitments, fundingTx = waitForFundingConfirmed.fundingTx, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished))
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished))
}
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments)
}

private def handleRemoteSpentFuture(commitTx: Transaction, d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) = {
Expand All @@ -2513,7 +2513,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint
val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)
val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished))
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments)
}
}

Expand All @@ -2532,13 +2532,13 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
// NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined
case _ => DATA_CLOSING(d.commitments, fundingTx = None, waitingSinceBlock = nodeParams.currentBlockHeight, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished))
}
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished)
goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments)
}

private def doPublish(remoteCommitPublished: RemoteCommitPublished): Unit = {
private def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = {
import remoteCommitPublished._

val publishQueue = (claimMainOutputTx ++ claimHtlcTxs.values.flatten).map(tx => PublishRawTx(tx, tx.fee, None))
val publishQueue = claimMainOutputTx.map(tx => PublishRawTx(tx, tx.fee, None)).toSeq ++ claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments))
publishIfNeeded(publishQueue, irrevocablySpent)

// we watch:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ object ReplaceableTxPublisher {
private case object TimeLocksOk extends Command
private case object CommitTxAlreadyConfirmed extends RuntimeException with Command
private case object RemoteCommitTxPublished extends RuntimeException with Command
private case object LocalCommitTxConfirmed extends Command
private case object RemoteCommitTxConfirmed extends Command
private case object PreconditionsOk extends Command
private case class FundingFailed(reason: Throwable) extends Command
Expand Down Expand Up @@ -120,6 +121,28 @@ object ReplaceableTxPublisher {
(updatedHtlcTx, fee)
}

def adjustClaimHtlcTxOutput(unsignedTx: ClaimHtlcTx, targetFeerate: FeeratePerKw, commitments: Commitments): Either[TxGenerationSkipped, (ClaimHtlcTx, Satoshi)] = {
require(unsignedTx.tx.txIn.size == 1, "claim-htlc transaction should have a single input")
require(unsignedTx.tx.txOut.size == 1, "claim-htlc transaction should have a single output")
val dummySignedTx = unsignedTx match {
case tx: ClaimHtlcSuccessTx => addSigs(tx, PlaceHolderSig, ByteVector32.Zeroes)
case tx: ClaimHtlcTimeoutTx => addSigs(tx, PlaceHolderSig)
case tx: LegacyClaimHtlcSuccessTx => tx
}
val targetFee = weight2fee(targetFeerate, dummySignedTx.tx.weight())
val outputAmount = unsignedTx.input.txOut.amount - targetFee
if (outputAmount < commitments.localParams.dustLimit) {
Left(AmountBelowDustLimit)
} else {
val updatedClaimHtlcTx = unsignedTx match {
case claimHtlcSuccess: ClaimHtlcSuccessTx => claimHtlcSuccess.copy(tx = claimHtlcSuccess.tx.copy(txOut = Seq(claimHtlcSuccess.tx.txOut.head.copy(amount = outputAmount))))
case claimHtlcTimeout: ClaimHtlcTimeoutTx => claimHtlcTimeout.copy(tx = claimHtlcTimeout.tx.copy(txOut = Seq(claimHtlcTimeout.tx.txOut.head.copy(amount = outputAmount))))
case legacyClaimHtlcSuccess: LegacyClaimHtlcSuccessTx => legacyClaimHtlcSuccess
}
Right(updatedClaimHtlcTx, targetFee)
}
}

sealed trait HtlcTxAndWitnessData {
// @formatter:off
def txInfo: HtlcTx
Expand Down Expand Up @@ -183,6 +206,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
cmd.txInfo match {
case _: ClaimLocalAnchorOutputTx => checkAnchorPreconditions(replyTo, cmd, targetFeerate)
case htlcTx: HtlcTx => checkHtlcPreconditions(replyTo, cmd, htlcTx, targetFeerate)
case claimHtlcTx: ClaimHtlcTx => checkClaimHtlcPreconditions(replyTo, cmd, claimHtlcTx, targetFeerate)
}
case Stop => Behaviors.stopped
}
Expand Down Expand Up @@ -255,6 +279,23 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}

def checkClaimHtlcPreconditions(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, claimHtlcTx: ClaimHtlcTx, targetFeerate: FeeratePerKw): Behavior[Command] = {
// We verify that:
// - our commit is not confirmed: if it is, there is no need to publish our claim-HTLC transactions
context.pipeToSelf(bitcoinClient.getTxConfirmations(cmd.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid)) {
case Success(Some(depth)) if depth >= nodeParams.minDepthBlocks => LocalCommitTxConfirmed
case Success(_) => PreconditionsOk
case Failure(_) => PreconditionsOk // if our checks fail, we don't want it to prevent us from publishing claim-HTLC transactions
}
Behaviors.receiveMessagePartial {
case PreconditionsOk => checkTimeLocks(replyTo, cmd, claimHtlcTx, targetFeerate)
case LocalCommitTxConfirmed =>
log.warn("cannot publish {}: local commit has been confirmed", cmd.desc)
sendResult(replyTo, TxPublisher.TxRejected(loggingInfo.id, cmd, TxPublisher.TxRejectedReason.ConflictingTxConfirmed))
case Stop => Behaviors.stopped
}
}

def checkTimeLocks(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, htlcTxWithWitnessData: HtlcTxAndWitnessData, targetFeerate: FeeratePerKw): Behavior[Command] = {
val timeLocksChecker = context.spawn(TxTimeLocksMonitor(nodeParams, watcher, loggingInfo), "time-locks-monitor")
timeLocksChecker ! CheckTx(context.messageAdapter[TxTimeLocksMonitor.TimeLocksOk](_ => TimeLocksOk), cmd.txInfo.tx, cmd.desc)
Expand All @@ -278,6 +319,48 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
}
}

def checkTimeLocks(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, claimHtlcTx: ClaimHtlcTx, targetFeerate: FeeratePerKw): Behavior[Command] = {
val timeLocksChecker = context.spawn(TxTimeLocksMonitor(nodeParams, watcher, loggingInfo), "time-locks-monitor")
timeLocksChecker ! CheckTx(context.messageAdapter[TxTimeLocksMonitor.TimeLocksOk](_ => TimeLocksOk), cmd.txInfo.tx, cmd.desc)
Behaviors.receiveMessagePartial {
case TimeLocksOk => adjustClaimHtlcTxOutput(claimHtlcTx, targetFeerate, cmd.commitments) match {
case Left(reason) =>
// The HTLC isn't economical to claim at the current feerate, but if the feerate goes down, we may want to claim it later.
log.warn("cannot publish {}: {}", cmd.desc, reason)
sendResult(replyTo, TxPublisher.TxRejected(loggingInfo.id, cmd, TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)))
case Right((updatedClaimHtlcTx, fee)) =>
val channelKeyPath = keyManager.keyPath(cmd.commitments.localParams, cmd.commitments.channelConfig)
val sig = keyManager.sign(updatedClaimHtlcTx, keyManager.htlcPoint(channelKeyPath), cmd.commitments.remoteCommit.remotePerCommitmentPoint, TxOwner.Local, cmd.commitments.commitmentFormat)
updatedClaimHtlcTx match {
case claimHtlcSuccess: LegacyClaimHtlcSuccessTx =>
// The payment hash has been added to claim-htlc-success in https://github.com/ACINQ/eclair/pull/2101
// Some transactions made with older versions of eclair may not set it correctly, in which case we simply
// publish the transaction as initially signed.
log.warn("payment hash not set for htlcId={}, publishing original transaction", claimHtlcSuccess.htlcId)
publish(replyTo, cmd, cmd.txInfo.tx, cmd.txInfo.fee)
case claimHtlcSuccess: ClaimHtlcSuccessTx =>
val preimage_opt = cmd.commitments.localChanges.all.collectFirst {
case u: UpdateFulfillHtlc if Crypto.sha256(u.paymentPreimage) == claimHtlcSuccess.paymentHash => u.paymentPreimage
}
preimage_opt match {
case Some(preimage) =>
val signedClaimHtlcTx = addSigs(claimHtlcSuccess, sig, preimage)
publish(replyTo, cmd, signedClaimHtlcTx.tx, fee)
case None =>
log.error("preimage not found for htlcId={}, skipping...", claimHtlcSuccess.htlcId)
sendResult(replyTo, TxPublisher.TxRejected(loggingInfo.id, cmd, TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = false)))
}
case claimHtlcTimeout: ClaimHtlcTimeoutTx =>
val signedClaimHtlcTx = addSigs(claimHtlcTimeout, sig)
publish(replyTo, cmd, signedClaimHtlcTx.tx, fee)
}
}
case Stop =>
timeLocksChecker ! TxTimeLocksMonitor.Stop
Behaviors.stopped
}
}

def fund(replyTo: ActorRef[TxPublisher.PublishTxResult], cmd: TxPublisher.PublishReplaceableTx, targetFeerate: FeeratePerKw): Behavior[Command] = {
context.pipeToSelf(addInputs(cmd.txInfo, targetFeerate, cmd.commitments)) {
case Success((fundedTx, fee)) => SignFundedTx(fundedTx, fee)
Expand Down Expand Up @@ -330,6 +413,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
case Success(signedTx) => PublishSignedTx(signedTx)
case Failure(reason) => UnknownFailure(reason)
}
case _: ClaimHtlcTx => log.error("claim-htlc-tx should not use external inputs")
}
Behaviors.receiveMessagePartial {
case PublishSignedTx(signedTx) => publish(replyTo, cmd, signedTx, fee)
Expand Down Expand Up @@ -390,6 +474,7 @@ private class ReplaceableTxPublisher(nodeParams: NodeParams,
txInfo match {
case anchorTx: ClaimLocalAnchorOutputTx => addInputs(anchorTx, targetFeerate, commitments)
case htlcTx: HtlcTx => addInputs(htlcTx, targetFeerate, commitments)
case _: ClaimHtlcTx => Future.failed(new RuntimeException("claim-htlc-tx should not use external inputs"))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ object Transactions {
case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long) extends HtlcTx { override def desc: String = "htlc-success" }
case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends HtlcTx { override def desc: String = "htlc-timeout" }
case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" }
sealed trait ClaimHtlcTx extends TransactionWithInputInfo { def htlcId: Long }
sealed trait ClaimHtlcTx extends ReplaceableTransactionWithInputInfo { def htlcId: Long }
case class LegacyClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" }
case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" }
case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" }
Expand Down
Loading