Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3b1537e
Add deadline information to tx publication
t-bast Dec 16, 2021
9bfdd90
Evaluate feerate at tx broadcast time
t-bast Dec 17, 2021
1e4ecc0
Retry conflicting replaceable transactions
t-bast Dec 17, 2021
cc65aee
Refactor replaceable tx publication
t-bast Dec 21, 2021
d1209ad
Regularly bump transaction fees
t-bast Dec 23, 2021
b066753
Fix review comments for 3b1537e
t-bast Jan 11, 2022
3cd7b83
Fix review comments for 9bfdd90
t-bast Jan 12, 2022
29b5039
fixup! Fix review comments for 9bfdd90
t-bast Jan 13, 2022
0a554f7
fixup! fixup! Fix review comments for 9bfdd90
t-bast Jan 13, 2022
fa3e17d
Add more fields to actors private class
t-bast Jan 14, 2022
441a8ee
fixup! Add more fields to actors private class
t-bast Jan 14, 2022
8d13b91
Fix first pass review comments for d1209ad
t-bast Jan 14, 2022
25fa5e6
Remove explicit stopping of leaf actors
t-bast Jan 17, 2022
6d1b34e
Remove intermediate CheckFee message
t-bast Jan 17, 2022
315b0a6
Harmonize sendResult
t-bast Jan 17, 2022
4e635de
ReplaceableTxPublisher update confirmation target
t-bast Jan 17, 2022
0bfeb53
Clarify comment about tx fee
t-bast Jan 17, 2022
63b7cde
fixup! ReplaceableTxPublisher update confirmation target
t-bast Jan 17, 2022
7e863ba
Create case class to hold publish attempts
t-bast Jan 18, 2022
e389af6
Handle funding tx not found
t-bast Jan 18, 2022
7e59cf6
Merge branch 'master' into tx-publisher-deadline
t-bast Jan 18, 2022
d76ae31
Test the PublishAttempts case class
t-bast Jan 18, 2022
67e0d96
Improve tests
t-bast Jan 18, 2022
8b54837
fixup! Improve tests
t-bast Jan 18, 2022
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
29 changes: 26 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2467,8 +2467,21 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
val redeemableHtlcTxs = htlcTxs.values.flatten.map(tx => PublishFinalTx(tx, tx.fee, Some(commitTx.txid)))
List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ (claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)))
case _: Transactions.AnchorOutputsCommitmentFormat =>
val claimLocalAnchor = claimAnchorTxs.collect { case tx: Transactions.ClaimLocalAnchorOutputTx => PublishReplaceableTx(tx, commitments) }
val redeemableHtlcTxs = htlcTxs.values.collect { case Some(tx) => PublishReplaceableTx(tx, commitments) }
val redeemableHtlcTxs = htlcTxs.values.collect {
case Some(tx) =>
val htlc_opt = tx match {
case _: Transactions.HtlcSuccessTx => commitments.localCommit.spec.findIncomingHtlcById(tx.htlcId).map(_.add)
case _: Transactions.HtlcTimeoutTx => commitments.localCommit.spec.findOutgoingHtlcById(tx.htlcId).map(_.add)
Comment thread
pm47 marked this conversation as resolved.
Outdated
}
val deadline = htlc_opt.map(_.cltvExpiry.toLong).getOrElse(nodeParams.currentBlockHeight + nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget)
Comment thread
pm47 marked this conversation as resolved.
Outdated
PublishReplaceableTx(tx, commitments, deadline)
}
val claimLocalAnchor = claimAnchorTxs.collect {
case tx: Transactions.ClaimLocalAnchorOutputTx =>
// NB: if we don't have pending HTLCs, we don't have funds at risk, so we can use a longer deadline.
val deadline = redeemableHtlcTxs.map(_.deadline).minOption.getOrElse(nodeParams.currentBlockHeight + nodeParams.onChainFeeConf.feeTargets.claimMainBlockTarget)
PublishReplaceableTx(tx, commitments, deadline)
}
List(PublishFinalTx(commitTx, commitInput, "commit-tx", Closing.commitTxFee(commitments.commitInput, commitTx, isFunder), None)) ++ claimLocalAnchor ++ claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) ++ redeemableHtlcTxs ++ claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None))
}
publishIfNeeded(publishQueue, irrevocablySpent)
Expand Down Expand Up @@ -2538,7 +2551,17 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, remo
private def doPublish(remoteCommitPublished: RemoteCommitPublished, commitments: Commitments): Unit = {
import remoteCommitPublished._

val publishQueue = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)).toSeq ++ claimHtlcTxs.values.flatten.map(tx => PublishReplaceableTx(tx, commitments))
val redeemableHtlcTxs = claimHtlcTxs.values.collect {
case Some(tx) =>
val htlc_opt = tx match {
case _: Transactions.LegacyClaimHtlcSuccessTx => commitments.remoteNextCommitInfo.left.toOption.flatMap(_.nextRemoteCommit.spec.findOutgoingHtlcById(tx.htlcId)).orElse(commitments.remoteCommit.spec.findOutgoingHtlcById(tx.htlcId)).map(_.add)
case _: Transactions.ClaimHtlcSuccessTx => commitments.remoteNextCommitInfo.left.toOption.flatMap(_.nextRemoteCommit.spec.findOutgoingHtlcById(tx.htlcId)).orElse(commitments.remoteCommit.spec.findOutgoingHtlcById(tx.htlcId)).map(_.add)
case _: Transactions.ClaimHtlcTimeoutTx => commitments.remoteNextCommitInfo.left.toOption.flatMap(_.nextRemoteCommit.spec.findIncomingHtlcById(tx.htlcId)).orElse(commitments.remoteCommit.spec.findIncomingHtlcById(tx.htlcId)).map(_.add)
}
val deadline = htlc_opt.map(_.cltvExpiry.toLong).getOrElse(nodeParams.currentBlockHeight + nodeParams.onChainFeeConf.feeTargets.commitmentBlockTarget)
PublishReplaceableTx(tx, commitments, deadline)
}
val publishQueue = 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 @@ -455,39 +455,39 @@ final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Com

/**
* @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the channel permanent features
* (see [[ChannelFeatures]]).
* features are updated at each reconnection and may be different from the channel permanent features
* (see [[ChannelFeatures]]).
*/
final case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
dustLimit: Satoshi,
maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi
channelReserve: Satoshi,
htlcMinimum: MilliSatoshi,
toSelfDelay: CltvExpiryDelta,
maxAcceptedHtlcs: Int,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
walletStaticPaymentBasepoint: Option[PublicKey],
initFeatures: Features)
case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
dustLimit: Satoshi,
maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi
channelReserve: Satoshi,
htlcMinimum: MilliSatoshi,
toSelfDelay: CltvExpiryDelta,
maxAcceptedHtlcs: Int,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
walletStaticPaymentBasepoint: Option[PublicKey],
initFeatures: Features)

/**
* @param initFeatures see [[LocalParams.initFeatures]]
*/
final case class RemoteParams(nodeId: PublicKey,
dustLimit: Satoshi,
maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi
channelReserve: Satoshi,
htlcMinimum: MilliSatoshi,
toSelfDelay: CltvExpiryDelta,
maxAcceptedHtlcs: Int,
fundingPubKey: PublicKey,
revocationBasepoint: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
initFeatures: Features,
shutdownScript: Option[ByteVector])
case class RemoteParams(nodeId: PublicKey,
dustLimit: Satoshi,
maxHtlcValueInFlightMsat: UInt64, // this is not MilliSatoshi because it can exceed the total amount of MilliSatoshi
channelReserve: Satoshi,
htlcMinimum: MilliSatoshi,
toSelfDelay: CltvExpiryDelta,
maxAcceptedHtlcs: Int,
fundingPubKey: PublicKey,
revocationBasepoint: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
initFeatures: Features,
shutdownScript: Option[ByteVector])

