From 0e7c9b4150714be9fdaefd29986343f2937be483 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 17 May 2022 17:19:20 +0200 Subject: [PATCH 1/6] Dual funding rbf support Add support for bumping the fees of a dual funding transaction. We spawn a transient dedicated actor: if the RBF attempt fails, or if we are disconnected before completing the protocol, we should forget it. --- docs/release-notes/eclair-vnext.md | 26 ++- eclair-core/eclair-cli | 1 + .../main/scala/fr/acinq/eclair/Eclair.scala | 7 + .../fr/acinq/eclair/channel/ChannelData.scala | 4 +- .../channel/fsm/ChannelOpenDualFunded.scala | 176 ++++++++++++++++-- .../channel/fsm/DualFundingHandlers.scala | 9 +- .../channel/version3/ChannelCodecs3.scala | 2 +- .../channel/InteractiveTxBuilderSpec.scala | 35 +++- ...WaitForDualFundingConfirmedStateSpec.scala | 101 ++++++++++ .../acinq/eclair/api/handlers/Channel.scala | 12 +- 10 files changed, 351 insertions(+), 22 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index c9a6e9c31c..6cdc1f3976 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -73,6 +73,27 @@ $ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=an $ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false ``` +### Experimental support for dual-funding + +This release adds experimental support for dual-funded channels, as specified [here](https://github.com/lightning/bolts/pull/851). +Dual-funded channels have many benefits: + +- both peers can contribute to channel funding +- the funding transaction can be RBF-ed + +This feature is turned off by default, because there may still be breaking changes in the specification. +To turn it on, simply enable the feature in your `eclair.conf`: + +```conf +eclair.features.option_dual_fund = optional +``` + +If your peer also supports the feature, eclair will automatically use dual-funding when opening a channel. +If the channel doesn't confirm, you can use the `rbfopen` RPC to initiate an RBF attempt and speed up confirmation. + +In this first version, the non-initiator cannot yet contribute funds to the channel. +This will be added in future updates. + ### Changes to features override Eclair supports overriding features on a per-peer basis, using the `eclair.override-init-features` field in `eclair.conf`. @@ -91,9 +112,10 @@ upgrading to this release. ### API changes -- `channelbalances`: retrieves information about the balances of all local channels (#2196) -- `stop`: stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `channelbalances` retrieves information about the balances of all local channels (#2196) - `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323) +- `stop` stops eclair: please note that the recommended way of stopping eclair is simply to kill its process (#2233) +- `rbfopen` lets the initiator of a dual-funded channel RBF the funding transaction (#2275) ### Miscellaneous improvements and bug fixes diff --git a/eclair-core/eclair-cli b/eclair-core/eclair-cli index 22be3fe5f5..065443cd69 100755 --- a/eclair-core/eclair-cli +++ b/eclair-core/eclair-cli @@ -38,6 +38,7 @@ and COMMAND is one of the available commands: === Channel === - open + - rbfopen - close - forceclose - channel diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 670c2b2fea..90ed250800 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -88,6 +88,8 @@ trait Eclair { def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] + def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] + def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] @@ -196,6 +198,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } yield res } + override def rbfOpen(channelId: ByteVector32, targetFeerate: FeeratePerKw, lockTime_opt: Option[Long])(implicit timeout: Timeout): Future[CommandResponse[CMD_BUMP_FUNDING_FEE]] = { + val cmd = CMD_BUMP_FUNDING_FEE(ActorRef.noSender, targetFeerate, lockTime_opt.getOrElse(appKit.nodeParams.currentBlockHeight.toLong)) + sendToChannel(Left(channelId), cmd) + } + override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { sendToChannels(channels, CMD_CLOSE(ActorRef.noSender, scriptPubKey_opt, closingFeerates_opt)) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index f7b85b7cf5..44b6fb9cbc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -194,6 +194,8 @@ final case class ClosingFeerates(preferred: FeeratePerKw, min: FeeratePerKw, max sealed trait CloseCommand extends HasReplyToCommand final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand + +final case class CMD_BUMP_FUNDING_FEE(replyTo: ActorRef, targetFeerate: FeeratePerKw, lockTime: Long) extends HasReplyToCommand final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand final case class CMD_GET_CHANNEL_STATE(replyTo: ActorRef) extends HasReplyToCommand final case class CMD_GET_CHANNEL_DATA(replyTo: ActorRef) extends HasReplyToCommand @@ -495,7 +497,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, previousFundingTxs: List[DualFundingTx], waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm lastChecked: BlockHeight, // last time we checked if the channel was double-spent - rbfAttempt: Option[typed.ActorRef[InteractiveTxBuilder.Command]], + rbfAttempt: Option[Either[CMD_BUMP_FUNDING_FEE, typed.ActorRef[InteractiveTxBuilder.Command]]], deferred: Option[ChannelReady]) extends PersistentChannelData final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, shortIds: ShortIds, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 5d0e27fb1c..e05c9c983f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.{ClassicActorContextOps, actorRefAdapter} +import akka.actor.{ActorRef, Status} import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.Funding @@ -362,23 +363,156 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() using d1 storing() calling publishFundingTx(d.fundingParams, fundingTx) } case _: FullySignedSharedTransaction => - log.warning("received duplicate tx_signatures") - stay() + d.rbfAttempt match { + case Some(Right(txBuilder)) => + txBuilder ! InteractiveTxBuilder.ReceiveTxSigs(txSigs) + stay() + case _ => + // Signatures are retransmitted on reconnection, but we may have already received them. + log.info("ignoring duplicate tx_signatures for txid={}", txSigs.txId) + stay() + } } - case Event(_: TxInitRbf, _: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - log.info("rbf not supported yet") - stay() + case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + if (d.fundingParams.isInitiator) { + // Only the initiator is allowed to initiate RBF. + log.info("rejecting tx_init_rbf, we're the initiator, not them!") + stay() sending TxAbort(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) + } else { + val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 + if (d.rbfAttempt.nonEmpty) { + log.info("rejecting rbf attempt: the current rbf attempt must be completed or aborted first") + stay() sending TxAbort(d.channelId, InvalidRbfAlreadyInProgress(d.channelId).getMessage) + } else if (msg.feerate < minNextFeerate) { + log.info("rejecting rbf attempt: the new feerate must be at least {} (proposed={})", minNextFeerate, msg.feerate) + stay() sending TxAbort(d.channelId, InvalidRbfFeerate(d.channelId, msg.feerate, minNextFeerate).getMessage) + } else { + log.info("our peer wants to raise the feerate of the funding transaction (previous={} target={})", d.fundingParams.targetFeerate, msg.feerate) + val fundingParams = InteractiveTxParams( + d.channelId, + d.fundingParams.isInitiator, + d.fundingParams.localAmount, // we don't change our funding contribution + msg.fundingContribution_opt.getOrElse(d.fundingParams.remoteAmount), + d.fundingParams.fundingPubkeyScript, + msg.lockTime, + d.fundingParams.dustLimit, + msg.feerate + ) + val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( + remoteNodeId, fundingParams, keyManager, + d.commitments.localParams, d.commitments.remoteParams, + d.commitments.localCommit.spec.commitTxFeerate, + d.commitments.remoteCommit.remotePerCommitmentPoint, + d.commitments.channelFlags, d.commitments.channelConfig, d.commitments.channelFeatures, + wallet)) + txBuilder ! InteractiveTxBuilder.Start(self, d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)) + stay() using d.copy(rbfAttempt = Some(Right(txBuilder))) sending TxAckRbf(d.channelId, fundingParams.localAmount) + } + } - case Event(_: TxAckRbf, _: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - log.info("rbf not supported yet") - stay() + case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + val replyTo = if (cmd.replyTo == ActorRef.noSender) sender() else cmd.replyTo + if (!d.fundingParams.isInitiator) { + replyTo ! Status.Failure(new RuntimeException("cannot initiate rbf: we're not the initiator of this dual-funding attempt")) + stay() + } else { + d.rbfAttempt match { + case Some(_) => + log.warning("cannot initiate rbf, another one is already in progress") + replyTo ! Status.Failure(InvalidRbfAlreadyInProgress(d.channelId)) + stay() + case None => + val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 + if (cmd.targetFeerate < minNextFeerate) { + replyTo ! Status.Failure(InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate)) + stay() + } else { + stay() using d.copy(rbfAttempt = Some(Left(cmd.copy(replyTo = replyTo)))) sending TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.fundingParams.localAmount) + } + } + } + + case Event(msg: TxAckRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + d.rbfAttempt match { + case Some(Left(cmd)) => + log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution_opt.getOrElse(0 sat)) + cmd.replyTo ! RES_SUCCESS(cmd, d.channelId) + val fundingParams = InteractiveTxParams( + d.channelId, + d.fundingParams.isInitiator, + d.fundingParams.localAmount, // we don't change our funding contribution + msg.fundingContribution_opt.getOrElse(d.fundingParams.remoteAmount), + d.fundingParams.fundingPubkeyScript, + cmd.lockTime, + d.fundingParams.dustLimit, + cmd.targetFeerate + ) + val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( + remoteNodeId, fundingParams, keyManager, + d.commitments.localParams, d.commitments.remoteParams, + d.commitments.localCommit.spec.commitTxFeerate, + d.commitments.remoteCommit.remotePerCommitmentPoint, + d.commitments.channelFlags, d.commitments.channelConfig, d.commitments.channelFeatures, + wallet)) + txBuilder ! InteractiveTxBuilder.Start(self, d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)) + stay() using d.copy(rbfAttempt = Some(Right(txBuilder))) + case _ => + log.info("ignoring unexpected tx_ack_rbf") + stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) + } case Event(msg: InteractiveTxConstructionMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) + d.rbfAttempt match { + case Some(Right(txBuilder)) => + txBuilder ! InteractiveTxBuilder.ReceiveTxMessage(msg) + stay() + case _ => + log.info("ignoring unexpected interactive-tx message: {}", msg.getClass.getSimpleName) + stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) + } + + case Event(commitSig: CommitSig, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + d.rbfAttempt match { + case Some(Right(txBuilder)) => + txBuilder ! InteractiveTxBuilder.ReceiveCommitSig(commitSig) + stay() + case _ => + log.info("ignoring unexpected commit_sig") + stay() sending Warning(d.channelId, UnexpectedCommitSig(d.channelId).getMessage) + } case Event(msg: TxAbort, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) + d.rbfAttempt match { + case Some(Right(txBuilder)) => + log.info("our peer aborted the rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) + txBuilder ! InteractiveTxBuilder.Abort + stay() using d.copy(rbfAttempt = None) + case Some(Left(cmd)) => + log.info("our peer rejected our rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) + cmd.replyTo ! Status.Failure(new RuntimeException(s"rbf attempt rejected by our peer: ${msg.toAscii}")) + stay() using d.copy(rbfAttempt = None) + case None => + log.info("ignoring unexpected tx_abort message") + stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) + } + + case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => msg match { + case InteractiveTxBuilder.SendMessage(msg) => stay() sending msg + case InteractiveTxBuilder.Succeeded(fundingParams, fundingTx, commitments) => + // We now have more than one version of the funding tx, so we cannot use zero-conf. + val fundingMinDepth = Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams).getOrElse(nodeParams.channelConf.minDepthBlocks.toLong) + blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth) + val previousFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs + val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, previousFundingTxs, d.waitingSince, d.lastChecked, None, d.deferred) + fundingTx match { + case fundingTx: PartiallySignedSharedTransaction => stay() using d1 storing() sending fundingTx.localSigs + case fundingTx: FullySignedSharedTransaction => stay() using d1 storing() sending fundingTx.localSigs calling publishFundingTx(fundingParams, fundingTx) + } + case f: InteractiveTxBuilder.Failed => + log.info("rbf attempt failed: {}", f.cause.getMessage) + stay() using d.copy(rbfAttempt = None) sending TxAbort(d.channelId, f.cause.getMessage) + } case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, confirmedTx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => // We find which funding transaction got confirmed. @@ -391,7 +525,19 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) d.deferred.foreach(self ! _) - goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady) storing() sending channelReady + val otherFundingTxs = allFundingTxs.filter(_.commitments.commitInput.outPoint.txid != confirmedTx.txid).map(_.fundingTx) + rollbackDualFundingTxs(otherFundingTxs) + val toSend = d.rbfAttempt match { + case Some(Right(txBuilder)) => + txBuilder ! InteractiveTxBuilder.Abort + Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady) + case Some(Left(cmd)) => + cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId)) + Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady) + case _ => + Seq(channelReady) + } + goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady) storing() sending toSend case None => log.error(s"internal error: the funding tx that confirmed doesn't match any of our funding txs: ${confirmedTx.bin}") rollbackDualFundingTxs(allFundingTxs.map(_.fundingTx)) @@ -425,6 +571,14 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { delayEarlyAnnouncementSigs(remoteAnnSigs) stay() + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + d.rbfAttempt match { + case Some(Right(txBuilder)) => txBuilder ! InteractiveTxBuilder.Abort + case Some(Left(cmd)) => cmd.replyTo ! Status.Failure(new RuntimeException("rbf attempt failed: disconnected")) + case None => // nothing to do + } + goto(OFFLINE) using d.copy(rbfAttempt = None) + case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 0bf77b2442..6a04bd799f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -17,6 +17,8 @@ package fr.acinq.eclair.channel.fsm import fr.acinq.bitcoin.scalacompat.{Transaction, TxIn} +import fr.acinq.eclair.NotificationsLogger +import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.CurrentBlockHeight import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing @@ -80,7 +82,12 @@ trait DualFundingHandlers extends CommonFundingHandlers { } def handleNewBlockDualFundingUnconfirmed(c: CurrentBlockHeight, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { - if (Channel.FUNDING_TIMEOUT_FUNDEE < c.blockHeight - d.waitingSince && Closing.nothingAtStake(d)) { + // We regularly notify the node operator that they may want to RBF this channel. + val blocksSinceOpen = c.blockHeight - d.waitingSince + if (d.fundingParams.isInitiator && (blocksSinceOpen % 288 == 0)) { + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Info, s"channelId=${d.channelId} is still unconfirmed after $blocksSinceOpen blocks, you may need to use the rbfopen RPC to make it confirm.")) + } + if (Channel.FUNDING_TIMEOUT_FUNDEE < blocksSinceOpen && Closing.nothingAtStake(d)) { log.warning("funding transaction did not confirm in {} blocks and we have nothing at stake, forgetting channel", Channel.FUNDING_TIMEOUT_FUNDEE) handleFundingTimeout(d) } else if (d.lastChecked + 6 < c.blockHeight) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index d5406c09dc..9139cf3082 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -390,7 +390,7 @@ private[channel] object ChannelCodecs3 { ("previousFundingTxs" | listOfN(uint16, dualFundingTxCodec)) :: ("waitingSince" | blockHeight) :: ("lastChecked" | blockHeight) :: - ("rbfAttempt" | provide(Option.empty[typed.ActorRef[InteractiveTxBuilder.Command]])) :: + ("rbfAttempt" | provide(Option.empty[Either[CMD_BUMP_FUNDING_FEE, typed.ActorRef[InteractiveTxBuilder.Command]]])) :: ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val DATA_WAIT_FOR_DUAL_FUNDING_READY_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( 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 1d473933b2..5f35d88dee 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 @@ -21,7 +21,7 @@ import akka.actor.typed.scaladsl.adapter.{ClassicActorSystemOps, actorRefAdapter import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxOut} import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{MempoolTx, Utxo} @@ -35,7 +35,7 @@ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{Feature, FeatureSupport, Features, InitFeature, NodeParams, TestConstants, TestKitBaseClass, UInt64, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, HexStringSyntax} import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global @@ -1079,4 +1079,35 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit assert(alice2bob.expectMsgType[RemoteFailure].cause.isInstanceOf[InvalidFundingSignature]) } + test("reference test vector") { + val channelId = ByteVector32.Zeroes + val parentTx = Transaction.read("02000000000101f86fd1d0db3ac5a72df968622f31e6b5e6566a09e29206d7c7a55df90e181de800000000171600141fb9623ffd0d422eacc450fd1e967efc477b83ccffffffff0580b2e60e00000000220020fd89acf65485df89797d9ba7ba7a33624ac4452f00db08107f34257d33e5b94680b2e60e0000000017a9146a235d064786b49e7043e4a042d4cc429f7eb6948780b2e60e00000000160014fbb4db9d85fba5e301f4399e3038928e44e37d3280b2e60e0000000017a9147ecd1b519326bc13b0ec716e469b58ed02b112a087f0006bee0000000017a914f856a70093da3a5b5c4302ade033d4c2171705d387024730440220696f6cee2929f1feb3fd6adf024ca0f9aa2f4920ed6d35fb9ec5b78c8408475302201641afae11242160101c6f9932aeb4fcd1f13a9c6df5d1386def000ea259a35001210381d7d5b1bc0d7600565d827242576d9cb793bfe0754334af82289ee8b65d137600000000") + val initiatorInput = TxAddInput(channelId, UInt64(20), parentTx, 0, 4294967293L) + val initiatorOutput = TxAddOutput(channelId, UInt64(30), 49999845 sat, hex"00141ca1cca8855bad6bc1ea5436edd8cff10b7e448b") + val sharedOutput = TxAddOutput(channelId, UInt64(44), 400000000 sat, hex"0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5") + val nonInitiatorInput = TxAddInput(channelId, UInt64(11), parentTx, 2, 4294967293L) + val nonInitiatorOutput = TxAddOutput(channelId, UInt64(33), 49999900 sat, hex"001444cb0c39f93ecc372b5851725bd29d865d333b10") + + val initiatorParams = InteractiveTxParams(channelId, isInitiator = true, 200_000_000 sat, 200_000_000 sat, hex"0020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec5", 120, 330 sat, FeeratePerKw(253 sat)) + val initiatorTx = SharedTransaction(List(initiatorInput), List(nonInitiatorInput).map(i => RemoteTxAddInput(i)), List(initiatorOutput, sharedOutput), List(nonInitiatorOutput).map(o => RemoteTxAddOutput(o)), lockTime = 120) + assert(initiatorTx.localFees(initiatorParams) == 155.sat) + val nonInitiatorParams = initiatorParams.copy(isInitiator = false) + val nonInitiatorTx = SharedTransaction(List(nonInitiatorInput), List(initiatorInput).map(i => RemoteTxAddInput(i)), List(nonInitiatorOutput), List(initiatorOutput, sharedOutput).map(o => RemoteTxAddOutput(o)), lockTime = 120) + assert(nonInitiatorTx.localFees(nonInitiatorParams) == 100.sat) + + val unsignedTx = Transaction.read("0200000002b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b100084d71700000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec578000000") + assert(initiatorTx.buildUnsignedTx().txid == unsignedTx.txid) + assert(nonInitiatorTx.buildUnsignedTx().txid == unsignedTx.txid) + + val initiatorSigs = TxSignatures(channelId, unsignedTx.txid, Seq(ScriptWitness(Seq(hex"68656c6c6f2074686572652c2074686973206973206120626974636f6e212121", hex"82012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff87")))) + val nonInitiatorSigs = TxSignatures(channelId, unsignedTx.txid, Seq(ScriptWitness(Seq(hex"304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d01", hex"034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484")))) + val initiatorSignedTx = FullySignedSharedTransaction(initiatorTx, initiatorSigs, nonInitiatorSigs) + assert(initiatorSignedTx.feerate == FeeratePerKw(262 sat)) + val nonInitiatorSignedTx = FullySignedSharedTransaction(nonInitiatorTx, nonInitiatorSigs, initiatorSigs) + assert(nonInitiatorSignedTx.feerate == FeeratePerKw(262 sat)) + val signedTx = Transaction.read("02000000000102b932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430200000000fdffffffb932b0669cd0394d0d5bcc27e01ab8c511f1662a6799925b346c0cf18fca03430000000000fdffffff03e5effa02000000001600141ca1cca8855bad6bc1ea5436edd8cff10b7e448b1cf0fa020000000016001444cb0c39f93ecc372b5851725bd29d865d333b100084d71700000000220020297b92c238163e820b82486084634b4846b86a3c658d87b9384192e6bea98ec50247304402207de9ba56bb9f641372e805782575ee840a899e61021c8b1572b3ec1d5b5950e9022069e9ba998915dae193d3c25cb89b5e64370e6a3a7755e7f31cf6d7cbc2a49f6d0121034695f5b7864c580bf11f9f8cb1a94eb336f2ce9ef872d2ae1a90ee276c772484022068656c6c6f2074686572652c2074686973206973206120626974636f6e2121212782012088a820add57dfe5277079d069ca4ad4893c96de91f88ffb981fdc6a2a34d5336c66aff8778000000") + assert(initiatorSignedTx.signedTx == signedTx) + assert(initiatorSignedTx.signedTx == nonInitiatorSignedTx.signedTx) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index f002243f55..93e57d4374 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.channel.states.c +import akka.actor.Status import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -165,6 +166,106 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(bob.stateName == WAIT_FOR_DUAL_FUNDING_READY) } + test("recv WatchFundingConfirmedTriggered (rbf in progress)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + alice2bob.expectMsgType[TxInitRbf] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAckRbf] + + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) + alice2bob.expectMsgType[TxAbort] + alice2bob.expectMsgType[ChannelReady] + probe.expectMsg(Status.Failure(InvalidRbfTxConfirmed(channelId(alice)))) + awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) + } + + test("rbf funding attempt", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution_opt.contains(TestConstants.fundingSatoshis)) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution_opt.contains(TestConstants.nonInitiatorFundingSatoshis)) + bob2alice.forward(alice) + probe.expectMsgType[RES_SUCCESS[CMD_BUMP_FUNDING_FEE]] + + // Alice and Bob build a new version of the funding transaction. + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddOutput] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxAddOutput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxComplete] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxComplete] + alice2bob.forward(bob) + bob2alice.expectMsgType[CommitSig] + bob2alice.forward(alice) + alice2bob.expectMsgType[CommitSig] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxSignatures] + bob2alice.forward(alice) + alice2bob.expectMsgType[TxSignatures] + alice2bob.forward(bob) + + val fundingTx2 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + assert(fundingTx1.signedTx.txid != fundingTx2.signedTx.txid) + assert(fundingTx1.feerate < fundingTx2.feerate) + // The new transaction double-spends previous inputs. + fundingTx1.signedTx.txIn.map(_.outPoint).foreach(o => assert(fundingTx2.signedTx.txIn.exists(_.outPoint == o))) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 1) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.fundingTx == fundingTx1) + } + + test("rbf funding attempt failure", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val probe = TestProbe() + val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution_opt.contains(TestConstants.fundingSatoshis)) + alice2bob.forward(bob) + assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution_opt.contains(TestConstants.nonInitiatorFundingSatoshis)) + bob2alice.forward(alice) + + // Alice and Bob build a new version of the funding transaction. + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + val bobInput = bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice, bobInput.copy(previousTxOutput = 42)) + alice2bob.expectMsgType[TxAbort] + alice2bob.forward(bob) + bob2alice.expectNoMessage(100 millis) + alice2bob.expectNoMessage(100 millis) + + // Alice and Bob clear RBF data from their state. + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfAttempt.isEmpty) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx == fundingTxAlice) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfAttempt.isEmpty) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx == fundingTxBob) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + } + test("recv CurrentBlockCount (funding in progress)", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val fundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index f7a04b0263..af7cb03b8d 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -55,13 +55,17 @@ trait Channel { if (!channelTypeOk) { reject(MalformedFormFieldRejection("channelType", s"Channel type not supported: must be one of ${supportedChannelTypes.keys.mkString(",")}")) } else { - complete { - eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt) - } + complete(eclairApi.open(nodeId, fundingSatoshis, pushMsat, channelType_opt, fundingFeerateSatByte, announceChannel_opt, openTimeout_opt)) } } } + val rbfOpen: Route = postRequest("rbfopen") { implicit f => + formFields(channelIdFormParam, "targetFeerateSatByte".as[FeeratePerByte], "lockTime".as[Long] ?) { + (channelId, targetFeerateSatByte, lockTime_opt) => complete(eclairApi.rbfOpen(channelId, FeeratePerKw(targetFeerateSatByte), lockTime_opt)) + } + } + val close: Route = postRequest("close") { implicit t => withChannelsIdentifier { channels => formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?, "preferredFeerateSatByte".as[FeeratePerByte].?, "minFeerateSatByte".as[FeeratePerByte].?, "maxFeerateSatByte".as[FeeratePerByte].?) { @@ -119,6 +123,6 @@ trait Channel { complete(eclairApi.channelBalances()) } - val channelRoutes: Route = open ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances + val channelRoutes: Route = open ~ rbfOpen ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances } From 4e7d28b90f9bf6602f91e4dd5fcc37ab7c64bc42 Mon Sep 17 00:00:00 2001 From: t-bast Date: Fri, 19 Aug 2022 17:11:40 +0200 Subject: [PATCH 2/6] Add unconfirmed offline closing tests Add more tests for scenarios where an unconfirmed channel is force-closed, where the funding transaction that confirms may not be the last one. --- ...WaitForDualFundingConfirmedStateSpec.scala | 293 ++++++++++++++++-- 1 file changed, 272 insertions(+), 21 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index 93e57d4374..be3d8cec2e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.channel.states.c import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -27,6 +28,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, TestConstants, TestKitBaseClass} @@ -185,28 +187,77 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateName == WAIT_FOR_DUAL_FUNDING_READY) } - test("rbf funding attempt", Tag(ChannelStateTestsTags.DualFunding)) { f => + test("recv WatchFundingConfirmedTriggered after restart", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val fundingTx = aliceData.fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val (alice2, bob2) = restartNodes(f, aliceData, bobData) + reconnectNodes(f, alice2, aliceData, bob2, bobData) + + alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) + alice2bob.expectMsgType[ChannelReady] + awaitCond(alice2.stateName == WAIT_FOR_DUAL_FUNDING_READY) + + bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) + bob2alice.expectMsgType[ChannelReady] + awaitCond(bob2.stateName == WAIT_FOR_DUAL_FUNDING_READY) + } + + test("recv WatchFundingConfirmedTriggered after restart (previous tx)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val fundingTx2 = testBumpFundingFees(f).signedTx + assert(fundingTx1.txid != fundingTx2.txid) + + val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val (alice2, bob2) = restartNodes(f, aliceData, bobData) + reconnectNodes(f, alice2, aliceData, bob2, bobData) + + alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + alice2bob.expectMsgType[ChannelReady] + awaitCond(alice2.stateName == WAIT_FOR_DUAL_FUNDING_READY) + + bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + bob2alice.expectMsgType[ChannelReady] + awaitCond(bob2.stateName == WAIT_FOR_DUAL_FUNDING_READY) + + assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.commitInput.outPoint.txid == fundingTx1.txid) + assert(bob2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.commitInput.outPoint.txid == fundingTx1.txid) + } + + def testBumpFundingFees(f: FixtureParam): FullySignedSharedTransaction = { import f._ val probe = TestProbe() - val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) - alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) + val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs + alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, 0) assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution_opt.contains(TestConstants.fundingSatoshis)) alice2bob.forward(bob) assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution_opt.contains(TestConstants.nonInitiatorFundingSatoshis)) bob2alice.forward(alice) probe.expectMsgType[RES_SUCCESS[CMD_BUMP_FUNDING_FEE]] - // Alice and Bob build a new version of the funding transaction. - alice2bob.expectMsgType[TxAddInput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddInput] - bob2alice.forward(alice) - alice2bob.expectMsgType[TxAddInput] - alice2bob.forward(bob) - bob2alice.expectMsgType[TxAddInput] - bob2alice.forward(alice) + // Alice and Bob build a new version of the funding transaction, with one new input every time. + val inputCount = previousFundingTxs.length + 2 + (1 to inputCount).foreach(_ => { + alice2bob.expectMsgType[TxAddInput] + alice2bob.forward(bob) + bob2alice.expectMsgType[TxAddInput] + bob2alice.forward(alice) + }) alice2bob.expectMsgType[TxAddOutput] alice2bob.forward(bob) bob2alice.expectMsgType[TxAddOutput] @@ -226,13 +277,27 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.expectMsgType[TxSignatures] alice2bob.forward(bob) - val fundingTx2 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] - assert(fundingTx1.signedTx.txid != fundingTx2.signedTx.txid) - assert(fundingTx1.feerate < fundingTx2.feerate) + val nextFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] + assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid) + assert(bobListener.expectMsgType[TransactionPublished].tx.txid == nextFundingTx.signedTx.txid) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == nextFundingTx.signedTx.txid) + assert(currentFundingTx.signedTx.txid != nextFundingTx.signedTx.txid) + assert(currentFundingTx.feerate < nextFundingTx.feerate) // The new transaction double-spends previous inputs. - fundingTx1.signedTx.txIn.map(_.outPoint).foreach(o => assert(fundingTx2.signedTx.txIn.exists(_.outPoint == o))) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 1) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.fundingTx == fundingTx1) + currentFundingTx.signedTx.txIn.map(_.outPoint).foreach(o => assert(nextFundingTx.signedTx.txIn.exists(_.outPoint == o))) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == previousFundingTxs.length + 1) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.head.fundingTx == currentFundingTx) + nextFundingTx + } + + test("rbf funding attempt", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) + testBumpFundingFees(f) + testBumpFundingFees(f) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.length == 2) } test("rbf funding attempt failure", Tag(ChannelStateTestsTags.DualFunding)) { f => @@ -386,8 +451,8 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateName == OFFLINE) // The funding tx confirms while we're offline. alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - alice2blockchain.expectNoMessage(100 millis) // Bob broadcasts his commit tx. val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx alice ! WatchFundingSpentTriggered(bobCommitTx.tx) @@ -395,10 +460,99 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(claimMain.input.txid == bobCommitTx.tx.txid) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.tx.txid) assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - alice2blockchain.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSING) } + test("recv WatchFundingSpentTriggered while offline (previous tx)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + val fundingTx2 = testBumpFundingFees(f).signedTx + assert(fundingTx1.txid != fundingTx2.txid) + val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(bobCommitTx1.txid != bobCommitTx2.txid) + + alice ! INPUT_DISCONNECTED + awaitCond(alice.stateName == OFFLINE) + // A previous funding tx confirms while we're offline. + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + // Bob broadcasts his commit tx. + alice ! WatchFundingSpentTriggered(bobCommitTx1) + val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMain.input.txid == bobCommitTx1.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + awaitCond(alice.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered after restart (remote commit)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val fundingTx = aliceData.fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val (alice2, bob2) = restartNodes(f, aliceData, bobData) + + alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) + alice2 ! WatchFundingSpentTriggered(bobData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx) + val claimMainAlice = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMainAlice.input.txid == bobData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainAlice.tx.txid) + awaitCond(alice2.stateName == CLOSING) + + bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) + assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) + bob2 ! WatchFundingSpentTriggered(aliceData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx) + val claimMainBob = bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMainBob.input.txid == aliceData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceData.commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainBob.tx.txid) + awaitCond(bob2.stateName == CLOSING) + } + + test("recv WatchFundingSpentTriggered after restart (previous tx)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + val fundingTx2 = testBumpFundingFees(f).signedTx + assert(fundingTx1.txid != fundingTx2.txid) + val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(bobCommitTx1.txid != bobCommitTx2.txid) + + val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] + val (alice2, bob2) = restartNodes(f, aliceData, bobData) + + alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + alice2 ! WatchFundingSpentTriggered(bobCommitTx1) + val claimMainAlice = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMainAlice.input.txid == bobCommitTx1.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainAlice.tx.txid) + awaitCond(alice2.stateName == CLOSING) + + bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + bob2 ! WatchFundingSpentTriggered(aliceCommitTx1) + val claimMainBob = bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMainBob.input.txid == aliceCommitTx1.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx1.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainBob.tx.txid) + awaitCond(bob2.stateName == CLOSING) + } + test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -428,6 +582,47 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainRemote.tx.txid) } + test("recv Error (previous tx confirms)", Tag(ChannelStateTestsTags.DualFunding)) { f => + import f._ + + val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx + val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx + assert(aliceCommitTx1.input.outPoint.txid == fundingTx1.txid) + assert(bobCommitTx1.input.outPoint.txid == fundingTx1.txid) + val fundingTx2 = testBumpFundingFees(f).signedTx + assert(fundingTx1.txid != fundingTx2.txid) + val aliceCommitTx2 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx + assert(aliceCommitTx2.input.outPoint.txid == fundingTx2.txid) + + // Alice receives an error and force-closes using the latest funding transaction. + alice ! Error(ByteVector32.Zeroes, "dual funding d34d") + awaitCond(alice.stateName == CLOSING) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx2.tx.txid) + val claimMain2 = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMain2.input.txid == aliceCommitTx2.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx2.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain2.tx.txid) + + // A previous funding transaction confirms, so Alice publishes the corresponding commit tx. + alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) + assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) + assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) + assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx1.tx.txid) + val claimMain1 = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMain1.input.txid == aliceCommitTx1.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx1.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain1.tx.txid) + + // Bob publishes his commit tx, Alice reacts by spending her remote main output. + alice ! WatchFundingSpentTriggered(bobCommitTx1.tx) + val claimMainRemote = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] + assert(claimMainRemote.input.txid == bobCommitTx1.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx1.tx.txid) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainRemote.tx.txid) + assert(alice.stateName == CLOSING) + } + test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag("no-funding-contribution")) { f => import f._ val commitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx @@ -459,4 +654,60 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) } + def restartNodes(f: FixtureParam, aliceData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED, bobData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED): (TestFSMRef[ChannelState, ChannelData, Channel], TestFSMRef[ChannelState, ChannelData, Channel]) = { + import f._ + + val (aliceNodeParams, bobNodeParams) = (alice.underlyingActor.nodeParams, bob.underlyingActor.nodeParams) + val (alicePeer, bobPeer) = (alice.getParent, bob.getParent) + + alice.stop() + bob.stop() + + val alice2 = TestFSMRef(new Channel(aliceNodeParams, wallet, bobNodeParams.nodeId, alice2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer) + alice2 ! INPUT_RESTORED(aliceData) + alice2blockchain.expectMsgType[SetChannelId] + // When restoring, we watch confirmation of all potential funding transactions to detect offline force-closes. + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.commitInput.outPoint.txid) + aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + awaitCond(alice2.stateName == OFFLINE) + + val bob2 = TestFSMRef(new Channel(bobNodeParams, wallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) + bob2 ! INPUT_RESTORED(bobData) + bob2blockchain.expectMsgType[SetChannelId] + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.commitInput.outPoint.txid) + bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + awaitCond(bob2.stateName == OFFLINE) + + alice2.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[TransactionPublished]) + alice2.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[TransactionConfirmed]) + bob2.underlying.system.eventStream.subscribe(bobListener.ref, classOf[TransactionPublished]) + bob2.underlying.system.eventStream.subscribe(bobListener.ref, classOf[TransactionConfirmed]) + + (alice2, bob2) + } + + def reconnectNodes(f: FixtureParam, alice2: TestFSMRef[ChannelState, ChannelData, Channel], aliceData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED, bob2: TestFSMRef[ChannelState, ChannelData, Channel], bobData: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED): Unit = { + import f._ + + val aliceInit = Init(alice2.underlyingActor.nodeParams.features.initFeatures()) + val bobInit = Init(bob2.underlyingActor.nodeParams.features.initFeatures()) + alice2 ! INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit) + val aliceChannelReestablish = alice2bob.expectMsgType[ChannelReestablish] + bob2 ! INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit) + val bobChannelReestablish = bob2alice.expectMsgType[ChannelReestablish] + alice2 ! bobChannelReestablish + // When reconnecting, we watch confirmation again, otherwise if a transaction was confirmed while we were offline, + // we won't be notified again and won't be able to transition to the next state. + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.commitInput.outPoint.txid) + aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + alice2bob.expectMsgType[TxSignatures] + bob2 ! aliceChannelReestablish + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.commitInput.outPoint.txid) + bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + bob2alice.expectMsgType[TxSignatures] + + awaitCond(alice2.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + awaitCond(bob2.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) + } + } From 56be83e50b7e618c333db6d993fb0346aad73d8a Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Aug 2022 11:37:28 +0200 Subject: [PATCH 3/6] Move CMD_BUM_FEE handler --- .../channel/fsm/ChannelOpenDualFunded.scala | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index e05c9c983f..93e36ef5fc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -374,6 +374,28 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } } + case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => + val replyTo = if (cmd.replyTo == ActorRef.noSender) sender() else cmd.replyTo + if (!d.fundingParams.isInitiator) { + replyTo ! Status.Failure(new RuntimeException("cannot initiate rbf: we're not the initiator of this dual-funding attempt")) + stay() + } else { + d.rbfAttempt match { + case Some(_) => + log.warning("cannot initiate rbf, another one is already in progress") + replyTo ! Status.Failure(InvalidRbfAlreadyInProgress(d.channelId)) + stay() + case None => + val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 + if (cmd.targetFeerate < minNextFeerate) { + replyTo ! Status.Failure(InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate)) + stay() + } else { + stay() using d.copy(rbfAttempt = Some(Left(cmd.copy(replyTo = replyTo)))) sending TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.fundingParams.localAmount) + } + } + } + case Event(msg: TxInitRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => if (d.fundingParams.isInitiator) { // Only the initiator is allowed to initiate RBF. @@ -411,28 +433,6 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } } - case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - val replyTo = if (cmd.replyTo == ActorRef.noSender) sender() else cmd.replyTo - if (!d.fundingParams.isInitiator) { - replyTo ! Status.Failure(new RuntimeException("cannot initiate rbf: we're not the initiator of this dual-funding attempt")) - stay() - } else { - d.rbfAttempt match { - case Some(_) => - log.warning("cannot initiate rbf, another one is already in progress") - replyTo ! Status.Failure(InvalidRbfAlreadyInProgress(d.channelId)) - stay() - case None => - val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 - if (cmd.targetFeerate < minNextFeerate) { - replyTo ! Status.Failure(InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate)) - stay() - } else { - stay() using d.copy(rbfAttempt = Some(Left(cmd.copy(replyTo = replyTo)))) sending TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.fundingParams.localAmount) - } - } - } - case Event(msg: TxAckRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.rbfAttempt match { case Some(Left(cmd)) => From ba3baefe8114d5930c8049d530a2224e1b8b104b Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Aug 2022 11:53:28 +0200 Subject: [PATCH 4/6] Small PR comments --- .../eclair/channel/ChannelExceptions.scala | 3 ++- .../eclair/channel/InteractiveTxBuilder.scala | 2 ++ .../channel/fsm/ChannelOpenDualFunded.scala | 18 +++++++++--------- .../channel/fsm/DualFundingHandlers.scala | 2 +- .../wire/protocol/LightningMessageTypes.scala | 6 +++--- .../WaitForDualFundingConfirmedStateSpec.scala | 8 ++++---- 6 files changed, 21 insertions(+), 18 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 fd68d1e9b4..d4c76f791d 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 @@ -67,8 +67,9 @@ 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, tx_opt: Option[Transaction]) extends ChannelException(channelId, s"invalid funding signature: tx=${tx_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 InvalidRbfAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt: the current rbf attempt must be completed or aborted first") +case class RbfAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt: the current rbf attempt must be completed or aborted first") case class InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed") +case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt") case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt") case class NoMoreHtlcsClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new htlcs, closing in progress") case class NoMoreFeeUpdateClosingInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot send new update_fee, closing in progress") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala index a1c0c35721..fe86da88ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/InteractiveTxBuilder.scala @@ -106,6 +106,8 @@ object InteractiveTxBuilder { dustLimit: Satoshi, targetFeerate: FeeratePerKw) { val fundingAmount: Satoshi = localAmount + remoteAmount + // BOLT 2: MUST set `feerate` greater than or equal to 25/24 times the `feerate` of the previously constructed transaction, rounded down. + val minNextFeerate: FeeratePerKw = targetFeerate * 25 / 24 } case class InteractiveTxSession(toSend: Seq[Either[TxAddInput, TxAddOutput]], diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 93e36ef5fc..0807903a39 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -377,16 +377,16 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(cmd: CMD_BUMP_FUNDING_FEE, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => val replyTo = if (cmd.replyTo == ActorRef.noSender) sender() else cmd.replyTo if (!d.fundingParams.isInitiator) { - replyTo ! Status.Failure(new RuntimeException("cannot initiate rbf: we're not the initiator of this dual-funding attempt")) + replyTo ! Status.Failure(InvalidRbfNonInitiator(d.channelId)) stay() } else { d.rbfAttempt match { case Some(_) => log.warning("cannot initiate rbf, another one is already in progress") - replyTo ! Status.Failure(InvalidRbfAlreadyInProgress(d.channelId)) + replyTo ! Status.Failure(RbfAlreadyInProgress(d.channelId)) stay() case None => - val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 + val minNextFeerate = d.fundingParams.minNextFeerate if (cmd.targetFeerate < minNextFeerate) { replyTo ! Status.Failure(InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate)) stay() @@ -400,12 +400,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { if (d.fundingParams.isInitiator) { // Only the initiator is allowed to initiate RBF. log.info("rejecting tx_init_rbf, we're the initiator, not them!") - stay() sending TxAbort(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) + stay() sending Error(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) } else { - val minNextFeerate = d.fundingParams.targetFeerate * 25 / 24 + val minNextFeerate = d.fundingParams.minNextFeerate if (d.rbfAttempt.nonEmpty) { log.info("rejecting rbf attempt: the current rbf attempt must be completed or aborted first") - stay() sending TxAbort(d.channelId, InvalidRbfAlreadyInProgress(d.channelId).getMessage) + stay() sending TxAbort(d.channelId, RbfAlreadyInProgress(d.channelId).getMessage) } else if (msg.feerate < minNextFeerate) { log.info("rejecting rbf attempt: the new feerate must be at least {} (proposed={})", minNextFeerate, msg.feerate) stay() sending TxAbort(d.channelId, InvalidRbfFeerate(d.channelId, msg.feerate, minNextFeerate).getMessage) @@ -415,7 +415,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { d.channelId, d.fundingParams.isInitiator, d.fundingParams.localAmount, // we don't change our funding contribution - msg.fundingContribution_opt.getOrElse(d.fundingParams.remoteAmount), + msg.fundingContribution, d.fundingParams.fundingPubkeyScript, msg.lockTime, d.fundingParams.dustLimit, @@ -436,13 +436,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: TxAckRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.rbfAttempt match { case Some(Left(cmd)) => - log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution_opt.getOrElse(0 sat)) + log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution) cmd.replyTo ! RES_SUCCESS(cmd, d.channelId) val fundingParams = InteractiveTxParams( d.channelId, d.fundingParams.isInitiator, d.fundingParams.localAmount, // we don't change our funding contribution - msg.fundingContribution_opt.getOrElse(d.fundingParams.remoteAmount), + msg.fundingContribution, d.fundingParams.fundingPubkeyScript, cmd.lockTime, d.fundingParams.dustLimit, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 6a04bd799f..9c7e124726 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -84,7 +84,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { def handleNewBlockDualFundingUnconfirmed(c: CurrentBlockHeight, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { // We regularly notify the node operator that they may want to RBF this channel. val blocksSinceOpen = c.blockHeight - d.waitingSince - if (d.fundingParams.isInitiator && (blocksSinceOpen % 288 == 0)) { + if (d.fundingParams.isInitiator && (blocksSinceOpen % 288 == 0)) { // 288 blocks = 2 days context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Info, s"channelId=${d.channelId} is still unconfirmed after $blocksSinceOpen blocks, you may need to use the rbfopen RPC to make it confirm.")) } if (Channel.FUNDING_TIMEOUT_FUNDEE < blocksSinceOpen && Closing.nothingAtStake(d)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 4c82527d7b..8cc0f4d2c8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.wire.protocol import com.google.common.base.Charsets import com.google.common.net.InetAddresses import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, ScriptWitness, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Satoshi, SatoshiLong, ScriptWitness, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ChannelType} import fr.acinq.eclair.payment.relay.Relayer @@ -117,7 +117,7 @@ case class TxInitRbf(channelId: ByteVector32, lockTime: Long, feerate: FeeratePerKw, tlvStream: TlvStream[TxInitRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { - val fundingContribution_opt: Option[Satoshi] = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount) + val fundingContribution: Satoshi = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount).getOrElse(0 sat) } object TxInitRbf { @@ -127,7 +127,7 @@ object TxInitRbf { case class TxAckRbf(channelId: ByteVector32, tlvStream: TlvStream[TxAckRbfTlv] = TlvStream.empty) extends InteractiveTxMessage with HasChannelId { - val fundingContribution_opt: Option[Satoshi] = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount) + val fundingContribution: Satoshi = tlvStream.get[TxRbfTlv.SharedOutputContributionTlv].map(_.amount).getOrElse(0 sat) } object TxAckRbf { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index be3d8cec2e..d5db21b22f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -244,9 +244,9 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val currentFundingTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] val previousFundingTxs = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs alice ! CMD_BUMP_FUNDING_FEE(probe.ref, currentFundingTx.feerate * 1.1, 0) - assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution_opt.contains(TestConstants.fundingSatoshis)) + assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) - assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution_opt.contains(TestConstants.nonInitiatorFundingSatoshis)) + assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) bob2alice.forward(alice) probe.expectMsgType[RES_SUCCESS[CMD_BUMP_FUNDING_FEE]] @@ -307,9 +307,9 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val fundingTxAlice = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] val fundingTxBob = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx.asInstanceOf[FullySignedSharedTransaction] alice ! CMD_BUMP_FUNDING_FEE(probe.ref, TestConstants.feeratePerKw * 1.1, 0) - assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution_opt.contains(TestConstants.fundingSatoshis)) + assert(alice2bob.expectMsgType[TxInitRbf].fundingContribution == TestConstants.fundingSatoshis) alice2bob.forward(bob) - assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution_opt.contains(TestConstants.nonInitiatorFundingSatoshis)) + assert(bob2alice.expectMsgType[TxAckRbf].fundingContribution == TestConstants.nonInitiatorFundingSatoshis) bob2alice.forward(alice) // Alice and Bob build a new version of the funding transaction. From 439c45c9f90fd404359dfe8182157b82dd6fa2c9 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Aug 2022 12:32:38 +0200 Subject: [PATCH 5/6] Refactor RbfStatus into a trait --- .../fr/acinq/eclair/channel/ChannelData.scala | 9 ++- .../channel/fsm/ChannelOpenDualFunded.scala | 73 +++++++++---------- .../channel/version3/ChannelCodecs3.scala | 2 +- ...WaitForDualFundingConfirmedStateSpec.scala | 4 +- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 44b6fb9cbc..68ead846d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -415,6 +415,13 @@ case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction) ext /** Once a dual funding tx has been signed, we must remember the associated commitments. */ case class DualFundingTx(fundingTx: SignedSharedTransaction, commitments: Commitments) +sealed trait RbfStatus +object RbfStatus { + case object NoRbf extends RbfStatus + case class RbfRequested(cmd: CMD_BUMP_FUNDING_FEE) extends RbfStatus + case class RbfInProgress(rbf: typed.ActorRef[InteractiveTxBuilder.Command]) extends RbfStatus +} + sealed trait ChannelData extends PossiblyHarmful { def channelId: ByteVector32 } @@ -497,7 +504,7 @@ final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments, previousFundingTxs: List[DualFundingTx], waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm lastChecked: BlockHeight, // last time we checked if the channel was double-spent - rbfAttempt: Option[Either[CMD_BUMP_FUNDING_FEE, typed.ActorRef[InteractiveTxBuilder.Command]]], + rbfStatus: RbfStatus, deferred: Option[ChannelReady]) extends PersistentChannelData final case class DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments: Commitments, shortIds: ShortIds, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 0807903a39..4f18109b4f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -307,7 +307,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams) match { case Some(fundingMinDepth) => blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth) - val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, None, None) + val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, RbfStatus.NoRbf, None) fundingTx match { case fundingTx: PartiallySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending fundingTx.localSigs case fundingTx: FullySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending fundingTx.localSigs calling publishFundingTx(fundingParams, fundingTx) @@ -363,8 +363,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() using d1 storing() calling publishFundingTx(d.fundingParams, fundingTx) } case _: FullySignedSharedTransaction => - d.rbfAttempt match { - case Some(Right(txBuilder)) => + d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => txBuilder ! InteractiveTxBuilder.ReceiveTxSigs(txSigs) stay() case _ => @@ -380,19 +380,18 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { replyTo ! Status.Failure(InvalidRbfNonInitiator(d.channelId)) stay() } else { - d.rbfAttempt match { - case Some(_) => - log.warning("cannot initiate rbf, another one is already in progress") - replyTo ! Status.Failure(RbfAlreadyInProgress(d.channelId)) - stay() - case None => - val minNextFeerate = d.fundingParams.minNextFeerate + d.rbfStatus match { + case RbfStatus.NoRbf => val minNextFeerate = d.fundingParams.minNextFeerate if (cmd.targetFeerate < minNextFeerate) { replyTo ! Status.Failure(InvalidRbfFeerate(d.channelId, cmd.targetFeerate, minNextFeerate)) stay() } else { - stay() using d.copy(rbfAttempt = Some(Left(cmd.copy(replyTo = replyTo)))) sending TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.fundingParams.localAmount) + stay() using d.copy(rbfStatus = RbfStatus.RbfRequested(cmd.copy(replyTo = replyTo))) sending TxInitRbf(d.channelId, cmd.lockTime, cmd.targetFeerate, d.fundingParams.localAmount) } + case _ => + log.warning("cannot initiate rbf, another one is already in progress") + replyTo ! Status.Failure(RbfAlreadyInProgress(d.channelId)) + stay() } } @@ -403,7 +402,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() sending Error(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) } else { val minNextFeerate = d.fundingParams.minNextFeerate - if (d.rbfAttempt.nonEmpty) { + if (d.rbfStatus != RbfStatus.NoRbf) { log.info("rejecting rbf attempt: the current rbf attempt must be completed or aborted first") stay() sending TxAbort(d.channelId, RbfAlreadyInProgress(d.channelId).getMessage) } else if (msg.feerate < minNextFeerate) { @@ -429,13 +428,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { d.commitments.channelFlags, d.commitments.channelConfig, d.commitments.channelFeatures, wallet)) txBuilder ! InteractiveTxBuilder.Start(self, d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)) - stay() using d.copy(rbfAttempt = Some(Right(txBuilder))) sending TxAckRbf(d.channelId, fundingParams.localAmount) + stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(txBuilder)) sending TxAckRbf(d.channelId, fundingParams.localAmount) } } case Event(msg: TxAckRbf, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - d.rbfAttempt match { - case Some(Left(cmd)) => + d.rbfStatus match { + case RbfStatus.RbfRequested(cmd) => log.info("our peer accepted our rbf attempt and will contribute {} to the funding transaction", msg.fundingContribution) cmd.replyTo ! RES_SUCCESS(cmd, d.channelId) val fundingParams = InteractiveTxParams( @@ -456,15 +455,15 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { d.commitments.channelFlags, d.commitments.channelConfig, d.commitments.channelFeatures, wallet)) txBuilder ! InteractiveTxBuilder.Start(self, d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)) - stay() using d.copy(rbfAttempt = Some(Right(txBuilder))) + stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(txBuilder)) case _ => log.info("ignoring unexpected tx_ack_rbf") stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) } case Event(msg: InteractiveTxConstructionMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - d.rbfAttempt match { - case Some(Right(txBuilder)) => + d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => txBuilder ! InteractiveTxBuilder.ReceiveTxMessage(msg) stay() case _ => @@ -473,8 +472,8 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case Event(commitSig: CommitSig, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - d.rbfAttempt match { - case Some(Right(txBuilder)) => + d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => txBuilder ! InteractiveTxBuilder.ReceiveCommitSig(commitSig) stay() case _ => @@ -483,16 +482,16 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case Event(msg: TxAbort, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - d.rbfAttempt match { - case Some(Right(txBuilder)) => + d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => log.info("our peer aborted the rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) txBuilder ! InteractiveTxBuilder.Abort - stay() using d.copy(rbfAttempt = None) - case Some(Left(cmd)) => + stay() using d.copy(rbfStatus = RbfStatus.NoRbf) + case RbfStatus.RbfRequested(cmd) => log.info("our peer rejected our rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) cmd.replyTo ! Status.Failure(new RuntimeException(s"rbf attempt rejected by our peer: ${msg.toAscii}")) - stay() using d.copy(rbfAttempt = None) - case None => + stay() using d.copy(rbfStatus = RbfStatus.NoRbf) + case RbfStatus.NoRbf => log.info("ignoring unexpected tx_abort message") stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) } @@ -504,14 +503,14 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val fundingMinDepth = Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams).getOrElse(nodeParams.channelConf.minDepthBlocks.toLong) blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth) val previousFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs - val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, previousFundingTxs, d.waitingSince, d.lastChecked, None, d.deferred) + val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, previousFundingTxs, d.waitingSince, d.lastChecked, RbfStatus.NoRbf, d.deferred) fundingTx match { case fundingTx: PartiallySignedSharedTransaction => stay() using d1 storing() sending fundingTx.localSigs case fundingTx: FullySignedSharedTransaction => stay() using d1 storing() sending fundingTx.localSigs calling publishFundingTx(fundingParams, fundingTx) } case f: InteractiveTxBuilder.Failed => log.info("rbf attempt failed: {}", f.cause.getMessage) - stay() using d.copy(rbfAttempt = None) sending TxAbort(d.channelId, f.cause.getMessage) + stay() using d.copy(rbfStatus = RbfStatus.NoRbf) sending TxAbort(d.channelId, f.cause.getMessage) } case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, confirmedTx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => @@ -527,14 +526,14 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { d.deferred.foreach(self ! _) val otherFundingTxs = allFundingTxs.filter(_.commitments.commitInput.outPoint.txid != confirmedTx.txid).map(_.fundingTx) rollbackDualFundingTxs(otherFundingTxs) - val toSend = d.rbfAttempt match { - case Some(Right(txBuilder)) => + val toSend = d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => txBuilder ! InteractiveTxBuilder.Abort Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady) - case Some(Left(cmd)) => + case RbfStatus.RbfRequested(cmd) => cmd.replyTo ! Status.Failure(InvalidRbfTxConfirmed(d.channelId)) Seq(TxAbort(d.channelId, InvalidRbfTxConfirmed(d.channelId).getMessage), channelReady) - case _ => + case RbfStatus.NoRbf => Seq(channelReady) } goto(WAIT_FOR_DUAL_FUNDING_READY) using DATA_WAIT_FOR_DUAL_FUNDING_READY(commitments, shortIds, channelReady) storing() sending toSend @@ -572,12 +571,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { stay() case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => - d.rbfAttempt match { - case Some(Right(txBuilder)) => txBuilder ! InteractiveTxBuilder.Abort - case Some(Left(cmd)) => cmd.replyTo ! Status.Failure(new RuntimeException("rbf attempt failed: disconnected")) - case None => // nothing to do + d.rbfStatus match { + case RbfStatus.RbfInProgress(txBuilder) => txBuilder ! InteractiveTxBuilder.Abort + case RbfStatus.RbfRequested(cmd) => cmd.replyTo ! Status.Failure(new RuntimeException("rbf attempt failed: disconnected")) + case RbfStatus.NoRbf => // nothing to do } - goto(OFFLINE) using d.copy(rbfAttempt = None) + goto(OFFLINE) using d.copy(rbfStatus = RbfStatus.NoRbf) case Event(e: Error, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => handleRemoteError(e, d) }) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 9139cf3082..754e8dfe6f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -390,7 +390,7 @@ private[channel] object ChannelCodecs3 { ("previousFundingTxs" | listOfN(uint16, dualFundingTxCodec)) :: ("waitingSince" | blockHeight) :: ("lastChecked" | blockHeight) :: - ("rbfAttempt" | provide(Option.empty[Either[CMD_BUMP_FUNDING_FEE, typed.ActorRef[InteractiveTxBuilder.Command]]])) :: + ("rbfStatus" | provide[RbfStatus](RbfStatus.NoRbf)) :: ("deferred" | optional(bool8, lengthDelimited(channelReadyCodec)))).as[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val DATA_WAIT_FOR_DUAL_FUNDING_READY_0c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_READY] = ( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index d5db21b22f..6e1bafb245 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -323,10 +323,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2bob.expectNoMessage(100 millis) // Alice and Bob clear RBF data from their state. - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfAttempt.isEmpty) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfStatus == RbfStatus.NoRbf) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx == fundingTxAlice) assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) - assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfAttempt.isEmpty) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].rbfStatus == RbfStatus.NoRbf) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].fundingTx == fundingTxBob) assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].previousFundingTxs.isEmpty) } From 135870bfb2f1660d74ab3eac7a07475b121651bb Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 22 Aug 2022 13:59:53 +0200 Subject: [PATCH 6/6] More PR comments --- .../eclair/channel/ChannelExceptions.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 2 ++ .../fr/acinq/eclair/channel/fsm/Channel.scala | 30 +++++++++---------- .../channel/fsm/ChannelOpenDualFunded.scala | 14 ++++----- .../channel/fsm/ChannelOpenSingleFunded.scala | 10 +++---- .../channel/fsm/CommonFundingHandlers.scala | 4 +-- .../channel/fsm/DualFundingHandlers.scala | 10 +++---- .../eclair/channel/fsm/ErrorHandlers.scala | 6 ++-- .../fr/acinq/eclair/router/Validation.scala | 2 +- ...WaitForDualFundingConfirmedStateSpec.scala | 20 ++++++------- .../basic/fixtures/MinimalNodeFixture.scala | 4 +-- 11 files changed, 53 insertions(+), 51 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 d4c76f791d..6d8f511597 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 @@ -67,7 +67,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, tx_opt: Option[Transaction]) extends ChannelException(channelId, s"invalid funding signature: tx=${tx_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 RbfAlreadyInProgress (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt: the current rbf attempt must be completed or aborted first") +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 InvalidRbfTxConfirmed (override val channelId: ByteVector32) extends ChannelException(channelId, "no need to rbf, transaction is already confirmed") case class InvalidRbfNonInitiator (override val channelId: ByteVector32) extends ChannelException(channelId, "cannot initiate rbf: we're not the initiator of this interactive-tx attempt") case class InvalidRbfAttempt (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid rbf attempt") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 23091ac163..fbf270960c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -199,6 +199,8 @@ case class Commitments(channelId: ByteVector32, commitTx } + val fundingTxId: ByteVector32 = commitInput.outPoint.txid + val commitmentFormat: CommitmentFormat = channelFeatures.commitmentFormat val channelType: SupportedChannelType = channelFeatures.channelType 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 24371220e8..0bb17af97e 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 @@ -263,8 +263,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (closing.alternativeCommitments.nonEmpty) { // There are unconfirmed, alternative funding transactions, so we wait for one to confirm before // watching transactions spending it. - blockchain ! WatchFundingConfirmed(self, data.commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - closing.alternativeCommitments.foreach(c => blockchain ! WatchFundingConfirmed(self, c.commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks)) + blockchain ! WatchFundingConfirmed(self, data.commitments.fundingTxId, nodeParams.channelConf.minDepthBlocks) + closing.alternativeCommitments.foreach(c => blockchain ! WatchFundingConfirmed(self, c.commitments.fundingTxId, nodeParams.channelConf.minDepthBlocks)) } closing.mutualClosePublished.foreach(mcp => doPublish(mcp, isInitiator)) closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments)) @@ -278,7 +278,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val if (closing.commitments.channelFeatures.hasFeature(Features.DualFunding)) { closing.fundingTx.flatMap(_.signedTx_opt).foreach(tx => wallet.publishTransaction(tx)) } else { - blockchain ! GetTxWithMeta(self, closing.commitments.commitInput.outPoint.txid) + blockchain ! GetTxWithMeta(self, closing.commitments.fundingTxId) } } } @@ -312,7 +312,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case funding: DATA_WAIT_FOR_FUNDING_CONFIRMED => watchFundingTx(funding.commitments) // we make sure that the funding tx has been published - blockchain ! GetTxWithMeta(self, funding.commitments.commitInput.outPoint.txid) + blockchain ! GetTxWithMeta(self, funding.commitments.fundingTxId) if (funding.waitingSince.toLong > 1_500_000_000) { // we were using timestamps instead of block heights when the channel was created: we reset it *and* we use block heights goto(OFFLINE) using funding.copy(waitingSince = nodeParams.currentBlockHeight) storing() @@ -324,8 +324,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val // we make sure that the funding tx with the highest feerate has been published publishFundingTx(funding.fundingParams, funding.fundingTx) // we watch confirmation of all funding candidates, and once one of them confirms we will watch spending txs - blockchain ! WatchFundingConfirmed(self, funding.commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) - funding.previousFundingTxs.map(_.commitments).foreach(c => blockchain ! WatchFundingConfirmed(self, c.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks)) + blockchain ! WatchFundingConfirmed(self, funding.commitments.fundingTxId, nodeParams.channelConf.minDepthBlocks) + funding.previousFundingTxs.map(_.commitments).foreach(c => blockchain ! WatchFundingConfirmed(self, c.fundingTxId, nodeParams.channelConf.minDepthBlocks)) goto(OFFLINE) using funding case _ => @@ -1082,7 +1082,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Left(cause) => handleCommandError(cause, c) } - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_CLOSING) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_CLOSING) if getTxResponse.txid == d.commitments.fundingTxId => // NB: waitingSinceBlock contains the block at which closing was initiated, not the block at which funding was initiated. // That means we're lenient with our peer and give its funding tx more time to confirm, to avoid having to store two distinct // waitingSinceBlock (e.g. closingWaitingSinceBlock and fundingWaitingSinceBlock). @@ -1093,7 +1093,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(BITCOIN_FUNDING_TIMEOUT, d: DATA_CLOSING) => handleFundingTimeout(d) case Event(w: WatchFundingConfirmedTriggered, d: DATA_CLOSING) => - d.alternativeCommitments.find(_.commitments.commitInput.outPoint.txid == w.tx.txid) match { + d.alternativeCommitments.find(_.commitments.fundingTxId == w.tx.txid) match { case Some(DualFundingTx(_, commitments1)) => // This is a corner case where: // - we are using dual funding @@ -1112,14 +1112,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) val otherFundingTxs = d.fundingTx.toSeq.collect { case DualFundedUnconfirmedFundingTx(fundingTx) => fundingTx } ++ - d.alternativeCommitments.filterNot(_.commitments.commitInput.outPoint.txid == w.tx.txid).map(_.fundingTx) + d.alternativeCommitments.filterNot(_.commitments.fundingTxId == w.tx.txid).map(_.fundingTx) rollbackDualFundingTxs(otherFundingTxs) val commitTx = commitments1.fullySignedLocalCommitTx(keyManager).tx val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(keyManager, commitments1, commitTx, nodeParams.currentBlockHeight, nodeParams.onChainFeeConf) val d1 = DATA_CLOSING(commitments1, None, d.waitingSince, alternativeCommitments = Nil, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) stay() using d1 storing() calling doPublish(localCommitPublished, commitments1) case None => - if (d.commitments.commitInput.outPoint.txid == w.tx.txid) { + if (d.commitments.fundingTxId == w.tx.txid) { // The best funding tx candidate has been confirmed, we can forget alternative commitments. stay() using d.copy(alternativeCommitments = Nil) storing() } else { @@ -1327,7 +1327,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => handleUpdateRelayFeeDisconnected(c, d) - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.fundingTxId => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) @@ -1371,7 +1371,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val defaultMinDepth.toLong } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth) + blockchain ! WatchFundingConfirmed(self, d.commitments.fundingTxId, minDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => @@ -1383,7 +1383,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val log.warning("min_depth should be defined since we're waiting for the funding tx to confirm, using default minDepth={}", defaultMinDepth) defaultMinDepth.toLong } - (d.commitments +: d.previousFundingTxs.map(_.commitments)).foreach(commitments => blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, minDepth)) + (d.commitments +: d.previousFundingTxs.map(_.commitments)).foreach(commitments => blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, minDepth)) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.fundingTx.localSigs case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) => @@ -1449,7 +1449,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case _ => // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been // fired while we were in OFFLINE (if not, the operation is idempotent anyway) - blockchain ! WatchFundingDeeplyBuried(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + blockchain ! WatchFundingDeeplyBuried(self, d.commitments.fundingTxId, ANNOUNCEMENTS_MINCONF) } if (d.commitments.announceChannel) { @@ -1529,7 +1529,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val case Event(c: CurrentFeerates, d: PersistentChannelData) => handleCurrentFeerateDisconnected(c, d) - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.fundingTxId => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 4f18109b4f..e9dc8236b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -306,7 +306,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { d.deferred.foreach(self ! _) Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams) match { case Some(fundingMinDepth) => - blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth) + blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth) val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, Nil, nodeParams.currentBlockHeight, nodeParams.currentBlockHeight, RbfStatus.NoRbf, None) fundingTx match { case fundingTx: PartiallySignedSharedTransaction => goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) using d1 storing() sending fundingTx.localSigs @@ -390,7 +390,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case _ => log.warning("cannot initiate rbf, another one is already in progress") - replyTo ! Status.Failure(RbfAlreadyInProgress(d.channelId)) + replyTo ! Status.Failure(InvalidRbfAlreadyInProgress(d.channelId)) stay() } } @@ -399,12 +399,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { if (d.fundingParams.isInitiator) { // Only the initiator is allowed to initiate RBF. log.info("rejecting tx_init_rbf, we're the initiator, not them!") - stay() sending Error(d.channelId, InvalidRbfAttempt(d.channelId).getMessage) + stay() sending TxAbort(d.channelId, InvalidRbfNonInitiator(d.channelId).getMessage) } else { val minNextFeerate = d.fundingParams.minNextFeerate if (d.rbfStatus != RbfStatus.NoRbf) { log.info("rejecting rbf attempt: the current rbf attempt must be completed or aborted first") - stay() sending TxAbort(d.channelId, RbfAlreadyInProgress(d.channelId).getMessage) + stay() sending TxAbort(d.channelId, InvalidRbfAlreadyInProgress(d.channelId).getMessage) } else if (msg.feerate < minNextFeerate) { log.info("rejecting rbf attempt: the new feerate must be at least {} (proposed={})", minNextFeerate, msg.feerate) stay() sending TxAbort(d.channelId, InvalidRbfFeerate(d.channelId, msg.feerate, minNextFeerate).getMessage) @@ -501,7 +501,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case InteractiveTxBuilder.Succeeded(fundingParams, fundingTx, commitments) => // We now have more than one version of the funding tx, so we cannot use zero-conf. val fundingMinDepth = Funding.minDepthDualFunding(nodeParams.channelConf, commitments.channelFeatures, fundingParams).getOrElse(nodeParams.channelConf.minDepthBlocks.toLong) - blockchain ! WatchFundingConfirmed(self, commitments.commitInput.outPoint.txid, fundingMinDepth) + blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth) val previousFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs val d1 = DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments, fundingTx, fundingParams, previousFundingTxs, d.waitingSince, d.lastChecked, RbfStatus.NoRbf, d.deferred) fundingTx match { @@ -516,7 +516,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, confirmedTx), d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => // We find which funding transaction got confirmed. val allFundingTxs = DualFundingTx(d.fundingTx, d.commitments) +: d.previousFundingTxs - allFundingTxs.find(_.commitments.commitInput.outPoint.txid == confirmedTx.txid) match { + allFundingTxs.find(_.commitments.fundingTxId == confirmedTx.txid) match { case Some(DualFundingTx(_, commitments)) => log.info("channelId={} was confirmed at blockHeight={} txIndex={} with funding txid={}", d.channelId, blockHeight, txIndex, confirmedTx.txid) watchFundingTx(commitments) @@ -524,7 +524,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { val realScidStatus = RealScidStatus.Temporary(RealShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt)) val (shortIds, channelReady) = acceptFundingTx(commitments, realScidStatus = realScidStatus) d.deferred.foreach(self ! _) - val otherFundingTxs = allFundingTxs.filter(_.commitments.commitInput.outPoint.txid != confirmedTx.txid).map(_.fundingTx) + val otherFundingTxs = allFundingTxs.filter(_.commitments.fundingTxId != confirmedTx.txid).map(_.fundingTx) rollbackDualFundingTxs(otherFundingTxs) val toSend = d.rbfStatus match { case RbfStatus.RbfInProgress(txBuilder) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 8b641db369..36fa86bb5c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -290,11 +290,11 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) // NB: we don't send a ChannelSignatureSent for the first commit - log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitments.fundingTxId}") watchFundingTx(commitments) Funding.minDepthFundee(nodeParams.channelConf, commitments.channelFeatures, fundingAmount) match { case Some(fundingMinDepth) => - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned case None => val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) @@ -335,13 +335,13 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { commitInput, ShaChain.init) val blockHeight = nodeParams.currentBlockHeight context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) - log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") + log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitments.fundingTxId}") watchFundingTx(commitments) // we will publish the funding tx only after the channel state has been written to disk because we want to // make sure we first persist the commitment that returns back the funds to us in case of problem Funding.minDepthFunder(commitments.channelFeatures) match { case Some(fundingMinDepth) => - blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) + blockchain ! WatchFundingConfirmed(self, commitments.fundingTxId, fundingMinDepth) goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), blockHeight, None, Left(fundingCreated)) storing() calling publishFundingTx(commitments, fundingTx, fundingTxFee) case None => val (shortIds, channelReady) = acceptFundingTx(commitments, RealScidStatus.Unknown) @@ -407,7 +407,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { delayEarlyAnnouncementSigs(remoteAnnSigs) stay() - case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.commitInput.outPoint.txid => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) + case Event(getTxResponse: GetTxWithMetaResponse, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) if getTxResponse.txid == d.commitments.fundingTxId => handleGetFundingTx(getTxResponse, d.waitingSince, d.fundingTx) case Event(BITCOIN_FUNDING_PUBLISH_FAILED, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => handleFundingPublishFailed(d) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index ebf72ddb58..5fd658737f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -44,7 +44,7 @@ trait CommonFundingHandlers extends CommonHandlers { } def acceptFundingTx(commitments: Commitments, realScidStatus: RealScidStatus): (ShortIds, ChannelReady) = { - blockchain ! WatchFundingLost(self, commitments.commitInput.outPoint.txid, nodeParams.channelConf.minDepthBlocks) + blockchain ! WatchFundingLost(self, commitments.fundingTxId, nodeParams.channelConf.minDepthBlocks) // the alias will use in our peer's channel_update message, the goal is to be able to use our channel as soon // as it reaches NORMAL state, and before it is announced on the network val shortIds = ShortIds(realScidStatus, ShortChannelId.generateLocalAlias(), remoteAlias_opt = None) @@ -75,7 +75,7 @@ trait CommonFundingHandlers extends CommonHandlers { // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) + blockchain ! WatchFundingDeeplyBuried(self, commitments.fundingTxId, ANNOUNCEMENTS_MINCONF) DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)), shortIds1, None, initialChannelUpdate, None, None, None) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala index 9c7e124726..6b0535c4cf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/DualFundingHandlers.scala @@ -60,19 +60,19 @@ trait DualFundingHandlers extends CommonFundingHandlers { } def handleDualFundingConfirmedOffline(w: WatchFundingConfirmedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { - if (w.tx.txid == d.commitments.commitInput.outPoint.txid) { + if (w.tx.txid == d.commitments.fundingTxId) { watchFundingTx(d.commitments) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) // We can forget previous funding attempts now that the funding tx is confirmed. rollbackDualFundingTxs(d.previousFundingTxs.map(_.fundingTx)) stay() using d.copy(previousFundingTxs = Nil) storing() - } else if (d.previousFundingTxs.exists(_.commitments.commitInput.outPoint.txid == w.tx.txid)) { + } else if (d.previousFundingTxs.exists(_.commitments.fundingTxId == w.tx.txid)) { log.info("channelId={} was confirmed at blockHeight={} txIndex={} with a previous funding txid={}", d.channelId, w.blockHeight, w.txIndex, w.tx.txid) - val confirmed = d.previousFundingTxs.find(_.commitments.commitInput.outPoint.txid == w.tx.txid).get + val confirmed = d.previousFundingTxs.find(_.commitments.fundingTxId == w.tx.txid).get watchFundingTx(confirmed.commitments) context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) // We can forget other funding attempts now that one of the funding txs is confirmed. - val otherFundingTxs = d.fundingTx +: d.previousFundingTxs.filter(_.commitments.commitInput.outPoint.txid != w.tx.txid).map(_.fundingTx) + val otherFundingTxs = d.fundingTx +: d.previousFundingTxs.filter(_.commitments.fundingTxId != w.tx.txid).map(_.fundingTx) rollbackDualFundingTxs(otherFundingTxs) stay() using d.copy(commitments = confirmed.commitments, fundingTx = confirmed.fundingTx, previousFundingTxs = Nil) storing() } else { @@ -107,7 +107,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { } def handleDualFundingDoubleSpent(e: BITCOIN_FUNDING_DOUBLE_SPENT, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) = { - val fundingTxIds = (d.commitments +: d.previousFundingTxs.map(_.commitments)).map(_.commitInput.outPoint.txid).toSet + val fundingTxIds = (d.commitments +: d.previousFundingTxs.map(_.commitments)).map(_.fundingTxId).toSet if (fundingTxIds.subsetOf(e.fundingTxIds)) { log.warning("{} funding attempts have been double-spent, forgetting channel", fundingTxIds.size) (d.fundingTx +: d.previousFundingTxs.map(_.fundingTx)).foreach(tx => wallet.rollback(tx.tx.buildUnsignedTx())) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala index 0150e29e21..4261532cbd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -318,7 +318,7 @@ trait ErrorHandlers extends CommonHandlers { case None => // the published tx was neither their current commitment nor a revoked one log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.fundingTxId} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) goto(ERR_INFORMATION_LEAK) } } @@ -342,8 +342,8 @@ trait ErrorHandlers extends CommonHandlers { def handleInformationLeak(tx: Transaction, d: PersistentChannelData) = { // this is never supposed to happen !! - log.error(s"our funding tx ${d.commitments.commitInput.outPoint.txid} was spent by txid=${tx.txid}!!") - context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.commitInput.outPoint.txid} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) + log.error(s"our funding tx ${d.commitments.fundingTxId} was spent by txid=${tx.txid}!!") + context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"funding tx ${d.commitments.fundingTxId} of channel ${d.channelId} was spent by an unknown transaction, indicating that your DB has lost data or your node has been breached: please contact the dev team.")) val exc = FundingTxSpent(d.channelId, tx) val error = Error(d.channelId, exc.getMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 254829550e..ce626af320 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -504,7 +504,7 @@ object Validation { // since this is a local channel, we can trust the announcement, no need to go through the full // verification process and make calls to bitcoin core val fundingTxId = lcu.commitments match { - case commitments: Commitments => commitments.commitInput.outPoint.txid + case commitments: Commitments => commitments.fundingTxId case _ => ByteVector32.Zeroes } val d1 = addPublicChannel(d, nodeParams, watcher, ann, fundingTxId, lcu.commitments.capacity, Some(privateChannel)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index 6e1bafb245..b94e979448 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -233,8 +233,8 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2alice.expectMsgType[ChannelReady] awaitCond(bob2.stateName == WAIT_FOR_DUAL_FUNDING_READY) - assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.commitInput.outPoint.txid == fundingTx1.txid) - assert(bob2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.commitInput.outPoint.txid == fundingTx1.txid) + assert(alice2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.fundingTxId == fundingTx1.txid) + assert(bob2.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.fundingTxId == fundingTx1.txid) } def testBumpFundingFees(f: FixtureParam): FullySignedSharedTransaction = { @@ -667,15 +667,15 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2 ! INPUT_RESTORED(aliceData) alice2blockchain.expectMsgType[SetChannelId] // When restoring, we watch confirmation of all potential funding transactions to detect offline force-closes. - assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.commitInput.outPoint.txid) - aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.fundingTxId) + aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.fundingTxId) awaitCond(alice2.stateName == OFFLINE) val bob2 = TestFSMRef(new Channel(bobNodeParams, wallet, aliceNodeParams.nodeId, bob2blockchain.ref, TestProbe().ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer) bob2 ! INPUT_RESTORED(bobData) bob2blockchain.expectMsgType[SetChannelId] - assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.commitInput.outPoint.txid) - bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.fundingTxId) + bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.fundingTxId) awaitCond(bob2.stateName == OFFLINE) alice2.underlying.system.eventStream.subscribe(aliceListener.ref, classOf[TransactionPublished]) @@ -698,12 +698,12 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2 ! bobChannelReestablish // When reconnecting, we watch confirmation again, otherwise if a transaction was confirmed while we were offline, // we won't be notified again and won't be able to transition to the next state. - assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.commitInput.outPoint.txid) - aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + assert(alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == aliceData.commitments.fundingTxId) + aliceData.previousFundingTxs.foreach(f => alice2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.fundingTxId) alice2bob.expectMsgType[TxSignatures] bob2 ! aliceChannelReestablish - assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.commitInput.outPoint.txid) - bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.commitInput.outPoint.txid) + assert(bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == bobData.commitments.fundingTxId) + bobData.previousFundingTxs.foreach(f => bob2blockchain.expectMsgType[WatchFundingConfirmed].txId == f.commitments.fundingTxId) bob2alice.expectMsgType[TxSignatures] awaitCond(alice2.stateName == WAIT_FOR_DUAL_FUNDING_CONFIRMED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala index 4025b3f2fc..20bab2e360 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/MinimalNodeFixture.scala @@ -165,7 +165,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat } def fundingTx(node: MinimalNodeFixture, channelId: ByteVector32)(implicit system: ActorSystem): Transaction = { - val fundingTxid = getChannelData(node, channelId).asInstanceOf[PersistentChannelData].commitments.commitInput.outPoint.txid + val fundingTxid = getChannelData(node, channelId).asInstanceOf[PersistentChannelData].commitments.fundingTxId node.wallet.funded(fundingTxid) } @@ -196,7 +196,7 @@ object MinimalNodeFixture extends Assertions with Eventually with IntegrationPat def confirmChannelDeep(node1: MinimalNodeFixture, node2: MinimalNodeFixture, channelId: ByteVector32, blockHeight: BlockHeight, txIndex: Int)(implicit system: ActorSystem): RealScidStatus.Final = { assert(getChannelState(node1, channelId) == NORMAL) val data1Before = getChannelData(node1, channelId).asInstanceOf[DATA_NORMAL] - val fundingTxid = data1Before.commitments.commitInput.outPoint.txid + val fundingTxid = data1Before.commitments.fundingTxId val fundingTx = node1.wallet.funded(fundingTxid) val watch1 = node1.watcher.fishForMessage() { case w: WatchFundingDeeplyBuried if w.txId == fundingTx.txid => true; case _ => false }.asInstanceOf[WatchFundingDeeplyBuried]