From 96d0c9a35b2291ecd9ad6cfb515a5d86c76e5e18 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:02:38 +0200 Subject: [PATCH] Add detailed error message when splice feerate is incorrect (#2920) This can be helpful when troubleshooting cross-compatibility issues. Replaces #2911. --- .../scala/fr/acinq/eclair/channel/ChannelExceptions.scala | 1 + .../src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala | 4 ++-- .../fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala | 4 ++-- .../fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 9155c841fa..90ff6f3ccf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -83,6 +83,7 @@ case class UnexpectedFundingSignatures (override val channelId: Byte case class InvalidFundingFeerate (override val channelId: ByteVector32, targetFeerate: FeeratePerKw, actualFeerate: FeeratePerKw) extends ChannelException(channelId, s"invalid funding feerate: target=$targetFeerate actual=$actualFeerate") case class InvalidFundingSignature (override val channelId: ByteVector32, txId_opt: Option[TxId]) extends ChannelException(channelId, s"invalid funding signature: txId=${txId_opt.map(_.toString()).getOrElse("n/a")}") case class InvalidRbfFeerate (override val channelId: ByteVector32, proposed: FeeratePerKw, expected: FeeratePerKw) extends ChannelException(channelId, s"invalid rbf attempt: the feerate must be at least $expected, you proposed $proposed") +case class InvalidSpliceFeerate (override val channelId: ByteVector32, proposed: FeeratePerKw, expected: FeeratePerKw) extends ChannelException(channelId, s"invalid splice request: the feerate must be at least $expected, you proposed $proposed") case class InvalidSpliceRequest (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice request") case class InvalidRbfAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt: the current rbf attempt must be completed or aborted first") case class InvalidSpliceAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid splice attempt: the current splice attempt must be completed or aborted first") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala index fe0dc8c21b..a9c21e88d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala @@ -986,8 +986,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info("rejecting splice request: channel not quiescent") stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceNotQuiescent(d.channelId).getMessage) } else if (msg.feerate < nodeParams.currentBitcoinCoreFeerates.minimum) { - log.info("rejecting splice request: feerate too low") - stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceRequest(d.channelId).getMessage) + log.info("rejecting splice request: feerate too low ({} < {})", msg.feerate, nodeParams.currentBitcoinCoreFeerates.minimum) + stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidSpliceFeerate(d.channelId, msg.feerate, nodeParams.currentBitcoinCoreFeerates.minimum).getMessage) } else if (d.commitments.active.count(_.fundingTxIndex == d.commitments.latest.fundingTxIndex) > 1) { val previousTxs = d.commitments.active.filter(_.fundingTxIndex == d.commitments.latest.fundingTxIndex).map(_.fundingTxId) log.info("rejecting splice request: the previous splice has unconfirmed rbf attempts (txIds={})", previousTxs.mkString(", ")) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index ffc2961848..e4e61be52a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -795,7 +795,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val nextFeerate = Transactions.fee2rate(sharedTx.fees, tx.weight()) if (nextFeerate <= previousFeerate) { log.warn("invalid interactive tx: next feerate isn't greater than previous feerate (previous={}, next={})", previousFeerate, nextFeerate) - return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) + return Left(InvalidRbfFeerate(fundingParams.channelId, nextFeerate, previousFeerate)) } case None => val feeWithoutWitness = Transactions.weight2fee(fundingParams.targetFeerate, tx.weight()) @@ -811,7 +811,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon } if (sharedTx.fees < minimumFee) { log.warn("invalid interactive tx: below the target feerate (target={}, actual={})", fundingParams.targetFeerate, Transactions.fee2rate(sharedTx.fees, tx.weight())) - return Left(InvalidCompleteInteractiveTx(fundingParams.channelId)) + return Left(InvalidSpliceFeerate(fundingParams.channelId, Transactions.fee2rate(sharedTx.fees, tx.weight()), fundingParams.targetFeerate)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala index db9ce3817b..446d5a0a51 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/InteractiveTxBuilderSpec.scala @@ -2669,7 +2669,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit probe.expectMsgType[SendMessage] // Alice --- tx_complete --> Bob bob ! ReceiveMessage(TxComplete(params.channelId)) - assert(probe.expectMsgType[RemoteFailure].cause == InvalidCompleteInteractiveTx(params.channelId)) + assert(probe.expectMsgType[RemoteFailure].cause.isInstanceOf[InvalidSpliceFeerate]) } test("previous attempts not double-spent") {