object ChannelFlags {
val AnnounceChannel = 0x01.toByte
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,11 @@ object Helpers {
log.error(s"some htlcs don't have a corresponding timeout transaction: tx=$tx, htlcs=$matchingHtlcs, timeout-txs=$matchingTxs")
}
matchingHtlcs.zip(matchingTxs).collectFirst {
// HTLC transactions cannot change when anchor outputs is unused, so we directly check the txid
Comment thread
pm47 marked this conversation as resolved.
Outdated
case (add, timeoutTx) if timeoutTx.txid == tx.txid => add
// Claim-HTLC transactions can be updated to pay more or less fees by changing the output amount, so we cannot
// rely on txid equality: we instead check that the input is the same and the output goes to the same address.
case (add, timeoutTx) if timeoutTx.txIn.head.outPoint == tx.txIn.head.outPoint && timeoutTx.txOut.head.publicKeyScript == tx.txOut.head.publicKeyScript => add
Comment thread
pm47 marked this conversation as resolved.
Outdated
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ private class FinalTxPublisher(nodeParams: NodeParams,
val txMonitor = context.spawn(MempoolTxMonitor(nodeParams, bitcoinClient, loggingInfo), "mempool-tx-monitor")
txMonitor ! MempoolTxMonitor.Publish(context.messageAdapter[MempoolTxMonitor.TxResult](WrappedTxResult), cmd.tx, cmd.input, cmd.desc, cmd.fee)
Behaviors.receiveMessagePartial {
case WrappedTxResult(MempoolTxMonitor.TxConfirmed) => sendResult(replyTo, TxPublisher.TxConfirmed(cmd, cmd.tx))
case WrappedTxResult(MempoolTxMonitor.TxRejected(reason)) => sendResult(replyTo, TxPublisher.TxRejected(loggingInfo.id, cmd, reason))
case WrappedTxResult(MempoolTxMonitor.TxConfirmed(tx)) => sendResult(replyTo, TxPublisher.TxConfirmed(cmd, tx))
case WrappedTxResult(MempoolTxMonitor.TxRejected(_, reason)) => sendResult(replyTo, TxPublisher.TxRejected(loggingInfo.id, cmd, reason))
case Stop =>
txMonitor ! MempoolTxMonitor.Stop
Behaviors.stopped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ object MempoolTxMonitor {

// @formatter:off
sealed trait TxResult
case object TxConfirmed extends TxResult
case class TxRejected(reason: TxPublisher.TxRejectedReason) extends TxResult
case class TxConfirmed(tx: Transaction) extends TxResult
case class TxRejected(txid: ByteVector32, reason: TxPublisher.TxRejectedReason) extends TxResult
// @formatter:on

def apply(nodeParams: NodeParams, bitcoinClient: BitcoinCoreClient, loggingInfo: TxPublishLogContext): Behavior[Command] = {
Expand Down Expand Up @@ -90,29 +90,29 @@ private class MempoolTxMonitor(nodeParams: NodeParams, bitcoinClient: BitcoinCor
waitForConfirmation(replyTo, tx, input)
case PublishFailed(reason) if reason.getMessage.contains("rejecting replacement") =>
log.info("could not publish tx: a conflicting mempool transaction is already in the mempool")
sendResult(replyTo, TxRejected(TxRejectedReason.ConflictingTxUnconfirmed))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.ConflictingTxUnconfirmed))
case PublishFailed(reason) if reason.getMessage.contains("bad-txns-inputs-missingorspent") =>
// This can only happen if one of our inputs is already spent by a confirmed transaction or doesn't exist (e.g.
// unconfirmed wallet input that has been replaced).
checkInputStatus(input)
Behaviors.same
case PublishFailed(reason) =>
log.error("could not publish transaction", reason)
sendResult(replyTo, TxRejected(TxRejectedReason.UnknownTxFailure))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.UnknownTxFailure))
case status: InputStatus =>
if (status.spentConfirmed) {
log.info("could not publish tx: a conflicting transaction is already confirmed")
sendResult(replyTo, TxRejected(TxRejectedReason.ConflictingTxConfirmed))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.ConflictingTxConfirmed))
} else if (status.spentUnconfirmed) {
log.info("could not publish tx: a conflicting mempool transaction is already in the mempool")
sendResult(replyTo, TxRejected(TxRejectedReason.ConflictingTxUnconfirmed))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.ConflictingTxUnconfirmed))
} else {
log.info("could not publish tx: one of our wallet inputs is not available")
sendResult(replyTo, TxRejected(TxRejectedReason.WalletInputGone))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.WalletInputGone))
}
case CheckInputFailed(reason) =>
log.error("could not check input status", reason)
sendResult(replyTo, TxRejected(TxRejectedReason.TxSkipped(retryNextBlock = true))) // we act as if the input is potentially still spendable
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.TxSkipped(retryNextBlock = true))) // we act as if the input is potentially still spendable
case Stop =>
Behaviors.stopped
}
Expand All @@ -136,7 +136,7 @@ private class MempoolTxMonitor(nodeParams: NodeParams, bitcoinClient: BitcoinCor
if (nodeParams.minDepthBlocks <= confirmations) {
log.info("txid={} has reached min depth", tx.txid)
context.system.eventStream ! EventStream.Publish(TransactionConfirmed(loggingInfo.channelId_opt.getOrElse(ByteVector32.Zeroes), loggingInfo.remoteNodeId, tx))
sendResult(replyTo, TxConfirmed, Some(messageAdapter))
sendResult(replyTo, TxConfirmed(tx), Some(messageAdapter))
} else {
Behaviors.same
}
Expand All @@ -151,17 +151,17 @@ private class MempoolTxMonitor(nodeParams: NodeParams, bitcoinClient: BitcoinCor
case status: InputStatus =>
if (status.spentConfirmed) {
log.info("tx was evicted from the mempool: a conflicting transaction has been confirmed")
sendResult(replyTo, TxRejected(TxRejectedReason.ConflictingTxConfirmed))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.ConflictingTxConfirmed))
} else if (status.spentUnconfirmed) {
log.info("tx was evicted from the mempool: a conflicting transaction replaced it")
sendResult(replyTo, TxRejected(TxRejectedReason.ConflictingTxUnconfirmed))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.ConflictingTxUnconfirmed))
} else {
log.info("tx was evicted from the mempool: one of our wallet inputs disappeared")
sendResult(replyTo, TxRejected(TxRejectedReason.WalletInputGone))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.WalletInputGone))
}
case CheckInputFailed(reason) =>
log.error("could not check input status", reason)
sendResult(replyTo, TxRejected(TxRejectedReason.TxSkipped(retryNextBlock = true)), Some(messageAdapter))
sendResult(replyTo, TxRejected(tx.txid, TxRejectedReason.TxSkipped(retryNextBlock = true)), Some(messageAdapter))
case Stop =>
context.system.eventStream ! EventStream.Unsubscribe(messageAdapter)
Behaviors.stopped
Expand Down
Loading