1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 22:25:26 +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:
Bastien Teinturier 2022-02-02 15:36:22 +01:00 committed by GitHub
parent 44510698f7
commit cc61f121ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 44 additions and 17 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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") {

View file

@ -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()

View file

@ -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._