mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-23 06:35:11 +01:00
Fix publish loop when funding tx double-spent (#2162)
If a funding tx is double-spent, we can't publish our commit tx. However, the previous code would retry very regularly in a loop, polluting the logs. When that happens, we now only retry when a new block is found.
This commit is contained in:
parent
44510698f7
commit
cc61f121ec
5 changed files with 44 additions and 17 deletions
|
@ -122,8 +122,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams,
|
|||
log.info("could not publish tx: a conflicting mempool transaction is already in the mempool")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed))
|
||||
} else {
|
||||
log.info("could not publish tx: one of our wallet inputs is not available")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone))
|
||||
log.info("could not publish tx: one of our inputs cannot be found")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone))
|
||||
}
|
||||
case CheckInputFailed(reason) =>
|
||||
log.error("could not check input status: ", reason)
|
||||
|
@ -174,8 +174,8 @@ private class MempoolTxMonitor(nodeParams: NodeParams,
|
|||
log.info("tx was evicted from the mempool: a conflicting transaction replaced it")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.ConflictingTxUnconfirmed))
|
||||
} else {
|
||||
log.info("tx was evicted from the mempool: one of our wallet inputs disappeared")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.WalletInputGone))
|
||||
log.info("tx was evicted from the mempool: one of our inputs disappeared")
|
||||
sendFinalResult(TxRejected(cmd.tx.txid, TxRejectedReason.InputGone))
|
||||
}
|
||||
case CheckInputFailed(reason) =>
|
||||
log.error("could not check input status: ", reason)
|
||||
|
|
|
@ -107,8 +107,8 @@ object TxPublisher {
|
|||
object TxRejectedReason {
|
||||
/** We don't have enough funds in our wallet to reach the given feerate. */
|
||||
case object CouldNotFund extends TxRejectedReason
|
||||
/** The transaction was published but then evicted from the mempool, because one of its wallet inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */
|
||||
case object WalletInputGone extends TxRejectedReason
|
||||
/** The transaction was published but then evicted from the mempool, because one of its inputs disappeared (e.g. unconfirmed output of a transaction that was replaced). */
|
||||
case object InputGone extends TxRejectedReason
|
||||
/** A conflicting transaction spending the same input has been confirmed. */
|
||||
case object ConflictingTxConfirmed extends TxRejectedReason
|
||||
/** A conflicting transaction spending the same input is in the mempool and we failed to replace it. */
|
||||
|
@ -255,12 +255,19 @@ private class TxPublisher(nodeParams: NodeParams, factory: TxPublisher.ChildFact
|
|||
stopAttempts(rejectedAttempts)
|
||||
val pending2 = if (remainingAttempts.isEmpty) pending - cmd.input else pending + (cmd.input -> remainingAttempts)
|
||||
reason match {
|
||||
case TxRejectedReason.WalletInputGone =>
|
||||
case TxRejectedReason.InputGone =>
|
||||
// Our transaction has been evicted from the mempool because it depended on an unconfirmed input that has
|
||||
// been replaced. We should be able to retry right now with new wallet inputs (no need to wait for a new
|
||||
// block).
|
||||
timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)
|
||||
run(pending2, retryNextBlock, channelContext)
|
||||
// been replaced.
|
||||
cmd match {
|
||||
case _: PublishReplaceableTx =>
|
||||
// We should be able to retry right now with new wallet inputs (no need to wait for a new block).
|
||||
timers.startSingleTimer(cmd, (1 + Random.nextLong(nodeParams.channelConf.maxTxPublishRetryDelay.toMillis)).millis)
|
||||
run(pending2, retryNextBlock, channelContext)
|
||||
case _: PublishFinalTx =>
|
||||
// The transaction cannot be replaced, so there is no point in retrying immediately, let's wait until
|
||||
// the next block to see if our input comes back to the mempool.
|
||||
run(pending2, retryNextBlock ++ rejectedAttempts.map(_.cmd), channelContext)
|
||||
}
|
||||
case TxRejectedReason.CouldNotFund =>
|
||||
// We don't have enough funds at the moment to afford our target feerate, but it may change once pending
|
||||
// transactions confirm, so we retry when a new block is found.
|
||||
|
|
|
@ -148,7 +148,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi
|
|||
val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0)
|
||||
val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0)))
|
||||
monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat)
|
||||
probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone))
|
||||
probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone))
|
||||
}
|
||||
|
||||
test("publish failed (confirmed parent, wallet input doesn't exist)") {
|
||||
|
@ -161,7 +161,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi
|
|||
val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 0, 0)
|
||||
val txUnknownInput = tx.copy(txIn = tx.txIn ++ Seq(TxIn(OutPoint(randomBytes32(), 13), Nil, 0)))
|
||||
monitor ! Publish(probe.ref, txUnknownInput, txUnknownInput.txIn.head.outPoint, "test-tx", 10 sat)
|
||||
probe.expectMsg(TxRejected(txUnknownInput.txid, WalletInputGone))
|
||||
probe.expectMsg(TxRejected(txUnknownInput.txid, InputGone))
|
||||
}
|
||||
|
||||
test("publish failed (wallet input spent by conflicting confirmed transaction)") {
|
||||
|
@ -176,7 +176,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi
|
|||
|
||||
val tx = createSpendManyP2WPKH(Seq(parentTx, walletTx), priv, priv.publicKey, 5_000 sat, 0, 0)
|
||||
monitor ! Publish(probe.ref, tx, tx.txIn.head.outPoint, "test-tx", 10 sat)
|
||||
probe.expectMsg(TxRejected(tx.txid, WalletInputGone))
|
||||
probe.expectMsg(TxRejected(tx.txid, InputGone))
|
||||
}
|
||||
|
||||
test("publish succeeds then transaction is replaced by an unconfirmed tx") {
|
||||
|
@ -235,7 +235,7 @@ class MempoolTxMonitorSpec extends TestKitBaseClass with AnyFunSuiteLike with Bi
|
|||
// When a new block is found, we detect that the transaction has been evicted.
|
||||
generateBlocks(1)
|
||||
system.eventStream.publish(CurrentBlockHeight(currentBlockHeight(probe)))
|
||||
probe.expectMsg(TxRejected(tx.txid, WalletInputGone))
|
||||
probe.expectMsg(TxRejected(tx.txid, InputGone))
|
||||
}
|
||||
|
||||
test("emit transaction events") {
|
||||
|
|
|
@ -278,7 +278,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
|
|||
|
||||
val result = probe.expectMsgType[TxRejected]
|
||||
assert(result.cmd === anchorTx)
|
||||
assert(result.reason === WalletInputGone)
|
||||
assert(result.reason === InputGone)
|
||||
|
||||
// Since our wallet input is gone, we will retry and discover that a commit tx has been confirmed.
|
||||
val publisher2 = createPublisher()
|
||||
|
|
|
@ -186,7 +186,7 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
|
|||
val attempt2 = factory.expectMsgType[ReplaceableTxPublisherSpawned]
|
||||
attempt2.actor.expectMsgType[ReplaceableTxPublisher.Publish]
|
||||
|
||||
txPublisher ! TxRejected(attempt2.id, cmd2, WalletInputGone)
|
||||
txPublisher ! TxRejected(attempt2.id, cmd2, InputGone)
|
||||
attempt2.actor.expectMsg(ReplaceableTxPublisher.Stop)
|
||||
attempt1.actor.expectNoMessage(100 millis) // this error doesn't impact other publishing attempts
|
||||
|
||||
|
@ -195,6 +195,26 @@ class TxPublisherSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
|
|||
assert(attempt3.actor.expectMsgType[ReplaceableTxPublisher.Publish].cmd === cmd2)
|
||||
}
|
||||
|
||||
test("publishing attempt fails (main input gone)") { f =>
|
||||
import f._
|
||||
|
||||
val input = OutPoint(randomBytes32(), 3)
|
||||
val tx = Transaction(2, TxIn(input, Nil, 0) :: Nil, Nil, 0)
|
||||
val cmd = PublishFinalTx(tx, input, "final-tx", 0 sat, None)
|
||||
txPublisher ! cmd
|
||||
val attempt1 = factory.expectMsgType[FinalTxPublisherSpawned]
|
||||
attempt1.actor.expectMsgType[FinalTxPublisher.Publish]
|
||||
|
||||
txPublisher ! TxRejected(attempt1.id, cmd, InputGone)
|
||||
attempt1.actor.expectMsg(FinalTxPublisher.Stop)
|
||||
|
||||
// We don't retry until a new block is found.
|
||||
factory.expectNoMessage(100 millis)
|
||||
system.eventStream.publish(CurrentBlockHeight(BlockHeight(8200)))
|
||||
val attempt2 = factory.expectMsgType[FinalTxPublisherSpawned]
|
||||
assert(attempt2.actor.expectMsgType[FinalTxPublisher.Publish].cmd === cmd)
|
||||
}
|
||||
|
||||
test("publishing attempt fails (not enough funds)") { f =>
|
||||
import f._
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue