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 ebc8c7aa5d..5a8712bfb8 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 @@ -454,8 +454,8 @@ sealed trait RbfStatus object RbfStatus { case object NoRbf extends RbfStatus case class RbfRequested(cmd: CMD_BUMP_FUNDING_FEE) extends RbfStatus - case class RbfInProgress(cmd_opt: Option[CMD_BUMP_FUNDING_FEE], rbf: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends RbfStatus - case class RbfWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends RbfStatus + case class RbfInProgress(cmd_opt: Option[CMD_BUMP_FUNDING_FEE], sessionContext: SessionContext, rbf: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends RbfStatus + case class RbfWaitingForSigs(sessionContext: SessionContext, signingSession: InteractiveTxSigningSession.WaitingForSigs) extends RbfStatus case object RbfAborted extends RbfStatus } @@ -481,9 +481,9 @@ object SpliceStatus { /** We told our peer we want to splice funds in the channel. */ case class SpliceRequested(cmd: CMD_SPLICE, init: SpliceInit) extends QuiescentSpliceStatus /** We both agreed to splice and are building the splice transaction. */ - case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], sessionId: ByteVector32, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends QuiescentSpliceStatus + case class SpliceInProgress(cmd_opt: Option[CMD_SPLICE], sessionContext: SessionContext, splice: typed.ActorRef[InteractiveTxBuilder.Command], remoteCommitSig: Option[CommitSig]) extends QuiescentSpliceStatus /** The splice transaction has been negotiated, we're exchanging signatures. */ - case class SpliceWaitingForSigs(signingSession: InteractiveTxSigningSession.WaitingForSigs) extends QuiescentSpliceStatus + case class SpliceWaitingForSigs(sessionContext: SessionContext, signingSession: InteractiveTxSigningSession.WaitingForSigs) extends QuiescentSpliceStatus /** The splice attempt was aborted by us, we're waiting for our peer to ack. */ case object SpliceAborted extends QuiescentSpliceStatus } 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 0f3a942d56..5b518b6130 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 @@ -564,7 +564,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case SpliceStatus.SpliceAborted => log.warning("received commit_sig after sending tx_abort, they probably sent it before receiving our tx_abort, ignoring...") stay() - case SpliceStatus.SpliceWaitingForSigs(signingSession) => + case SpliceStatus.SpliceWaitingForSigs(sessionContext, signingSession) => signingSession.receiveCommitSig(nodeParams, d.commitments.params, commit) match { case Left(f) => rollbackFundingAttempt(signingSession.fundingTx.tx, Nil) @@ -573,7 +573,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case signingSession1: InteractiveTxSigningSession.WaitingForSigs => // In theory we don't have to store their commit_sig here, as they would re-send it if we disconnect, but // it is more consistent with the case where we send our tx_signatures first. - val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession1)) + val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(sessionContext, signingSession1)) stay() using d1 storing() case signingSession1: InteractiveTxSigningSession.SendingSigs => // We don't have their tx_sigs, but they have ours, and could publish the funding tx without telling us. @@ -968,9 +968,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with targetFeerate = msg.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceAck.requireConfirmedInputs) ) - val sessionId = randomBytes32() + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - sessionId, + sessionContext, nodeParams, fundingParams, channelParams = d.commitments.params, purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), @@ -978,7 +978,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with wallet )) txBuilder ! InteractiveTxBuilder.Start(self) - stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionId, txBuilder, remoteCommitSig = None)) sending spliceAck + stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = None, sessionContext, txBuilder, remoteCommitSig = None)) sending spliceAck } case SpliceStatus.SpliceAborted => log.info("rejecting splice attempt: our previous tx_abort was not acked") @@ -1006,9 +1006,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with targetFeerate = spliceInit.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = spliceInit.requireConfirmedInputs) ) - val sessionId = randomBytes32() + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - sessionId, + sessionContext, nodeParams, fundingParams, channelParams = d.commitments.params, purpose = InteractiveTxBuilder.SpliceTx(parentCommitment), @@ -1016,7 +1016,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with wallet )) txBuilder ! InteractiveTxBuilder.Start(self) - stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionId, txBuilder, remoteCommitSig = None)) + stay() using d.copy(spliceStatus = SpliceStatus.SpliceInProgress(cmd_opt = Some(cmd), sessionContext, txBuilder, remoteCommitSig = None)) case _ => log.info(s"ignoring unexpected splice_ack=$msg") stay() @@ -1039,7 +1039,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with cmd_opt.foreach(cmd => cmd.replyTo ! RES_FAILURE(cmd, SpliceAttemptAborted(d.channelId))) txBuilder ! InteractiveTxBuilder.Abort stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) calling endQuiescence(d) - case SpliceStatus.SpliceWaitingForSigs(signingSession) => + case SpliceStatus.SpliceWaitingForSigs(_, signingSession) => log.info("our peer aborted the splice attempt: ascii='{}' bin={}", msg.toAscii, msg.data) rollbackFundingAttempt(signingSession.fundingTx.tx, previousTxs = Seq.empty) // no splice rbf yet stay() using d.copy(spliceStatus = SpliceStatus.NoSplice) sending TxAbort(d.channelId, SpliceAttemptAborted(d.channelId).getMessage) calling endQuiescence(d) @@ -1066,10 +1066,10 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with case Event(msg: InteractiveTxBuilder.Response, d: DATA_NORMAL) => d.spliceStatus match { - case SpliceStatus.SpliceInProgress(cmd_opt, currentSessionId, _, remoteCommitSig_opt) => + case SpliceStatus.SpliceInProgress(cmd_opt, sessionContext, _, remoteCommitSig_opt) => msg match { case InteractiveTxBuilder.SendMessage(sessionId, msg) => - if (sessionId == currentSessionId) { + if (sessionId == sessionContext.sessionId) { stay() sending msg } else { log.info("ignoring outgoing interactive-tx message {} from previous session", msg.getClass.getSimpleName) @@ -1079,7 +1079,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with log.info(s"splice tx created with fundingTxIndex=${signingSession.fundingTxIndex} fundingTxId=${signingSession.fundingTx.txId}") cmd_opt.foreach(cmd => cmd.replyTo ! RES_SPLICE(fundingTxIndex = signingSession.fundingTxIndex, signingSession.fundingTx.txId, signingSession.fundingParams.fundingAmount, signingSession.localCommit.fold(_.spec, _.spec).toLocal)) remoteCommitSig_opt.foreach(self ! _) - val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(signingSession)) + val d1 = d.copy(spliceStatus = SpliceStatus.SpliceWaitingForSigs(sessionContext, signingSession)) stay() using d1 storing() sending commitSig case f: InteractiveTxBuilder.Failed => log.info("splice attempt failed: {}", f.cause.getMessage) @@ -1115,7 +1115,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with } case _ => d.spliceStatus match { - case SpliceStatus.SpliceWaitingForSigs(signingSession) => + case SpliceStatus.SpliceWaitingForSigs(_, signingSession) => // we have not yet sent our tx_signatures signingSession.receiveTxSigs(nodeParams, d.commitments.params, msg) match { case Left(f) => @@ -1890,14 +1890,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommitIndex) val rbfTlv: Set[ChannelReestablishTlv] = d match { case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.rbfStatus match { - case RbfStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) + case RbfStatus.RbfWaitingForSigs(_, status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) case _ => d.latestFundingTx.sharedTx match { case _: InteractiveTxBuilder.PartiallySignedSharedTransaction => Set(ChannelReestablishTlv.NextFundingTlv(d.latestFundingTx.sharedTx.txId)) case _: InteractiveTxBuilder.FullySignedSharedTransaction => Set.empty } } case d: DATA_NORMAL => d.spliceStatus match { - case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) + case SpliceStatus.SpliceWaitingForSigs(_, status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId)) case _ => d.commitments.latest.localFundingStatus match { case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId)) case _ => Set.empty @@ -1954,7 +1954,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) => d.rbfStatus match { - case RbfStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + case RbfStatus.RbfWaitingForSigs(_, signingSession) if signingSession.fundingTx.txId == fundingTxId => // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig @@ -2009,7 +2009,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with val spliceStatus1 = channelReestablish.nextFundingTxId_opt match { case Some(fundingTxId) => d.spliceStatus match { - case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId => + case SpliceStatus.SpliceWaitingForSigs(_, signingSession) if signingSession.fundingTx.txId == fundingTxId => // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig. log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId) val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput) 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 08651e269b..03f62b8f18 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 @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.SatoshiLong import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs} +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs, SessionContext} import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.crypto.ShaChain @@ -212,11 +212,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { targetFeerate = open.fundingFeerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = open.requireConfirmedInputs, forRemote = accept.requireConfirmedInputs) ) + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val purpose = InteractiveTxBuilder.FundingTx(open.commitmentFeerate, open.firstPerCommitmentPoint, feeBudget_opt = None) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - randomBytes32(), - nodeParams, fundingParams, - channelParams, purpose, + sessionContext, + nodeParams, fundingParams, channelParams, + purpose, localPushAmount = accept.pushAmount, remotePushAmount = open.pushAmount, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) @@ -275,9 +276,10 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { targetFeerate = d.lastSent.fundingFeerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = accept.requireConfirmedInputs, forRemote = d.lastSent.requireConfirmedInputs) ) + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val purpose = InteractiveTxBuilder.FundingTx(d.lastSent.commitmentFeerate, accept.firstPerCommitmentPoint, feeBudget_opt = d.init.fundingTxFeeBudget_opt) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - randomBytes32(), + sessionContext, nodeParams, fundingParams, channelParams, purpose, localPushAmount = d.lastSent.pushAmount, remotePushAmount = accept.pushAmount, @@ -462,7 +464,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { } case _: FullySignedSharedTransaction => d.rbfStatus match { - case RbfStatus.RbfWaitingForSigs(signingSession) => + case RbfStatus.RbfWaitingForSigs(_, signingSession) => signingSession.receiveTxSigs(nodeParams, d.commitments.params, txSigs) match { case Left(f) => rollbackRbfAttempt(signingSession, d) @@ -545,8 +547,9 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { targetFeerate = msg.feerate, requireConfirmedInputs = RequireConfirmedInputs(forLocal = msg.requireConfirmedInputs, forRemote = nodeParams.channelConf.requireConfirmedInputsForDualFunding) ) + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - randomBytes32(), + sessionContext, nodeParams, fundingParams, channelParams = d.commitments.params, purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = None), @@ -557,7 +560,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { Some(TxAckRbf(d.channelId, fundingParams.localContribution, nodeParams.channelConf.requireConfirmedInputsForDualFunding)), if (remainingRbfAttempts <= 3) Some(Warning(d.channelId, s"will accept at most ${remainingRbfAttempts - 1} future rbf attempts")) else None, ).flatten - stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = None, txBuilder, remoteCommitSig = None)) sending toSend + stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = None, sessionContext, txBuilder, remoteCommitSig = None)) sending toSend } case RbfStatus.RbfAborted => log.info("rejecting rbf attempt: our previous tx_abort was not acked") @@ -583,15 +586,16 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { lockTime = cmd.lockTime, targetFeerate = cmd.targetFeerate, ) + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val txBuilder = context.spawnAnonymous(InteractiveTxBuilder( - randomBytes32(), + sessionContext, nodeParams, fundingParams, channelParams = d.commitments.params, purpose = InteractiveTxBuilder.PreviousTxRbf(d.commitments.active.head, 0 msat, 0 msat, previousTransactions = d.allFundingTxs.map(_.sharedTx), feeBudget_opt = Some(cmd.fundingFeeBudget)), localPushAmount = d.localPushAmount, remotePushAmount = d.remotePushAmount, wallet)) txBuilder ! InteractiveTxBuilder.Start(self) - stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), txBuilder, remoteCommitSig = None)) + stay() using d.copy(rbfStatus = RbfStatus.RbfInProgress(cmd_opt = Some(cmd), sessionContext, txBuilder, remoteCommitSig = None)) case _ => log.info("ignoring unexpected tx_ack_rbf") stay() sending Warning(d.channelId, UnexpectedInteractiveTxMessage(d.channelId, msg).getMessage) @@ -599,7 +603,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxConstructionMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.rbfStatus match { - case RbfStatus.RbfInProgress(_, txBuilder, _) => + case RbfStatus.RbfInProgress(_, _, txBuilder, _) => txBuilder ! InteractiveTxBuilder.ReceiveMessage(msg) stay() case _ => @@ -612,7 +616,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case s: RbfStatus.RbfInProgress => log.debug("received their commit_sig, deferring message") stay() using d.copy(rbfStatus = s.copy(remoteCommitSig = Some(commitSig))) - case RbfStatus.RbfWaitingForSigs(signingSession) => + case RbfStatus.RbfWaitingForSigs(sessionContext, signingSession) => signingSession.receiveCommitSig(nodeParams, d.commitments.params, commitSig) match { case Left(f) => rollbackRbfAttempt(signingSession, d) @@ -620,7 +624,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Right(signingSession1) => signingSession1 match { case signingSession1: InteractiveTxSigningSession.WaitingForSigs => // No need to store their commit_sig, they will re-send it if we disconnect. - stay() using d.copy(rbfStatus = RbfStatus.RbfWaitingForSigs(signingSession1)) + stay() using d.copy(rbfStatus = RbfStatus.RbfWaitingForSigs(sessionContext, signingSession1)) case signingSession1: InteractiveTxSigningSession.SendingSigs => val minDepth_opt = d.commitments.params.minDepthDualFunding(nodeParams.channelConf.minDepthBlocks, signingSession1.fundingTx.sharedTx.tx) watchFundingConfirmed(signingSession.fundingTx.txId, minDepth_opt, delay_opt = None) @@ -636,12 +640,12 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: TxAbort, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.rbfStatus match { - case RbfStatus.RbfInProgress(cmd_opt, txBuilder, _) => + case RbfStatus.RbfInProgress(cmd_opt, _, txBuilder, _) => log.info("our peer aborted the rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) cmd_opt.foreach(cmd => cmd.replyTo ! RES_FAILURE(cmd, RbfAttemptAborted(d.channelId))) txBuilder ! InteractiveTxBuilder.Abort stay() using d.copy(rbfStatus = RbfStatus.NoRbf) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) - case RbfStatus.RbfWaitingForSigs(signingSession) => + case RbfStatus.RbfWaitingForSigs(_, signingSession) => log.info("our peer aborted the rbf attempt: ascii='{}' bin={}", msg.toAscii, msg.data) rollbackRbfAttempt(signingSession, d) stay() using d.copy(rbfStatus = RbfStatus.NoRbf) sending TxAbort(d.channelId, RbfAttemptAborted(d.channelId).getMessage) @@ -660,13 +664,13 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxBuilder.Response, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) => d.rbfStatus match { - case RbfStatus.RbfInProgress(cmd_opt, _, remoteCommitSig_opt) => + case RbfStatus.RbfInProgress(cmd_opt, sessionContext, _, remoteCommitSig_opt) => msg match { case InteractiveTxBuilder.SendMessage(_, msg) => stay() sending msg case InteractiveTxBuilder.Succeeded(signingSession, commitSig) => cmd_opt.foreach(cmd => cmd.replyTo ! RES_BUMP_FUNDING_FEE(rbfIndex = d.previousFundingTxs.length, signingSession.fundingTx.txId, signingSession.fundingTx.tx.localFees.truncateToSatoshi)) remoteCommitSig_opt.foreach(self ! _) - val d1 = d.copy(rbfStatus = RbfStatus.RbfWaitingForSigs(signingSession)) + val d1 = d.copy(rbfStatus = RbfStatus.RbfWaitingForSigs(sessionContext, signingSession)) stay() using d1 storing() sending commitSig case f: InteractiveTxBuilder.Failed => log.info("rbf attempt failed: {}", f.cause.getMessage) 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 953e6b7f94..94e44aeab0 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 @@ -140,7 +140,7 @@ trait DualFundingHandlers extends CommonFundingHandlers { def reportRbfFailure(rbfStatus: RbfStatus, f: Throwable): Unit = { rbfStatus match { case RbfStatus.RbfRequested(cmd) => cmd.replyTo ! RES_FAILURE(cmd, f) - case RbfStatus.RbfInProgress(cmd_opt, txBuilder, _) => + case RbfStatus.RbfInProgress(cmd_opt, _, txBuilder, _) => txBuilder ! InteractiveTxBuilder.Abort cmd_opt.foreach(cmd => cmd.replyTo ! RES_FAILURE(cmd, f)) case _ => () diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index f3983b6688..64daadd2a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{Purpose, SessionContext} import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner} @@ -340,7 +340,16 @@ object InteractiveTxBuilder { } // @formatter:on - def apply(sessionId: ByteVector32, + // @formatter:off + sealed trait SessionContext { + def sessionId: ByteVector32 + } + object SessionContext { + case class Unspecified(sessionId: ByteVector32) extends SessionContext + } + // @formatter:on + + def apply(sessionContext: SessionContext, nodeParams: NodeParams, fundingParams: InteractiveTxParams, channelParams: ChannelParams, @@ -366,7 +375,7 @@ object InteractiveTxBuilder { replyTo ! LocalFailure(InvalidFundingBalances(channelParams.channelId, fundingParams.fundingAmount, nextLocalBalance, nextRemoteBalance)) Behaviors.stopped } else { - val actor = new InteractiveTxBuilder(replyTo, sessionId, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context) + val actor = new InteractiveTxBuilder(replyTo, sessionContext, nodeParams, channelParams, fundingParams, purpose, localPushAmount, remotePushAmount, wallet, stash, context) actor.start() } case Abort => Behaviors.stopped @@ -382,7 +391,7 @@ object InteractiveTxBuilder { } private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Response], - sessionId: ByteVector32, + sessionContext: SessionContext, nodeParams: NodeParams, channelParams: ChannelParams, fundingParams: InteractiveTxBuilder.InteractiveTxParams, @@ -396,6 +405,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon import InteractiveTxBuilder._ private val log = context.log + private val sessionId = sessionContext.sessionId private val keyManager = nodeParams.channelKeyManager private val localFundingPubKey: PublicKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex).publicKey private val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubKey, fundingParams.remoteFundingPubKey))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index e8be2ded6e..5139b0cb1b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -2,11 +2,11 @@ package fr.acinq.eclair.wire.internal.channel.version4 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath -import fr.acinq.bitcoin.scalacompat.{OutPoint, ScriptWitness, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, ScriptWitness, Transaction, TxOut} import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget} import fr.acinq.eclair.channel.LocalFundingStatus._ import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction} +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{FullySignedSharedTransaction, PartiallySignedSharedTransaction, SessionContext} import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.crypto.ShaChain @@ -589,13 +589,20 @@ private[channel] object ChannelCodecs4 { waitingForSigsCodec } + private val sessionContextCodec: Codec[SessionContext] = discriminated[SessionContext].by(uint8) + .typecase(0x01, bytes32.as[SessionContext.Unspecified]) + + // Order matters! val rbfStatusCodec: Codec[RbfStatus] = discriminated[RbfStatus].by(uint8) - .\(0x01) { case status: RbfStatus if !status.isInstanceOf[RbfStatus.RbfWaitingForSigs] => RbfStatus.NoRbf }(provide(RbfStatus.NoRbf)) - .\(0x02) { case status: RbfStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[RbfStatus.RbfWaitingForSigs]) + .\(0x03) { case status: RbfStatus.RbfWaitingForSigs => status }((sessionContextCodec :: interactiveTxWaitingForSigsCodec).as[RbfStatus.RbfWaitingForSigs]) + .\(0x02) { case status: RbfStatus.RbfWaitingForSigs => status }((provide[SessionContext](SessionContext.Unspecified(ByteVector32.Zeroes)) :: interactiveTxWaitingForSigsCodec).as[RbfStatus.RbfWaitingForSigs]) + .\(0x01) { case _: RbfStatus => RbfStatus.NoRbf }(provide(RbfStatus.NoRbf)) + // Order matters! val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) - .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) - .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }((sessionContextCodec :: interactiveTxWaitingForSigsCodec).as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }((provide[SessionContext](SessionContext.Unspecified(ByteVector32.Zeroes)) :: interactiveTxWaitingForSigsCodec).as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x01) { case _: SpliceStatus => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodecWithoutFirstRemoteCommitIndex) :: 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 6f0cecfa52..577f5a3358 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 @@ -104,6 +104,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit private val firstPerCommitmentPointA = nodeParamsA.channelKeyManager.commitmentPoint(nodeParamsA.channelKeyManager.keyPath(channelParamsA.localParams, ChannelConfig.standard), 0) private val firstPerCommitmentPointB = nodeParamsB.channelKeyManager.commitmentPoint(nodeParamsB.channelKeyManager.keyPath(channelParamsB.localParams, ChannelConfig.standard), 0) val fundingPubkeyScript: ByteVector = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingParamsB.remoteFundingPubKey, fundingParamsA.remoteFundingPubKey))) + private val defaultSessionContext = SessionContext.Unspecified(sessionId = ByteVector32.Zeroes) def dummySharedInputB(amount: Satoshi): SharedFundingInput = { val inputInfo = InputInfo(OutPoint(randomTxId(), 3), TxOut(amount, fundingPubkeyScript), Nil) @@ -123,56 +124,56 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit } def spawnTxBuilderAlice(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsA): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsA, fundingParams, channelParamsA, FundingTx(commitFeerate, firstPerCommitmentPointB, feeBudget_opt = None), 0 msat, 0 msat, wallet)) def spawnTxBuilderRbfAlice(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsA, fundingParams, channelParamsA, PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) def spawnTxBuilderSpliceAlice(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsA, fundingParams, channelParamsA, SpliceTx(commitment), 0 msat, 0 msat, wallet)) def spawnTxBuilderSpliceRbfAlice(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsA, fundingParams, channelParamsA, PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) def spawnTxBuilderBob(wallet: OnChainWallet, fundingParams: InteractiveTxParams = fundingParamsB): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsB, fundingParams, channelParamsB, FundingTx(commitFeerate, firstPerCommitmentPointA, feeBudget_opt = None), 0 msat, 0 msat, wallet)) def spawnTxBuilderRbfBob(fundingParams: InteractiveTxParams, commitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsB, fundingParams, channelParamsB, PreviousTxRbf(commitment, 0 msat, 0 msat, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, wallet)) def spawnTxBuilderSpliceBob(fundingParams: InteractiveTxParams, commitment: Commitment, wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsB, fundingParams, channelParamsB, SpliceTx(commitment), 0 msat, 0 msat, wallet)) def spawnTxBuilderSpliceRbfBob(fundingParams: InteractiveTxParams, parentCommitment: Commitment, replacedCommitment: Commitment, previousTransactions: Seq[InteractiveTxBuilder.SignedSharedTransaction], wallet: OnChainWallet): ActorRef[InteractiveTxBuilder.Command] = system.spawnAnonymous(InteractiveTxBuilder( - ByteVector32.Zeroes, + defaultSessionContext, nodeParamsB, fundingParams, channelParamsB, PreviousTxRbf(replacedCommitment, parentCommitment.localCommit.spec.toLocal, parentCommitment.remoteCommit.spec.toLocal, previousTransactions, feeBudget_opt = None), 0 msat, 0 msat, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index a1706baef6..299ae54e28 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -9,7 +9,7 @@ import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs, SharedTransaction} +import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.{InteractiveTxParams, PartiallySignedSharedTransaction, RequireConfirmedInputs, SessionContext, SharedTransaction} import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningSession} import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo} @@ -135,6 +135,7 @@ class ChannelCodecs4Spec extends AnyFunSuite { fundingInput, Transaction(2, Seq(TxIn(fundingInput.outPoint, Nil, 0)), Seq(TxOut(150_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), ) + val sessionContext = SessionContext.Unspecified(sessionId = randomBytes32()) val waitingForSigs = InteractiveTxSigningSession.WaitingForSigs( InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 75_000 sat, None, randomKey().publicKey, Nil, 0, 330 sat, FeeratePerKw(500 sat), RequireConfirmedInputs(forLocal = false, forRemote = false)), fundingTxIndex = 0, @@ -145,8 +146,8 @@ class ChannelCodecs4Spec extends AnyFunSuite { val testCases = Map( RbfStatus.NoRbf -> RbfStatus.NoRbf, RbfStatus.RbfRequested(CMD_BUMP_FUNDING_FEE(null, FeeratePerKw(750 sat), fundingFeeBudget = 100_000.sat, 0)) -> RbfStatus.NoRbf, - RbfStatus.RbfInProgress(None, null, None) -> RbfStatus.NoRbf, - RbfStatus.RbfWaitingForSigs(waitingForSigs) -> RbfStatus.RbfWaitingForSigs(waitingForSigs), + RbfStatus.RbfInProgress(None, null, null, None) -> RbfStatus.NoRbf, + RbfStatus.RbfWaitingForSigs(sessionContext, waitingForSigs) -> RbfStatus.RbfWaitingForSigs(sessionContext, waitingForSigs), RbfStatus.RbfAborted -> RbfStatus.NoRbf, ) testCases.foreach { case (status, expected) =>