diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index 299516b424..5643df795b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -143,8 +143,7 @@ object ZmqWatcher { case class WatchFundingConfirmed(replyTo: ActorRef[WatchFundingConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchFundingConfirmedTriggered] case class WatchFundingConfirmedTriggered(blockHeight: BlockHeight, txIndex: Int, tx: Transaction) extends WatchConfirmedTriggered - case class RelativeDelay(parentTxId: TxId, delay: Long) - case class WatchTxConfirmed(replyTo: ActorRef[WatchTxConfirmedTriggered], txId: TxId, minDepth: Int, delay_opt: Option[RelativeDelay] = None) extends WatchConfirmed[WatchTxConfirmedTriggered] + case class WatchTxConfirmed(replyTo: ActorRef[WatchTxConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchTxConfirmedTriggered] case class WatchTxConfirmedTriggered(blockHeight: BlockHeight, txIndex: Int, tx: Transaction) extends WatchConfirmedTriggered case class WatchParentTxConfirmed(replyTo: ActorRef[WatchParentTxConfirmedTriggered], txId: TxId, minDepth: Int) extends WatchConfirmed[WatchParentTxConfirmedTriggered] @@ -463,10 +462,10 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client private def checkConfirmed(w: WatchConfirmed[_ <: WatchConfirmedTriggered], currentHeight: BlockHeight): Future[Unit] = { log.debug("checking confirmations of txid={}", w.txId) - // NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really - // matter because this only happens once, when the watched transaction has reached min_depth client.getTxConfirmations(w.txId).flatMap { case Some(confirmations) if confirmations >= w.minDepth => + // NB: this is very inefficient since internally we call `getrawtransaction` three times, but it doesn't really + // matter because this only happens once, when the watched transaction has reached min_depth client.getTransaction(w.txId).flatMap { tx => client.getTransactionShortId(w.txId).map { case (height, index) => w match { @@ -483,27 +482,11 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth - confirmations)) Future.successful(()) case None => - w match { - case WatchTxConfirmed(_, _, _, Some(relativeDelay)) => - log.debug("txId={} has a relative delay of {} blocks, checking parentTxId={}", w.txId, relativeDelay.delay, relativeDelay.parentTxId) - // Note how we add one block to avoid an off-by-one: - // - if the parent is confirmed at block P - // - the CSV delay is D and the minimum depth is M - // - the first block that can include the child is P + D - // - the first block at which we can reach minimum depth is P + D + M - // - if we are currently at block P + N, the parent has C = N + 1 confirmations - // - we want to check at block P + N + D + M + 1 - C = P + N + D + M + 1 - (N + 1) = P + D + M - val delay = relativeDelay.delay + w.minDepth + 1 - client.getTxConfirmations(relativeDelay.parentTxId).map(_.getOrElse(0)).collect { - case confirmations if confirmations < delay => context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + delay - confirmations)) - } - case _ => - // The transaction is unconfirmed: we don't need to check again at every new block: we can check only once - // every minDepth blocks, which is more efficient. If the transaction is included at the current height in - // a reorg, we will trigger the watch one block later than expected, but this is fine. - context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth)) - Future.successful(()) - } + // The transaction is unconfirmed: we don't need to check again at every new block: we can check only once + // every minDepth blocks, which is more efficient. If the transaction is included at the current height in + // a reorg, we will trigger the watch one block later than expected, but this is fine. + context.self ! SetWatchHint(w, CheckAfterBlock(currentHeight + w.minDepth)) + Future.successful(()) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index c7dad1c874..857579d2ae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -875,8 +875,14 @@ object Helpers { object LocalClose { + /** Transactions spending outputs of our commitment transaction. */ + case class SecondStageTransactions(mainDelayedTx_opt: Option[ClaimLocalDelayedOutputTx], anchorTx_opt: Option[ClaimAnchorOutputTx], htlcTxs: Seq[HtlcTx]) + + /** Transactions spending outputs of our HTLC transactions. */ + case class ThirdStageTransactions(htlcDelayedTxs: Seq[HtlcDelayedTx]) + /** Claim all the outputs that belong to us in our local commitment transaction. */ - def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): LocalCommitPublished = { + def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, SecondStageTransactions) = { require(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid == commitTx.txid, "txid mismatch, provided tx is not the current local commit tx") val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitmentKeys = commitment.localKeys(channelKeys) @@ -891,7 +897,7 @@ object Helpers { } else { None } - LocalCommitPublished( + val lcp = LocalCommitPublished( commitTx = commitTx, claimMainDelayedOutputTx = mainDelayedTx_opt, htlcTxs = htlcTxs, @@ -899,6 +905,8 @@ object Helpers { claimAnchorTxs = anchorTx_opt.toList, irrevocablySpent = Map.empty ) + val txs = SecondStageTransactions(mainDelayedTx_opt, anchorTx_opt, htlcTxs.values.flatten.toSeq) + (lcp, txs) } def claimAnchor(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimAnchorOutputTx] = { @@ -1002,19 +1010,19 @@ object Helpers { * NB: with anchor outputs, it's possible to have transactions that spend *many* HTLC outputs at once, but we're not * doing that because it introduces a lot of subtle edge cases. */ - def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, Option[HtlcDelayedTx]) = { + def claimHtlcDelayedOutput(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, tx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, ThirdStageTransactions) = { if (tx.txIn.exists(txIn => localCommitPublished.htlcTxs.contains(txIn.outPoint))) { - val feeratePerKwDelayed = onChainFeeConf.getClosingFeerate(feerates) + val feerateDelayed = onChainFeeConf.getClosingFeerate(feerates) val commitKeys = commitment.localKeys(channelKeys) // Note that this will return None if the transaction wasn't one of our HTLC transactions, which may happen // if our peer was able to claim the HTLC output before us (race condition between success and timeout). val htlcDelayedTx_opt = withTxGenerationLog("htlc-delayed") { - HtlcDelayedTx.createSignedTx(commitKeys, tx, commitment.localParams.dustLimit, commitment.remoteParams.toSelfDelay, finalScriptPubKey, feeratePerKwDelayed, commitment.params.commitmentFormat) + HtlcDelayedTx.createSignedTx(commitKeys, tx, commitment.localParams.dustLimit, commitment.remoteParams.toSelfDelay, finalScriptPubKey, feerateDelayed, commitment.params.commitmentFormat) } val localCommitPublished1 = localCommitPublished.copy(claimHtlcDelayedTxs = localCommitPublished.claimHtlcDelayedTxs ++ htlcDelayedTx_opt.toSeq) - (localCommitPublished1, htlcDelayedTx_opt) + (localCommitPublished1, ThirdStageTransactions(htlcDelayedTx_opt.toSeq)) } else { - (localCommitPublished, None) + (localCommitPublished, ThirdStageTransactions(Nil)) } } @@ -1022,8 +1030,11 @@ object Helpers { object RemoteClose { + /** Transactions spending outputs of a remote commitment transaction. */ + case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteCommitMainOutputTx], anchorTx_opt: Option[ClaimAnchorOutputTx], htlcTxs: Seq[ClaimHtlcTx]) + /** Claim all the outputs that belong to us in the remote commitment transaction (which can be either their current or next commitment). */ - def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): RemoteCommitPublished = { + def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RemoteCommitPublished, SecondStageTransactions) = { require(remoteCommit.txid == commitTx.txid, "txid mismatch, provided tx is not the current remote commit tx") val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) @@ -1035,13 +1046,15 @@ object Helpers { } else { None } - RemoteCommitPublished( + val rcp = RemoteCommitPublished( commitTx = commitTx, claimMainOutputTx = mainTx_opt, claimHtlcTxs = htlcTxs, claimAnchorTxs = anchorTx_opt.toList, irrevocablySpent = Map.empty ) + val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcTxs.values.flatten.toSeq) + (rcp, txs) } def claimAnchor(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat)(implicit log: LoggingAdapter): Option[ClaimAnchorOutputTx] = { @@ -1171,6 +1184,12 @@ object Helpers { object RevokedClose { + /** Transactions spending outputs of a revoked remote commitment transactions. */ + case class SecondStageTransactions(mainTx_opt: Option[ClaimRemoteCommitMainOutputTx], mainPenaltyTx_opt: Option[MainPenaltyTx], htlcPenaltyTxs: Seq[HtlcPenaltyTx]) + + /** Transactions spending outputs of confirmed remote HTLC transactions. */ + case class ThirdStageTransactions(htlcDelayedPenaltyTxs: Seq[ClaimHtlcDelayedOutputPenaltyTx]) + /** * When an unexpected transaction spending the funding tx is detected, we must be in one of the following scenarios: * @@ -1203,7 +1222,7 @@ object Helpers { * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that * will punish our peer by stealing all their funds. */ - def claimCommitTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, db: ChannelsDb, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): RevokedCommitPublished = { + def claimCommitTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, commitmentNumber: Long, remotePerCommitmentSecret: PrivateKey, db: ChannelsDb, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, SecondStageTransactions) = { import params._ log.warning("a revoked commit has been published with commitmentNumber={}", commitmentNumber) @@ -1236,7 +1255,7 @@ object Helpers { val htlcPenaltyTxs = HtlcPenaltyTx.createSignedTxs(commitKeys, revocationKey, commitTx, htlcInfos, localParams.dustLimit, finalScriptPubKey, feeratePenalty, commitmentFormat) .flatMap(htlcPenaltyTx => withTxGenerationLog("htlc-penalty")(htlcPenaltyTx)) - RevokedCommitPublished( + val rvk = RevokedCommitPublished( commitTx = commitTx, claimMainOutputTx = mainTx_opt, mainPenaltyTx = mainPenaltyTx_opt, @@ -1244,6 +1263,8 @@ object Helpers { claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx irrevocablySpent = Map.empty ) + val txs = SecondStageTransactions(mainTx_opt, mainPenaltyTx_opt, htlcPenaltyTxs) + (rvk, txs) } /** @@ -1259,7 +1280,7 @@ object Helpers { * NB: when anchor outputs is used, htlc transactions can be aggregated in a single transaction if they share the same * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. */ - def claimHtlcTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, Seq[ClaimHtlcDelayedOutputPenaltyTx]) = { + def claimHtlcTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RevokedCommitPublished, ThirdStageTransactions) = { // We published HTLC-penalty transactions for every HTLC output: this transaction may be ours, or it may be one // of their HTLC transactions that confirmed before our HTLC-penalty transaction. If it is spending an HTLC // output, we assume that it's an HTLC transaction published by our peer and try to create penalty transactions @@ -1284,10 +1305,11 @@ object Helpers { } }) val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) - (revokedCommitPublished1, penaltyTxs) - }.getOrElse((revokedCommitPublished, Nil)) + val txs = ThirdStageTransactions(penaltyTxs) + (revokedCommitPublished1, txs) + }.getOrElse((revokedCommitPublished, ThirdStageTransactions(Nil))) } else { - (revokedCommitPublished, Nil) + (revokedCommitPublished, ThirdStageTransactions(Nil)) } } @@ -1519,26 +1541,6 @@ object Helpers { revokedCommitPublished.copy(irrevocablySpent = revokedCommitPublished.irrevocablySpent ++ relevantOutpoints.map(o => o -> tx).toMap) } - /** - * This helper function tells if some of the utxos consumed by the given transaction have already been irrevocably spent (possibly by this very transaction). - * - * It can be useful to: - * - not attempt to publish this tx when we know this will fail - * - not watch for confirmations if we know the tx is already confirmed - * - not watch the corresponding utxo when we already know the final spending tx - * - * @param tx an arbitrary transaction - * @param irrevocablySpent a map of known spent outpoints - * @return true if we know for sure that the utxos consumed by the tx have already irrevocably been spent, false otherwise - */ - def inputsAlreadySpent(tx: Transaction, irrevocablySpent: Map[OutPoint, Transaction]): Boolean = { - tx.txIn.exists(txIn => irrevocablySpent.contains(txIn.outPoint)) - } - - def inputAlreadySpent(input: OutPoint, irrevocablySpent: Map[OutPoint, Transaction]): Boolean = { - irrevocablySpent.contains(input) - } - } } 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 d8fd0bf572..5a82f46aef 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 @@ -38,7 +38,7 @@ import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxFunder, InteractiveTxSigningSession} -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishReplaceableTx, SetChannelId} import fr.acinq.eclair.channel.publish.{ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor, TxPublisher} import fr.acinq.eclair.crypto.keymanager.ChannelKeys import fr.acinq.eclair.db.DbEventHandler.ChannelEvent.EventType @@ -287,7 +287,6 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.debug("restoring channel") context.system.eventStream.publish(ChannelRestored(self, data.channelId, peer, remoteNodeId, data)) txPublisher ! SetChannelId(remoteNodeId, data.channelId) - // We watch all unconfirmed funding txs, whatever our state is. // There can be multiple funding txs due to rbf, and they can be unconfirmed in any state due to zero-conf. // To avoid a herd effect on restart, we add a delay before watching funding txs. @@ -342,7 +341,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } } } - + // We resume the channel and re-publish closing transactions if it was closing. data match { // NB: order matters! case closing: DATA_CLOSING if Closing.nothingAtStake(closing) => @@ -362,20 +361,28 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Some(c: Closing.MutualClose) => doPublish(c.tx, localPaysClosingFees) case Some(c: Closing.LocalClose) => - doPublish(c.localCommitPublished, closing.commitments.latest) + val secondStageTransactions = Closing.LocalClose.SecondStageTransactions(c.localCommitPublished.claimMainDelayedOutputTx, c.localCommitPublished.claimAnchorTx_opt, c.localCommitPublished.htlcTxs.values.flatten.toSeq) + doPublish(c.localCommitPublished, secondStageTransactions, closing.commitments.latest) + val thirdStageTransactions = Closing.LocalClose.ThirdStageTransactions(c.localCommitPublished.claimHtlcDelayedTxs) + doPublish(c.localCommitPublished, thirdStageTransactions) case Some(c: Closing.RemoteClose) => - doPublish(c.remoteCommitPublished, closing.commitments.latest) + val secondStageTransactions = Closing.RemoteClose.SecondStageTransactions(c.remoteCommitPublished.claimMainOutputTx, c.remoteCommitPublished.claimAnchorTx_opt, c.remoteCommitPublished.claimHtlcTxs.values.flatten.toSeq) + doPublish(c.remoteCommitPublished, secondStageTransactions, closing.commitments.latest) case Some(c: Closing.RecoveryClose) => - doPublish(c.remoteCommitPublished, closing.commitments.latest) + val secondStageTransactions = Closing.RemoteClose.SecondStageTransactions(c.remoteCommitPublished.claimMainOutputTx, c.remoteCommitPublished.claimAnchorTx_opt, c.remoteCommitPublished.claimHtlcTxs.values.flatten.toSeq) + doPublish(c.remoteCommitPublished, secondStageTransactions, closing.commitments.latest) case Some(c: Closing.RevokedClose) => - doPublish(c.revokedCommitPublished) + val secondStageTransactions = Closing.RevokedClose.SecondStageTransactions(c.revokedCommitPublished.claimMainOutputTx, c.revokedCommitPublished.mainPenaltyTx, c.revokedCommitPublished.htlcPenaltyTxs) + doPublish(c.revokedCommitPublished, secondStageTransactions) + val thirdStageTransactions = Closing.RevokedClose.ThirdStageTransactions(c.revokedCommitPublished.claimHtlcDelayedPenaltyTxs) + doPublish(c.revokedCommitPublished, thirdStageTransactions) case None => closing.mutualClosePublished.foreach(mcp => doPublish(mcp, localPaysClosingFees)) - closing.localCommitPublished.foreach(lcp => doPublish(lcp, closing.commitments.latest)) - closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) - closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) - closing.revokedCommitPublished.foreach(doPublish) - closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, closing.commitments.latest)) + closing.localCommitPublished.foreach(lcp => doPublish(lcp, Closing.LocalClose.SecondStageTransactions(lcp.claimMainDelayedOutputTx, lcp.claimAnchorTx_opt, lcp.htlcTxs.values.flatten.toSeq), closing.commitments.latest)) + closing.remoteCommitPublished.foreach(rcp => doPublish(rcp, Closing.RemoteClose.SecondStageTransactions(rcp.claimMainOutputTx, rcp.claimAnchorTx_opt, rcp.claimHtlcTxs.values.flatten.toSeq), closing.commitments.latest)) + closing.nextRemoteCommitPublished.foreach(rcp => doPublish(rcp, Closing.RemoteClose.SecondStageTransactions(rcp.claimMainOutputTx, rcp.claimAnchorTx_opt, rcp.claimHtlcTxs.values.flatten.toSeq), closing.commitments.latest)) + closing.revokedCommitPublished.foreach(rvk => doPublish(rvk, Closing.RevokedClose.SecondStageTransactions(rvk.claimMainOutputTx, rvk.mainPenaltyTx, rvk.htlcPenaltyTxs))) + closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, Closing.RemoteClose.SecondStageTransactions(rcp.claimMainOutputTx, rcp.claimAnchorTx_opt, rcp.claimHtlcTxs.values.flatten.toSeq), closing.commitments.latest)) } // no need to go OFFLINE, we can directly switch to CLOSING goto(CLOSING) using closing @@ -2065,12 +2072,9 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // first we check if this tx belongs to one of the current local/remote commits, update it and update the channel data val d1 = d.copy( localCommitPublished = d.localCommitPublished.map(localCommitPublished => { - // If the tx is one of our HTLC txs, we now publish a 3rd-stage claim-htlc-tx that claims its output. - val (localCommitPublished1, claimHtlcTx_opt) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, channelKeys, d.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.finalScriptPubKey) - claimHtlcTx_opt.foreach(claimHtlcTx => { - txPublisher ! PublishFinalTx(claimHtlcTx, claimHtlcTx.fee, None) - blockchain ! WatchTxConfirmed(self, claimHtlcTx.tx.txid, nodeParams.channelConf.minDepth, Some(RelativeDelay(tx.txid, d.commitments.params.remoteParams.toSelfDelay.toInt.toLong))) - }) + // If the tx is one of our HTLC txs, we now publish a 3rd-stage transaction that claims its output. + val (localCommitPublished1, htlcDelayedTxs) = Closing.LocalClose.claimHtlcDelayedOutput(localCommitPublished, channelKeys, d.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, d.finalScriptPubKey) + doPublish(localCommitPublished1, htlcDelayedTxs) Closing.updateLocalCommitPublished(localCommitPublished1, tx) }), remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), @@ -2080,8 +2084,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // If the tx is one of our peer's HTLC txs, they were able to claim the output before us. // In that case, we immediately publish a penalty transaction spending their HTLC tx to steal their funds. val (rvk1, penaltyTxs) = Closing.RevokedClose.claimHtlcTxOutputs(d.commitments.params, channelKeys, d.commitments.remotePerCommitmentSecrets, rvk, tx, nodeParams.currentBitcoinCoreFeerates, d.finalScriptPubKey) - penaltyTxs.foreach(claimTx => txPublisher ! PublishFinalTx(claimTx, claimTx.fee, None)) - penaltyTxs.foreach(claimTx => blockchain ! WatchOutputSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, claimTx.amountIn, hints = Set(claimTx.tx.txid))) + doPublish(rvk1, penaltyTxs) Closing.updateRevokedCommitPublished(rvk1, tx) }) ) 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 5a761edeb1..cddc81545d 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 @@ -18,10 +18,10 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, FSM} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{RelativeDelay, WatchOutputSpent, WatchTxConfirmed} +import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ @@ -173,20 +173,11 @@ trait ErrorHandlers extends CommonHandlers { * This helper method will publish txs only if they haven't yet reached minDepth */ private def publishIfNeeded(txs: Iterable[PublishTx], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { - val (skip, process) = txs.partition(publishTx => Closing.inputAlreadySpent(publishTx.input, irrevocablySpent)) + val (skip, process) = txs.partition(publishTx => irrevocablySpent.contains(publishTx.input)) process.foreach { publishTx => txPublisher ! publishTx } skip.foreach(publishTx => log.debug("no need to republish tx spending {}:{}, it has already been confirmed", publishTx.input.txid, publishTx.input.index)) } - /** - * This helper method will watch txs only if they haven't yet reached minDepth - */ - private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction], relativeDelays: Map[TxId, RelativeDelay]): Unit = { - val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchTxConfirmed(self, tx.txid, nodeParams.channelConf.minDepth, relativeDelays.get(tx.txid))) - skip.foreach(tx => log.debug(s"no need to watch txid=${tx.txid}, it has already been confirmed")) - } - /** * This helper method will watch txs only if the utxo they spend hasn't already been irrevocably spent * @@ -202,6 +193,13 @@ trait ErrorHandlers extends CommonHandlers { skip.foreach(output => log.debug(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) } + /** This helper method will watch the given output only if it hasn't already been irrevocably spent. */ + private def watchSpentIfNeeded(input: InputInfo, irrevocablySpent: Map[OutPoint, Transaction]): Unit = { + if (!irrevocablySpent.contains(input.outPoint)) { + blockchain ! WatchOutputSpent(self, input.outPoint.txid, input.outPoint.index.toInt, input.txOut.amount, Set.empty) + } + } + def spendLocalCurrent(d: ChannelDataWithCommitments) = { val outdatedCommitment = d match { case _: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => true @@ -217,48 +215,52 @@ trait ErrorHandlers extends CommonHandlers { log.error(s"force-closing with fundingIndex=${commitment.fundingTxIndex}") context.system.eventStream.publish(NotifyNodeOperator(NotificationsLogger.Error, s"force-closing channel ${d.channelId} with fundingIndex=${commitment.fundingTxIndex}")) val commitTx = commitment.fullySignedLocalCommitTx(channelKeys) - val localCommitPublished = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val (localCommitPublished, closingTxs) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, negotiating.closingTxProposed.flatten.map(_.unsignedTx), localCommitPublished = Some(localCommitPublished)) case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, localCommitPublished = Some(localCommitPublished)) case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, localCommitPublished = Some(localCommitPublished)) } - goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, commitment) + goto(CLOSING) using nextData storing() calling doPublish(localCommitPublished, closingTxs, commitment) } } - def doPublish(localCommitPublished: LocalCommitPublished, commitment: FullCommitment): Unit = { - import localCommitPublished._ - + /** Publish 2nd-stage transactions for our local commitment. */ + def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = { val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitKeys = commitment.localKeys(channelKeys) - val publishCommitTx = PublishFinalTx(commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, commitTx, commitment.localParams.paysCommitTxFees), None) - val publishAnchorTx_opt = claimAnchorTx_opt match { - case Some(anchorTx) if !localCommitPublished.isConfirmed => + val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, lcp.commitTx, commitment.localParams.paysCommitTxFees), None) + val publishAnchorTx_opt = txs.anchorTx_opt match { + case Some(anchorTx) if !lcp.isConfirmed => val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localParams.dustLimit, commitment.params.commitmentFormat, nodeParams.onChainFeeConf) - Some(PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget)) + Some(PublishReplaceableTx(ReplaceableLocalCommitAnchor(anchorTx, fundingKey, commitKeys, lcp.commitTx, commitment), confirmationTarget)) case _ => None } - val publishMainDelayedTx_opt = claimMainDelayedOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishHtlcTxs = redeemableHtlcTxs(commitTx, commitKeys, commitment) - val publishHtlcDelayedTxs = claimHtlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs ++ publishHtlcDelayedTxs - publishIfNeeded(publishQueue, irrevocablySpent) - - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - // Our 'final txs" have a long relative delay: we provide that information to the watcher for efficiency. - val relativeDelays = (claimMainDelayedOutputTx ++ claimHtlcDelayedTxs).map(tx => tx.tx.txid -> RelativeDelay(tx.input.outPoint.txid, commitment.remoteParams.toSelfDelay.toInt.toLong)).toMap - val watchConfirmedQueue = List(commitTx) ++ claimMainDelayedOutputTx.map(_.tx) ++ claimHtlcDelayedTxs.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays) - - // We watch outputs of the commitment tx that both parties may spend. - // We also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees - // in the audit DB, even if we restart before confirmation. - val watchSpentQueue = htlcTxs.keys.toSeq ++ publishAnchorTx_opt.map(_.input) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + val publishMainDelayedTx_opt = txs.mainDelayedTx_opt.map(tx => PublishFinalTx(tx, tx.fee, None)) + val publishHtlcTxs = redeemableHtlcTxs(lcp.commitTx, commitKeys, commitment) + val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs + publishIfNeeded(publishQueue, lcp.irrevocablySpent) + + if (!lcp.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, lcp.commitTx.txid, nodeParams.channelConf.minDepth) + } + + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = lcp.claimMainDelayedOutputTx.map(_.input.outPoint) ++ lcp.claimAnchorTx_opt.map(_.input.outPoint) ++ lcp.htlcTxs.keys.toSeq + watchSpentIfNeeded(lcp.commitTx, watchSpentQueue, lcp.irrevocablySpent) + } + + /** Publish 3rd-stage transactions for our local commitment. */ + def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.ThirdStageTransactions): Unit = { + val publishHtlcDelayedTxs = txs.htlcDelayedTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + publishIfNeeded(publishHtlcDelayedTxs, lcp.irrevocablySpent) + // We watch the spent outputs to detect our RBF attempts. + txs.htlcDelayedTxs.foreach(tx => watchSpentIfNeeded(tx.input, lcp.irrevocablySpent)) } private def redeemableHtlcTxs(commitTx: Transaction, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Iterable[PublishTx] = { @@ -295,14 +297,14 @@ trait ErrorHandlers extends CommonHandlers { require(commitTx.txid == commitments.remoteCommit.txid, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), remoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING_SIMPLE => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.proposedClosingTxs.flatMap(_.all), mutualClosePublished = negotiating.publishedClosingTxs, remoteCommitPublished = Some(remoteCommitPublished)) case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, remoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, commitments) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitments) } def handleRemoteSpentNext(commitTx: Transaction, d: ChannelDataWithCommitments) = { @@ -314,7 +316,7 @@ trait ErrorHandlers extends CommonHandlers { val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "next-remote-commit")) - val remoteCommitPublished = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) case negotiating: DATA_NEGOTIATING => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = negotiating.closingTxProposed.flatten.map(_.unsignedTx), nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -322,40 +324,39 @@ trait ErrorHandlers extends CommonHandlers { // NB: if there is a next commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, nextRemoteCommitPublished = Some(remoteCommitPublished)) } - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, commitment) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitment) } - def doPublish(remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment): Unit = { - import remoteCommitPublished._ - + /** Publish 2nd-stage transactions for the remote commitment (no need for 3rd-stage transactions in that case). */ + def doPublish(rcp: RemoteCommitPublished, txs: Closing.RemoteClose.SecondStageTransactions, commitment: FullCommitment): Unit = { val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val remoteCommit = commitment.nextRemoteCommit_opt match { - case Some(c) if remoteCommitPublished.commitTx.txid == c.commit.txid => c.commit + case Some(c) if rcp.commitTx.txid == c.commit.txid => c.commit case _ => commitment.remoteCommit } val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) - val publishAnchorTx_opt = claimAnchorTx_opt match { - case Some(anchorTx) if !remoteCommitPublished.isConfirmed => + val publishAnchorTx_opt = txs.anchorTx_opt match { + case Some(anchorTx) if !rcp.isConfirmed => val confirmationTarget = Closing.confirmationTarget(remoteCommit, commitment.remoteParams.dustLimit, commitment.params.commitmentFormat, nodeParams.onChainFeeConf) - Some(PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, commitTx, commitment), confirmationTarget)) + Some(PublishReplaceableTx(ReplaceableRemoteCommitAnchor(anchorTx, fundingKey, commitKeys, rcp.commitTx, commitment), confirmationTarget)) case _ => None } - val publishMainTx_opt = claimMainOutputTx.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishHtlcTxs = redeemableClaimHtlcTxs(remoteCommitPublished, commitKeys, commitment) + val publishMainTx_opt = txs.mainTx_opt.map(tx => PublishFinalTx(tx, tx.fee, None)) + val publishHtlcTxs = redeemableClaimHtlcTxs(rcp, commitKeys, commitment) val publishQueue = publishAnchorTx_opt ++ publishMainTx_opt ++ publishHtlcTxs - publishIfNeeded(publishQueue, irrevocablySpent) - - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays = Map.empty) - - // We watch outputs of the commitment tx that both parties may spend. - // We also watch our local anchor: this ensures that we will correctly detect when it's confirmed and count its fees - // in the audit DB, even if we restart before confirmation. - val watchSpentQueue = claimHtlcTxs.keys.toSeq ++ publishAnchorTx_opt.map(_.input) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + publishIfNeeded(publishQueue, rcp.irrevocablySpent) + + if (!rcp.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, rcp.commitTx.txid, nodeParams.channelConf.minDepth) + } + + // We watch outputs of the commitment transaction that we may spend: every time we detect a spending transaction, + // we will watch for its confirmation. This ensures that we detect double-spends that could come from: + // - our own RBF attempts + // - remote transactions for outputs that both parties may spend (e.g. HTLCs) + val watchSpentQueue = rcp.claimMainOutputTx.map(_.input.outPoint) ++ rcp.claimAnchorTx_opt.map(_.input.outPoint) ++ rcp.claimHtlcTxs.keys.toSeq + watchSpentIfNeeded(rcp.commitTx, watchSpentQueue, rcp.irrevocablySpent) } private def redeemableClaimHtlcTxs(remoteCommitPublished: RemoteCommitPublished, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment): Iterable[PublishReplaceableTx] = { @@ -380,7 +381,7 @@ trait ErrorHandlers extends CommonHandlers { val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) Closing.RevokedClose.getRemotePerCommitmentSecret(d.commitments.params, channelKeys, d.commitments.remotePerCommitmentSecrets, tx) match { case Some((commitmentNumber, remotePerCommitmentSecret)) => - val revokedCommitPublished = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.params, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) + val (revokedCommitPublished, closingTxs) = Closing.RevokedClose.claimCommitTxOutputs(d.commitments.params, channelKeys, tx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) log.warning(s"txid=${tx.txid} was a revoked commitment, publishing the penalty tx") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput, tx, d.commitments.params.localParams.paysCommitTxFees), "revoked-commit")) val exc = FundingTxSpent(d.channelId, tx.txid) @@ -392,21 +393,23 @@ trait ErrorHandlers extends CommonHandlers { // NB: if there is a revoked commitment, we can't be in DATA_WAIT_FOR_FUNDING_CONFIRMED so we don't have the case where fundingTx is defined case _ => DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, revokedCommitPublished = revokedCommitPublished :: Nil) } - goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished) sending error + goto(CLOSING) using nextData storing() calling doPublish(revokedCommitPublished, closingTxs) sending error case None => d match { case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => log.warning(s"they published a future commit (because we asked them to) in txid=${tx.txid}") context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput, tx, d.commitments.latest.localParams.paysCommitTxFees), "future-remote-commit")) val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val commitKeys = d.commitments.latest.remoteKeys(channelKeys, remotePerCommitmentPoint) + val mainTx_opt = Closing.RemoteClose.claimMainOutput(d.commitments.params, commitKeys, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val remoteCommitPublished = RemoteCommitPublished( commitTx = tx, - claimMainOutputTx = Closing.RemoteClose.claimMainOutput(d.commitments.params, commitKeys, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey), + claimMainOutputTx = mainTx_opt, claimHtlcTxs = Map.empty, claimAnchorTxs = List.empty, irrevocablySpent = Map.empty) + val closingTxs = Closing.RemoteClose.SecondStageTransactions(mainTx_opt, anchorTx_opt = None, htlcTxs = Nil) val nextData = DATA_CLOSING(d.commitments, waitingSince = nodeParams.currentBlockHeight, finalScriptPubKey = finalScriptPubKey, mutualCloseProposed = Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, d.commitments.latest) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, d.commitments.latest) case _ => // the published tx doesn't seem to be a valid commitment transaction log.error(s"couldn't identify txid=${tx.txid}, something very bad is going on!!!") @@ -416,21 +419,27 @@ trait ErrorHandlers extends CommonHandlers { } } - def doPublish(revokedCommitPublished: RevokedCommitPublished): Unit = { - import revokedCommitPublished._ + /** Publish 2nd-stage transactions for a revoked remote commitment. */ + def doPublish(rvk: RevokedCommitPublished, txs: Closing.RevokedClose.SecondStageTransactions): Unit = { + val publishQueue = (txs.mainTx_opt ++ txs.mainPenaltyTx_opt ++ txs.htlcPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) + publishIfNeeded(publishQueue, rvk.irrevocablySpent) - val publishQueue = (claimMainOutputTx ++ mainPenaltyTx ++ htlcPenaltyTxs ++ claimHtlcDelayedPenaltyTxs).map(tx => PublishFinalTx(tx, tx.fee, None)) - publishIfNeeded(publishQueue, irrevocablySpent) + if (!rvk.isConfirmed) { + // We watch the commitment transaction: once confirmed, it invalidates other types of force-close. + blockchain ! WatchTxConfirmed(self, rvk.commitTx.txid, nodeParams.channelConf.minDepth) + } - // We watch: - // - the commitment tx itself, so that we can handle the case where we don't have any outputs - // - 'final txs' that send funds to our wallet and that spend outputs that only us control - val watchConfirmedQueue = List(commitTx) ++ claimMainOutputTx.map(_.tx) - watchConfirmedIfNeeded(watchConfirmedQueue, irrevocablySpent, relativeDelays = Map.empty) + // We watch outputs of the commitment tx that both parties may spend, or that we may RBF. + val watchSpentQueue = (rvk.claimMainOutputTx ++ rvk.mainPenaltyTx ++ rvk.htlcPenaltyTxs).map(_.input.outPoint) + watchSpentIfNeeded(rvk.commitTx, watchSpentQueue, rvk.irrevocablySpent) + } - // We watch outputs of the commitment tx that both parties may spend. - val watchSpentQueue = (mainPenaltyTx ++ htlcPenaltyTxs).map(_.input.outPoint) - watchSpentIfNeeded(commitTx, watchSpentQueue, irrevocablySpent) + /** Publish 3rd-stage transactions for a revoked remote commitment. */ + def doPublish(rvk: RevokedCommitPublished, txs: Closing.RevokedClose.ThirdStageTransactions): Unit = { + val publishQueue = txs.htlcDelayedPenaltyTxs.map(tx => PublishFinalTx(tx, tx.fee, None)) + publishIfNeeded(publishQueue, rvk.irrevocablySpent) + // We watch the spent outputs to detect our own RBF attempts. + txs.htlcDelayedPenaltyTxs.foreach(tx => watchSpentIfNeeded(tx.input, rvk.irrevocablySpent)) } def handleOutdatedCommitment(channelReestablish: ChannelReestablish, d: ChannelDataWithCommitments) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index 36413742a3..af5cbd3b7c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala @@ -10,14 +10,15 @@ import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.channel.Helpers.Closing.{CurrentRemoteClose, LocalClose} import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.channel.{CLOSING, CMD_SIGN, DATA_CLOSING, DATA_NORMAL, Upstream} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ import fr.acinq.eclair.db.pg.PgUtils.using +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.protocol.{CommitSig, Error, RevokeAndAck, TlvStream, UpdateAddHtlc, UpdateAddHtlcTlv} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampMilli, ToMilliSatoshiConversion, randomBytes32} -import org.scalatest.{Outcome, Tag} import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import org.scalatest.{Outcome, Tag} import org.sqlite.SQLiteConfig import java.io.File @@ -168,13 +169,13 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ // We add 4 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice - val (_, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) // for this one we set a non-local upstream to simulate a relayed payment - val (_, htlca4) = addHtlc(30000000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Hot.Trampoline(Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 42, 30003000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L), TestConstants.Alice.nodeParams.nodeId) :: Nil), replyTo = TestProbe().ref) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (_, _) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + val (_, htlca4) = addHtlc(30_000_000 msat, CltvExpiryDelta(144), alice, bob, alice2bob, bob2alice, upstream = Upstream.Hot.Trampoline(Upstream.Hot.Channel(UpdateAddHtlc(randomBytes32(), 42, 30_003_000 msat, randomBytes32(), CltvExpiry(144), TestConstants.emptyOnionPacket, TlvStream.empty[UpdateAddHtlcTlv]), TimestampMilli(1687345927000L), TestConstants.Alice.nodeParams.nodeId) :: Nil), replyTo = TestProbe().ref) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) // And fulfill one htlc in each direction without signing a new commit tx fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) @@ -183,7 +184,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.last.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 7) // two main outputs and 5 pending htlcs (one is dust) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -197,34 +198,29 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with htlcsUnpublished = htlca4.amountMsat.truncateToSatoshi + htlcb1.amountMsat.truncateToSatoshi )) - alice2blockchain.expectMsgType[PublishFinalTx] // claim-main - val htlcTx1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx2 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx3 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx4 = alice2blockchain.expectMsgType[PublishFinalTx] - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - alice2blockchain.expectMsgType[WatchTxConfirmed] // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 4 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 5 - - // 3rd-stage txs are published when htlc-timeout txs confirm - val claimHtlcDelayedTxs = Seq(htlcTx1, htlcTx2, htlcTx3, htlcTx4).map { htlcTimeoutTx => - alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.tx.txid) - claimHtlcDelayedTx + val mainTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + val htlcTx1 = alice2blockchain.expectFinalTxPublished("htlc-timeout") + val htlcTx2 = alice2blockchain.expectFinalTxPublished("htlc-success") + val htlcTx3 = alice2blockchain.expectFinalTxPublished("htlc-timeout") + val htlcTx4 = alice2blockchain.expectFinalTxPublished("htlc-timeout") + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(mainTx.input +: localCommitPublished.htlcTxs.keys.toSeq) + + // 3rd-stage txs are published when htlc transactions confirm + val htlcDelayedTxs = Seq(htlcTx1, htlcTx2, htlcTx3, htlcTx4).map { htlcTx => + alice ! WatchOutputSpentTriggered(htlcTx.amount, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTx.tx) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + htlcDelayedTx } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) assert(CheckBalance.computeLocalCloseBalance(commitments.changes, LocalClose(commitments.active.last.localCommit, alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get), commitments.originChannels, knownPreimages) == PossiblyPublishedMainAndHtlcBalance( toLocal = Map(OutPoint(localCommitPublished.claimMainDelayedOutputTx.get.tx.txid, 0) -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount), - htlcs = claimHtlcDelayedTxs.map(claimTx => OutPoint(claimTx.tx.txid, 0) -> claimTx.tx.txOut.head.amount.toBtc).toMap, + htlcs = htlcDelayedTxs.map(claimTx => OutPoint(claimTx.tx.txid, 0) -> claimTx.tx.txOut.head.amount.toBtc).toMap, htlcsUnpublished = 0.sat )) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 768cf132dd..b5833de43a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -245,39 +245,6 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind }) } - test("watch for confirmed transactions with relative delay") { - withWatcher(f => { - import f._ - - // We simulate a transaction with a 3-blocks CSV delay. - val (priv, address) = createExternalAddress() - val parentTx = sendToAddress(address, 50.millibtc, probe) - val tx = createSpendP2WPKH(parentTx, priv, priv.publicKey, 5_000 sat, 3, 0) - val delay = RelativeDelay(parentTx.txid, 3) - - watcher ! WatchTxConfirmed(probe.ref, tx.txid, 6, Some(delay)) - probe.expectNoMessage(100 millis) - - // We make the parent tx confirm to satisfy the CSV delay and publish the delayed transaction. - generateBlocks(3) - bitcoinClient.publishTransaction(tx).pipeTo(probe.ref) - probe.expectMsg(tx.txid) - probe.expectNoMessage(100 millis) - - // The delayed transaction confirms, but hasn't reached its minimum depth. - generateBlocks(3) - probe.expectNoMessage(100 millis) - - // The delayed transaction reaches its minimum depth. - generateBlocks(3) - assert(probe.expectMsgType[WatchTxConfirmedTriggered].tx.txid == tx.txid) - - // If we watch the transaction when it's already confirmed, we immediately receive the WatchConfirmedTriggered event. - watcher ! WatchTxConfirmed(probe.ref, tx.txid, 3, Some(delay.copy(delay = 720))) - assert(probe.expectMsgType[WatchTxConfirmedTriggered].tx.txid == tx.txid) - }) - } - test("watch for spent transactions") { withWatcher(f => { import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala deleted file mode 100644 index a016e29b87..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelDataSpec.scala +++ /dev/null @@ -1,619 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.channel - -import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, SatoshiLong, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered -import fr.acinq.eclair.channel.Helpers.Closing -import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.crypto.keymanager.ChannelKeys -import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{CommitSig, FailureReason, RevokeAndAck, UnknownNextPeer, UpdateAddHtlc} -import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshiLong, NodeParams, TestKitBaseClass} -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector - -class ChannelDataSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStateTestsBase { - - implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging - - case class HtlcWithPreimage(preimage: ByteVector32, htlc: UpdateAddHtlc) - - case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, probe: TestProbe) - - private def setupClosingChannel(testTags: Set[String] = Set.empty): Fixture = { - val probe = TestProbe() - val setup = init() - reachNormal(setup, testTags) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb1.id, rb1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca1.id, ra1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - - // Alice publishes her commitment. - alice ! CMD_FORCECLOSE(probe.ref) - probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] - awaitCond(alice.stateName == CLOSING) - - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - - Fixture(alice, HtlcWithPreimage(rb2, htlcb2), bob, HtlcWithPreimage(ra2, htlca2), TestProbe()) - } - - case class LocalFixture(nodeParams: NodeParams, channelKeys: ChannelKeys, alice: TestFSMRef[ChannelState, ChannelData, Channel], alicePendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, htlcTimeoutTxs: Seq[HtlcTimeoutTx], htlcSuccessTxs: Seq[HtlcSuccessTx], probe: TestProbe) { - val aliceClosing: DATA_CLOSING = alice.stateData.asInstanceOf[DATA_CLOSING] - } - - private def setupClosingChannelForLocalClose(): LocalFixture = { - val f = setupClosingChannel() - import f._ - - val nodeParams = alice.underlyingActor.nodeParams - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.localCommitPublished.nonEmpty) - val lcp = aliceClosing.localCommitPublished.get - assert(lcp.commitTx.txOut.length == 6) - assert(lcp.claimMainDelayedOutputTx.nonEmpty) - assert(lcp.htlcTxs.size == 4) // we have one entry for each non-dust htlc - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) - assert(htlcTimeoutTxs.length == 2) - val htlcSuccessTxs = getHtlcSuccessTxs(lcp) - assert(htlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = lcp.htlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(lcp.claimHtlcDelayedTxs.isEmpty) // we will publish 3rd-stage txs once htlc txs confirm - assert(!lcp.isConfirmed) - assert(!lcp.isDone) - - // Commit tx has been confirmed. - val lcp1 = Closing.updateLocalCommitPublished(lcp, lcp.commitTx) - assert(lcp1.irrevocablySpent.nonEmpty) - assert(lcp1.isConfirmed) - assert(!lcp1.isDone) - - // Main output has been confirmed. - val lcp2 = Closing.updateLocalCommitPublished(lcp1, lcp.claimMainDelayedOutputTx.get.tx) - assert(lcp2.isConfirmed) - assert(!lcp2.isDone) - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.remoteCommitPublished.nonEmpty) - val rcp = bobClosing.remoteCommitPublished.get - - LocalFixture(nodeParams, alice.underlyingActor.channelKeys, f.alice, alicePendingHtlc, remainingHtlcOutpoint, lcp2, rcp, htlcTimeoutTxs, htlcSuccessTxs, probe) - } - - test("local commit published (our HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, channelKeys, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - assert(theirClaimHtlcTimeout !== None) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, theirClaimHtlcTimeout.get.tx) - assert(lcp5.isDone) - } - - test("local commit published (our HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, channelKeys, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - alice ! CMD_FULFILL_HTLC(alicePendingHtlc.htlc.id, alicePendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(lcp5.htlcTxs(remainingHtlcOutpoint) !== None) - assert(lcp5.claimHtlcDelayedTxs.length == 3) - - val newHtlcSuccessTx = lcp5.htlcTxs(remainingHtlcOutpoint).get.tx - val (lcp6, Some(newClaimHtlcDelayedTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp5, channelKeys, aliceClosing.commitments.latest, newHtlcSuccessTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - assert(lcp6.claimHtlcDelayedTxs.length == 4) - - val lcp7 = Closing.updateLocalCommitPublished(lcp6, newHtlcSuccessTx) - assert(!lcp7.isDone) - - val lcp8 = Closing.updateLocalCommitPublished(lcp7, newClaimHtlcDelayedTx.tx) - assert(lcp8.isDone) - } - - test("local commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val remoteHtlcSuccess = rcp.claimHtlcTxs.values.collectFirst { case Some(tx: ClaimHtlcSuccessTx) => tx }.get - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ Seq(remoteHtlcSuccess.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, _) = Closing.LocalClose.claimHtlcDelayedOutput(current, channelKeys, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(lcp3.claimHtlcDelayedTxs.length == 1) - assert(!lcp3.isDone) - - val lcp4 = Closing.updateLocalCommitPublished(lcp3, lcp3.claimHtlcDelayedTxs.head.tx) - assert(!lcp4.isDone) - - val remainingHtlcTimeoutTxs = htlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingHtlcTimeoutTxs.length == 1) - val (lcp5, Some(remainingClaimHtlcTx)) = Closing.LocalClose.claimHtlcDelayedOutput(lcp4, channelKeys, aliceClosing.commitments.latest, remainingHtlcTimeoutTxs.head.tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - assert(lcp5.claimHtlcDelayedTxs.length == 2) - - val lcp6 = (remainingHtlcTimeoutTxs.map(_.tx) ++ Seq(remainingClaimHtlcTx.tx)).foldLeft(lcp5) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp6.isDone) - - val theirClaimHtlcTimeout = rcp.claimHtlcTxs(remainingHtlcOutpoint) - val lcp7 = Closing.updateLocalCommitPublished(lcp6, theirClaimHtlcTimeout.get.tx) - assert(lcp7.isDone) - } - - test("local commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = htlcTimeoutTxs.map(_.tx).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, channelKeys, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 2) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - val remoteHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp).map(_.tx) - assert(remoteHtlcTimeoutTxs.length == 2) - val lcp5 = Closing.updateLocalCommitPublished(lcp4, remoteHtlcTimeoutTxs.head) - assert(!lcp5.isDone) - - val lcp6 = Closing.updateLocalCommitPublished(lcp5, remoteHtlcTimeoutTxs.last) - assert(lcp6.isDone) - } - - test("local commit published (our HTLC txs are confirmed and the remaining HTLC is failed)") { - val f = setupClosingChannelForLocalClose() - import f._ - - val lcp3 = (htlcSuccessTxs.map(_.tx) ++ htlcTimeoutTxs.map(_.tx)).foldLeft(lcp) { - case (current, tx) => - val (current1, Some(_)) = Closing.LocalClose.claimHtlcDelayedOutput(current, channelKeys, aliceClosing.commitments.latest, tx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, aliceClosing.finalScriptPubKey) - Closing.updateLocalCommitPublished(current1, tx) - } - - assert(!lcp3.isDone) - assert(lcp3.claimHtlcDelayedTxs.length == 3) - - val lcp4 = lcp3.claimHtlcDelayedTxs.map(_.tx).foldLeft(lcp3) { - case (current, tx) => Closing.updateLocalCommitPublished(current, tx) - } - assert(!lcp4.isDone) - - // at this point the pending incoming htlc is waiting for a preimage - assert(lcp4.htlcTxs(remainingHtlcOutpoint).isEmpty) - - alice ! CMD_FAIL_HTLC(1, FailureReason.LocalFailure(UnknownNextPeer()), None, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] - val aliceClosing1 = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp5 = aliceClosing1.localCommitPublished.get.copy(irrevocablySpent = lcp4.irrevocablySpent, claimHtlcDelayedTxs = lcp4.claimHtlcDelayedTxs) - assert(!lcp5.htlcTxs.contains(remainingHtlcOutpoint)) - assert(lcp5.claimHtlcDelayedTxs.length == 3) - - assert(lcp5.isDone) - } - - case class RemoteFixture(bob: TestFSMRef[ChannelState, ChannelData, Channel], bobPendingHtlc: HtlcWithPreimage, remainingHtlcOutpoint: OutPoint, lcp: LocalCommitPublished, rcp: RemoteCommitPublished, claimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], claimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], probe: TestProbe) - - private def setupClosingChannelForRemoteClose(): RemoteFixture = { - val f = setupClosingChannel(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.remoteCommitPublished.nonEmpty) - val rcp = bobClosing.remoteCommitPublished.get - assert(rcp.commitTx.txOut.length == 8) - assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length == 2) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(!rcp.isConfirmed) - assert(!rcp.isDone) - - // Commit tx has been confirmed. - val rcp1 = Closing.updateRemoteCommitPublished(rcp, rcp.commitTx) - assert(rcp1.irrevocablySpent.nonEmpty) - assert(rcp1.isConfirmed) - assert(!rcp1.isDone) - - // Main output has been confirmed. - val rcp2 = Closing.updateRemoteCommitPublished(rcp1, rcp.claimMainOutputTx.get.tx) - assert(rcp2.isConfirmed) - assert(!rcp2.isDone) - - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.localCommitPublished.nonEmpty) - val lcp = aliceClosing.localCommitPublished.get - - RemoteFixture(f.bob, f.bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) - } - - test("remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) - } - - test("remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) - } - - test("remote commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length == 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) - } - - test("remote commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) - } - - test("remote commit published (our claim-HTLC txs are confirmed and the remaining one is failed)") { - val f = setupClosingChannelForRemoteClose() - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FAIL_HTLC(bobPendingHtlc.htlc.id, FailureReason.LocalFailure(UnknownNextPeer()), None, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FAIL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.remoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(!rcp4.claimHtlcTxs.contains(remainingHtlcOutpoint)) - assert(rcp4.claimHtlcTxs.size == 3) - assert(getClaimHtlcSuccessTxs(rcp4).size == 1) - assert(getClaimHtlcTimeoutTxs(rcp4).size == 2) - - assert(rcp4.isDone) - } - - private def setupClosingChannelForNextRemoteClose(tags: Set[String] = Set.empty): RemoteFixture = { - val probe = TestProbe() - val setup = init(tags = tags) - reachNormal(setup, tags = tags) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - bob ! CMD_SIGN(Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_SIGN]] - bob2alice.expectMsgType[CommitSig] - bob2alice.forward(alice) - alice2bob.expectMsgType[RevokeAndAck] - - // Alice and Bob both know the preimage for only one of the two HTLCs they received. - alice ! CMD_FULFILL_HTLC(htlcb1.id, rb1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - bob ! CMD_FULFILL_HTLC(htlca1.id, ra1, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - - // Alice publishes her last commitment. - alice ! CMD_FORCECLOSE(probe.ref) - probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] - awaitCond(alice.stateName == CLOSING) - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - val lcp = aliceClosing.localCommitPublished.get - - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - - val bobClosing = bob.stateData.asInstanceOf[DATA_CLOSING] - assert(bobClosing.nextRemoteCommitPublished.nonEmpty) - val rcp = bobClosing.nextRemoteCommitPublished.get - assert(rcp.commitTx.txOut.length == 8) - assert(rcp.claimMainOutputTx.nonEmpty) - assert(rcp.claimHtlcTxs.size == 4) // we have one entry for each non-dust htlc - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) - assert(claimHtlcTimeoutTxs.length == 2) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) - assert(claimHtlcSuccessTxs.length == 1) // we only have the preimage for 1 of the 2 non-dust htlcs - val remainingHtlcOutpoint = rcp.claimHtlcTxs.collect { case (outpoint, None) => outpoint }.head - assert(!rcp.isConfirmed) - assert(!rcp.isDone) - - // Commit tx has been confirmed. - val rcp1 = Closing.updateRemoteCommitPublished(rcp, rcp.commitTx) - assert(rcp1.irrevocablySpent.nonEmpty) - assert(rcp1.isConfirmed) - assert(!rcp1.isDone) - - // Main output has been confirmed. - val rcp2 = Closing.updateRemoteCommitPublished(rcp1, rcp.claimMainOutputTx.get.tx) - assert(rcp2.isConfirmed) - assert(!rcp2.isDone) - - val bobPendingHtlc = HtlcWithPreimage(ra2, htlca2) - - RemoteFixture(bob, bobPendingHtlc, remainingHtlcOutpoint, lcp, rcp2, claimHtlcTimeoutTxs, claimHtlcSuccessTxs, probe) - } - - test("next remote commit published (our claim-HTLC txs are confirmed, they claim the remaining HTLC)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, theirHtlcTimeout.get.tx) - assert(rcp4.isDone) - } - - test("next remote commit published (our claim-HTLC txs are confirmed and we claim the remaining HTLC)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = (claimHtlcSuccessTxs ++ claimHtlcTimeoutTxs).map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - bob ! CMD_FULFILL_HTLC(bobPendingHtlc.htlc.id, bobPendingHtlc.preimage, replyTo_opt = Some(probe.ref)) - probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] - val bobClosing1 = bob.stateData.asInstanceOf[DATA_CLOSING] - val rcp4 = bobClosing1.nextRemoteCommitPublished.get.copy(irrevocablySpent = rcp3.irrevocablySpent) - assert(rcp4.claimHtlcTxs(remainingHtlcOutpoint) !== None) - val newClaimHtlcSuccessTx = rcp4.claimHtlcTxs(remainingHtlcOutpoint).get - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, newClaimHtlcSuccessTx.tx) - assert(rcp5.isDone) - } - - test("next remote commit published (they fulfill one of the HTLCs we sent them)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val remoteHtlcSuccess = lcp.htlcTxs.values.collectFirst { case Some(tx: HtlcSuccessTx) => tx }.get - val rcp3 = (remoteHtlcSuccess.tx +: claimHtlcSuccessTxs.map(_.tx)).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val remainingClaimHtlcTimeoutTx = claimHtlcTimeoutTxs.filter(_.input.outPoint != remoteHtlcSuccess.input.outPoint) - assert(remainingClaimHtlcTimeoutTx.length == 1) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, remainingClaimHtlcTimeoutTx.head.tx) - assert(!rcp4.isDone) - - val theirHtlcTimeout = lcp.htlcTxs(remainingHtlcOutpoint) - assert(theirHtlcTimeout !== None) - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, theirHtlcTimeout.get.tx) - assert(rcp5.isDone) - } - - test("next remote commit published (they get back the HTLCs they sent us)") { - val f = setupClosingChannelForNextRemoteClose(Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import f._ - - val rcp3 = claimHtlcTimeoutTxs.map(_.tx).foldLeft(rcp) { - case (current, tx) => Closing.updateRemoteCommitPublished(current, tx) - } - assert(!rcp3.isDone) - - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp).map(_.tx) - val rcp4 = Closing.updateRemoteCommitPublished(rcp3, htlcTimeoutTxs.head) - assert(!rcp4.isDone) - - val rcp5 = Closing.updateRemoteCommitPublished(rcp4, htlcTimeoutTxs.last) - assert(rcp5.isDone) - } - - test("revoked commit published") { - val setup = init(tags = Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - reachNormal(setup, tags = Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs, ChannelStateTestsTags.StaticRemoteKey)) - import setup._ - awaitCond(alice.stateName == NORMAL) - awaitCond(bob.stateName == NORMAL) - val (ra1, htlca1) = addHtlc(15_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(16_000_000 msat, alice, bob, alice2bob, bob2alice) - addHtlc(500_000 msat, alice, bob, alice2bob, bob2alice) // below dust - crossSign(alice, bob, alice2bob, bob2alice) - addHtlc(17_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) - addHtlc(400_000 msat, bob, alice, bob2alice, alice2bob) // below dust - crossSign(bob, alice, bob2alice, alice2bob) - val revokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - - alice ! WatchFundingSpentTriggered(revokedCommitTx) - awaitCond(alice.stateName == CLOSING) - val aliceClosing = alice.stateData.asInstanceOf[DATA_CLOSING] - assert(aliceClosing.revokedCommitPublished.length == 1) - val rvk = aliceClosing.revokedCommitPublished.head - assert(rvk.claimMainOutputTx.nonEmpty) - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.length == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) - assert(!rvk.isDone) - - // Commit tx has been confirmed. - val rvk1 = Closing.updateRevokedCommitPublished(rvk, rvk.commitTx) - assert(rvk1.irrevocablySpent.nonEmpty) - assert(!rvk1.isDone) - - // Main output has been confirmed. - val rvk2 = Closing.updateRevokedCommitPublished(rvk1, rvk.claimMainOutputTx.get.tx) - assert(!rvk2.isDone) - - // Two of our htlc penalty txs have been confirmed. - val rvk3 = rvk.htlcPenaltyTxs.map(_.tx).take(2).foldLeft(rvk2) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - } - assert(!rvk3.isDone) - - // Scenario 1: the remaining penalty txs have been confirmed. - { - val rvk4a = rvk.htlcPenaltyTxs.map(_.tx).drop(2).foldLeft(rvk3) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - } - assert(!rvk4a.isDone) - - val rvk4b = Closing.updateRevokedCommitPublished(rvk4a, rvk.mainPenaltyTx.get.tx) - assert(rvk4b.isDone) - } - - // Scenario 2: they claim the remaining outputs. - { - val remoteMainOutput = rvk.mainPenaltyTx.get.tx.copy(txOut = Seq(TxOut(35_000 sat, ByteVector.empty))) - val rvk4a = Closing.updateRevokedCommitPublished(rvk3, remoteMainOutput) - assert(!rvk4a.isDone) - - val htlcSuccess = rvk.htlcPenaltyTxs(2).tx.copy(txOut = Seq(TxOut(3_000 sat, ByteVector.empty), TxOut(2_500 sat, ByteVector.empty))) - val htlcTimeout = rvk.htlcPenaltyTxs(3).tx.copy(txOut = Seq(TxOut(3_500 sat, ByteVector.empty), TxOut(3_100 sat, ByteVector.empty))) - // When Bob claims these outputs, the channel should call Helpers.claimRevokedHtlcTxOutputs to punish them by claiming the output of their htlc tx. - // This is tested in ClosingStateSpec. - val rvk4b = Seq(htlcSuccess, htlcTimeout).foldLeft(rvk4a) { - case (current, tx) => Closing.updateRevokedCommitPublished(current, tx) - }.copy( - claimHtlcDelayedPenaltyTxs = List( - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcSuccess, 0), TxOut(2_500 sat, Nil), ByteVector.empty), Transaction(2, Seq(TxIn(OutPoint(htlcSuccess, 0), ByteVector.empty, 0)), Seq(TxOut(5_000 sat, ByteVector.empty)), 0), CltvExpiryDelta(720)), - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(OutPoint(htlcTimeout, 0), TxOut(3_000 sat, Nil), ByteVector.empty), Transaction(2, Seq(TxIn(OutPoint(htlcTimeout, 0), ByteVector.empty, 0)), Seq(TxOut(6_000 sat, ByteVector.empty)), 0), CltvExpiryDelta(720)) - ) - ) - assert(!rvk4b.isDone) - - // We claim one of the remaining outputs, they claim the other. - val rvk5a = Closing.updateRevokedCommitPublished(rvk4b, rvk4b.claimHtlcDelayedPenaltyTxs.head.tx) - assert(!rvk5a.isDone) - val theirClaimHtlcTimeout = rvk4b.claimHtlcDelayedPenaltyTxs(1).tx.copy(txOut = Seq(TxOut(1_500.sat, ByteVector.empty), TxOut(2_500.sat, ByteVector.empty))) - val rvk5b = Closing.updateRevokedCommitPublished(rvk5a, theirClaimHtlcTimeout) - assert(rvk5b.isDone) - } - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 04353d75e1..23e6baabd6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala @@ -23,7 +23,10 @@ import fr.acinq.eclair.TestUtils.NoLoggingDiagnostics import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchFundingSpentTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.publish.TxPublisher.PublishReplaceableTx +import fr.acinq.eclair.channel.publish.{ReplaceableHtlc, ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong, randomKey} @@ -47,7 +50,15 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat Helpers.nextChannelUpdateRefresh(TimestampSecond.now()).toSeconds should equal(10 * 24 * 3600L +- 100) } - case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], aliceCommitPublished: LocalCommitPublished, aliceHtlcs: Set[UpdateAddHtlc], bob: TestFSMRef[ChannelState, ChannelData, Channel], bobCommitPublished: RemoteCommitPublished, bobHtlcs: Set[UpdateAddHtlc], probe: TestProbe) + case class Fixture(alice: TestFSMRef[ChannelState, ChannelData, Channel], + aliceHtlcs: Set[UpdateAddHtlc], + aliceHtlcSuccessTxs: Seq[HtlcSuccessTx], + aliceHtlcTimeoutTxs: Seq[HtlcTimeoutTx], + bob: TestFSMRef[ChannelState, ChannelData, Channel], + bobHtlcs: Set[UpdateAddHtlc], + bobClaimHtlcSuccessTxs: Seq[ClaimHtlcSuccessTx], + bobClaimHtlcTimeoutTxs: Seq[ClaimHtlcTimeoutTx], + probe: TestProbe) def setupHtlcs(testTags: Set[String] = Set.empty): Fixture = { val probe = TestProbe() @@ -80,27 +91,38 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat // Alice publishes her commitment. alice ! CMD_FORCECLOSE(probe.ref) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx") awaitCond(alice.stateName == CLOSING) - // Bob detects it. - bob ! WatchFundingSpentTriggered(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.commitTx) - awaitCond(bob.stateName == CLOSING) - val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get + lcp.claimAnchorTx_opt.foreach(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor]) + lcp.claimMainDelayedOutputTx.foreach(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")) + // Alice is missing the preimage for 2 of the HTLCs she received. assert(lcp.htlcTxs.size == 6) - val htlcTimeoutTxs = getHtlcTimeoutTxs(lcp) + val htlcTxs = (0 until 4).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]).map(_.tx).collect { case tx: ReplaceableHtlc => tx.sign(Map.empty) } + alice2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) + val htlcTimeoutTxs = htlcTxs.map(_.txInfo).collect { case tx: HtlcTimeoutTx => tx } assert(htlcTimeoutTxs.length == 3) - val htlcSuccessTxs = getHtlcSuccessTxs(lcp) + val htlcSuccessTxs = htlcTxs.map(_.txInfo).collect { case tx: HtlcSuccessTx => tx } assert(htlcSuccessTxs.length == 1) + // Bob detects Alice's force-close. + bob ! WatchFundingSpentTriggered(commitTx.tx) + awaitCond(bob.stateName == CLOSING) + val rcp = bob.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get + rcp.claimAnchorTx_opt.foreach(_ => bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor]) + rcp.claimMainOutputTx.foreach(_ => bob2blockchain.expectFinalTxPublished("remote-main-delayed")) + // Bob is missing the preimage for 2 of the HTLCs she received. assert(rcp.claimHtlcTxs.size == 6) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(rcp) + val claimHtlcTxs = (0 until 4).map(_ => bob2blockchain.expectMsgType[PublishReplaceableTx]) + bob2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) + val claimHtlcTimeoutTxs = claimHtlcTxs.map(_.tx.txInfo).collect { case tx: ClaimHtlcTimeoutTx => tx } assert(claimHtlcTimeoutTxs.length == 3) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(rcp) + val claimHtlcSuccessTxs = claimHtlcTxs.map(_.tx.txInfo).collect { case tx: ClaimHtlcSuccessTx => tx } assert(claimHtlcSuccessTxs.length == 1) - Fixture(alice, lcp, Set(htlca1a, htlca1b, htlca2), bob, rcp, Set(htlcb1a, htlcb1b, htlcb2), probe) + Fixture(alice, Set(htlca1a, htlca1b, htlca2), htlcSuccessTxs, htlcTimeoutTxs, bob, Set(htlcb1a, htlcb1b, htlcb2), claimHtlcSuccessTxs, claimHtlcTimeoutTxs, probe) } def findTimedOutHtlcs(f: Fixture): Unit = { @@ -113,47 +135,39 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat val remoteCommit = remoteCommitment.remoteCommit val commitmentFormat = remoteCommitment.params.commitmentFormat - val htlcTimeoutTxs = getHtlcTimeoutTxs(aliceCommitPublished) - val htlcSuccessTxs = getHtlcSuccessTxs(aliceCommitPublished) // Claim-HTLC txs can be modified to pay more (or less) fees by changing the output amount. - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(bobCommitPublished) - val claimHtlcTimeoutTxsModifiedFees = claimHtlcTimeoutTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) - val claimHtlcSuccessTxs = getClaimHtlcSuccessTxs(bobCommitPublished) - val claimHtlcSuccessTxsModifiedFees = claimHtlcSuccessTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) + val bobClaimHtlcTimeoutTxsModifiedFees = bobClaimHtlcTimeoutTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) + val bobClaimHtlcSuccessTxsModifiedFees = bobClaimHtlcSuccessTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) - val aliceTimedOutHtlcs = htlcTimeoutTxs.map(htlcTimeout => { + val aliceTimedOutHtlcs = aliceHtlcTimeoutTxs.map(htlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, htlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(aliceTimedOutHtlcs.toSet == aliceHtlcs) - val bobTimedOutHtlcs = claimHtlcTimeoutTxs.map(claimHtlcTimeout => { + val bobTimedOutHtlcs = bobClaimHtlcTimeoutTxs.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs.toSet == bobHtlcs) - val bobTimedOutHtlcs2 = claimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { + val bobTimedOutHtlcs2 = bobClaimHtlcTimeoutTxsModifiedFees.map(claimHtlcTimeout => { val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) assert(bobTimedOutHtlcs2.toSet == bobHtlcs) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, htlcSuccess.tx).isEmpty)) - htlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) - claimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) - htlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcTimeout.tx).isEmpty)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcTimeout.tx).isEmpty)) - } - - test("find timed out htlcs") { - findTimedOutHtlcs(setupHtlcs()) + aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, htlcSuccess.tx).isEmpty)) + aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) + aliceHtlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcTimeout.tx).isEmpty)) + bobClaimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcTimeout.tx).isEmpty)) } test("find timed out htlcs (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index bd93900e7f..b416b8fc78 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -37,6 +37,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateFee} @@ -1079,18 +1080,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsg(signedCommitTx.txid) generateBlocks(1) - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableLocalCommitAnchor]) - alice2blockchain.expectMsgType[PublishFinalTx] // claim main output + val anchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val main = alice2blockchain.expectFinalTxPublished("local-main-delayed") val htlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(htlcSuccess.tx.isInstanceOf[ReplaceableHtlcSuccess]) val htlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(htlcTimeout.tx.isInstanceOf[ReplaceableHtlcTimeout]) - - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim main output - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-anchor tx - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-success tx - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-timeout tx + alice2blockchain.expectWatchTxConfirmed(signedCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(main.input, anchor.txInfo.input.outPoint, htlcSuccess.input, htlcTimeout.input)) alice2blockchain.expectNoMessage(100 millis) (signedCommitTx, htlcSuccess, htlcTimeout) @@ -1615,19 +1612,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val anchorTx_opt = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => None - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(alice2blockchain.expectMsgType[PublishReplaceableTx]) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor]) } - if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[PublishFinalTx] // claim main output + val mainTx_opt = if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectFinalTxPublished("remote-main-delayed")) else None val claimHtlcSuccess = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(claimHtlcSuccess.tx.isInstanceOf[ReplaceableClaimHtlcSuccess]) val claimHtlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(claimHtlcTimeout.tx.isInstanceOf[ReplaceableClaimHtlcTimeout]) - - alice2blockchain.expectMsgType[WatchTxConfirmed] // commit tx - if (!bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures.paysDirectlyToWallet) alice2blockchain.expectMsgType[WatchTxConfirmed] // claim main output - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-htlc-success tx - alice2blockchain.expectMsgType[WatchOutputSpent] // claim-htlc-timeout tx - anchorTx_opt.foreach(anchor => alice2blockchain.expectMsgType[WatchOutputSpent]) + alice2blockchain.expectWatchTxConfirmed(remoteCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(mainTx_opt.map(_.input).toSeq ++ anchorTx_opt.map(_.txInfo.input.outPoint).toSeq ++ Seq(claimHtlcSuccess.input, claimHtlcTimeout.input)) alice2blockchain.expectNoMessage(100 millis) (remoteCommitTx, claimHtlcSuccess, claimHtlcTimeout) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index 6ea471baf6..ee76294cec 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -30,8 +30,8 @@ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{DummyOnChainWallet, OnChainPubkeyCache, OnChainWallet, SingleKeyOnChainWallet} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel -import fr.acinq.eclair.channel.publish.{ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor, TxPublisher} import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} +import fr.acinq.eclair.channel.publish._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.payment.{Invoice, OutgoingPaymentPacket} @@ -386,6 +386,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { fundingTx } + def channelId(a: TestFSMRef[ChannelState, ChannelData, Channel]): ByteVector32 = a.stateData.channelId + def localOrigin(replyTo: ActorRef): Origin.Hot = Origin.Hot(replyTo, localUpstream()) def localUpstream(): Upstream.Local = Upstream.Local(UUID.randomUUID()) @@ -552,7 +554,11 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } } - def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): LocalCommitPublished = { + case class PublishedForceCloseTxs(mainTx_opt: Option[Transaction], anchorTx_opt: Option[Transaction], htlcSuccessTxs: Seq[Transaction], htlcTimeoutTxs: Seq[Transaction]) { + val htlcTxs: Seq[Transaction] = htlcSuccessTxs ++ htlcTimeoutTxs + } + + def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (LocalCommitPublished, PublishedForceCloseTxs) = { // an error occurs and s publishes its commit tx val localCommit = s.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit // check that we store the local txs without sigs @@ -565,49 +571,74 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val closingState = s.stateData.asInstanceOf[DATA_CLOSING] assert(closingState.localCommitPublished.isDefined) val localCommitPublished = closingState.localCommitPublished.get + // It may be strictly greater if we're waiting for preimages for some of our HTLC-success txs, or if we're ignoring + // HTLCs that where failed downstream or not relayed. + assert(localCommitPublished.htlcTxs.size >= htlcSuccessCount + htlcTimeoutCount) - val publishedLocalCommitTx = s2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx + val publishedLocalCommitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx assert(publishedLocalCommitTx.txid == commitTx.txid) + assert(publishedLocalCommitTx.wtxid != commitTx.wtxid) val commitInput = closingState.commitments.latest.commitInput Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - if (closingState.commitments.params.commitmentFormat.isInstanceOf[Transactions.AnchorOutputsCommitmentFormat]) { - assert(s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableLocalCommitAnchor]) + val publishedAnchorTx_opt = closingState.commitments.params.commitmentFormat match { + case DefaultCommitmentFormat => None + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(s2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor].txInfo.tx) } // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - localCommitPublished.claimMainDelayedOutputTx.foreach(tx => s2blockchain.expectMsg(TxPublisher.PublishFinalTx(tx, tx.fee, None))) - closingState.commitments.params.commitmentFormat match { + val publishedMainTx_opt = localCommitPublished.claimMainDelayedOutputTx.map(_ => s2blockchain.expectFinalTxPublished("local-main-delayed").tx) + val (publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs) = closingState.commitments.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => - // all htlcs success/timeout should be published as-is, without claiming their outputs - s2blockchain.expectMsgAllOf(localCommitPublished.htlcTxs.values.toSeq.collect { case Some(tx) => TxPublisher.PublishFinalTx(tx, tx.fee, Some(commitTx.txid)) }: _*) - assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + // all htlcs success/timeout should be published as-is, we cannot RBF + val publishedHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => + val htlcTx = s2blockchain.expectMsgType[PublishFinalTx] + assert(htlcTx.parentTx_opt.contains(commitTx.txid)) + assert(localCommitPublished.htlcTxs.contains(htlcTx.input)) + assert(htlcTx.desc == "htlc-success" || htlcTx.desc == "htlc-timeout") + htlcTx + } + val successTxs = publishedHtlcTxs.filter(_.desc == "htlc-success").map(_.tx) + val timeoutTxs = publishedHtlcTxs.filter(_.desc == "htlc-timeout").map(_.tx) + (successTxs, timeoutTxs) case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => - // all htlcs success/timeout should be published as replaceable txs, without claiming their outputs - val htlcTxs = localCommitPublished.htlcTxs.values.collect { case Some(tx: HtlcTx) => tx } - val publishedTxs = htlcTxs.map(_ => s2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx]) - assert(publishedTxs.map(_.input).toSet == htlcTxs.map(_.input.outPoint).toSet) - assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + // all htlcs success/timeout should be published as replaceable txs + val publishedHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => + val htlcTx = s2blockchain.expectReplaceableTxPublished[ReplaceableHtlc] + assert(htlcTx.commitTx == publishedLocalCommitTx) + assert(localCommitPublished.htlcTxs.contains(htlcTx.txInfo.input.outPoint)) + htlcTx + } + // the publisher actors will sign those transactions before broadcasting them + val successTxs = publishedHtlcTxs.collect { case tx: ReplaceableHtlcSuccess => tx.sign(Map.empty).txInfo.tx } + val timeoutTxs = publishedHtlcTxs.collect { case tx: ReplaceableHtlcTimeout => tx.sign(Map.empty).txInfo.tx } + (successTxs, timeoutTxs) } + assert(publishedHtlcSuccessTxs.size == htlcSuccessCount) + assert(publishedHtlcTimeoutTxs.size == htlcTimeoutCount) + (publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(htlcTx => Transaction.correctlySpends(htlcTx, publishedLocalCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + // we're not claiming the outputs of htlc txs yet + assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + + // we watch the confirmation of the commitment transaction + s2blockchain.expectWatchTxConfirmed(commitTx.txid) + + // we watch outputs of the commitment tx that we want to claim + localCommitPublished.claimMainDelayedOutputTx.foreach(tx => s2blockchain.expectWatchOutputSpent(tx.input.outPoint)) + localCommitPublished.claimAnchorTx_opt.foreach(tx => s2blockchain.expectWatchOutputSpent(tx.input.outPoint)) + s2blockchain.expectWatchOutputsSpent(localCommitPublished.htlcTxs.keys.toSeq) + s2blockchain.expectNoMessage(100 millis) - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - localCommitPublished.claimMainDelayedOutputTx.foreach(claimMain => { - val watchConfirmed = s2blockchain.expectMsgType[WatchTxConfirmed] - assert(watchConfirmed.txId == claimMain.tx.txid) - assert(watchConfirmed.delay_opt.map(_.parentTxId).contains(publishedLocalCommitTx.txid)) + // once our closing transactions are published, we watch for their confirmation + (publishedMainTx_opt ++ publishedAnchorTx_opt ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => { + s ! WatchOutputSpentTriggered(tx.txOut.headOption.map(_.amount).getOrElse(330 sat), tx) + s2blockchain.expectWatchTxConfirmed(tx.txid) }) - // we watch outputs of the commitment tx that both parties may spend and anchor outputs - val watchedOutputIndexes = localCommitPublished.htlcTxs.keySet.map(_.index) ++ localCommitPublished.claimAnchorTxs.collect { case tx: ClaimAnchorOutputTx => tx.input.outPoint.index } - val spentWatches = watchedOutputIndexes.map(_ => s2blockchain.expectMsgType[WatchOutputSpent]) - spentWatches.foreach(ws => assert(ws.txId == commitTx.txid)) - assert(spentWatches.map(_.outputIndex) == watchedOutputIndexes) - s2blockchain.expectNoMessage(100 millis) - // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state - closingState.localCommitPublished.get + val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx_opt, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs) + (localCommitPublished, publishedTxs) } - def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe): RemoteCommitPublished = { + def remoteClose(rCommitTx: Transaction, s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (RemoteCommitPublished, PublishedForceCloseTxs) = { // we make s believe r unilaterally closed the channel s ! WatchFundingSpentTriggered(rCommitTx) eventually(assert(s.stateName == CLOSING)) @@ -616,50 +647,51 @@ trait ChannelStateTestsBase extends Assertions with Eventually { assert(remoteCommitPublished_opt.isDefined) assert(closingData.localCommitPublished.isEmpty) val remoteCommitPublished = remoteCommitPublished_opt.get + // It may be strictly greater if we're waiting for preimages for some of our HTLC-success txs, or if we're ignoring + // HTLCs that where failed downstream or not relayed. + assert(remoteCommitPublished.claimHtlcTxs.size >= htlcSuccessCount + htlcTimeoutCount) // If anchor outputs is used, we use the anchor output to bump the fees if necessary. - val anchorTx_opt = closingData.commitments.params.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(s2blockchain.expectMsgType[PublishReplaceableTx]) + val publishedAnchorTx_opt = closingData.commitments.params.commitmentFormat match { + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(s2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor].txInfo.tx) case Transactions.DefaultCommitmentFormat => None } - anchorTx_opt.foreach(anchor => assert(anchor.tx.isInstanceOf[ReplaceableRemoteCommitAnchor])) // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => { - Transaction.correctlySpends(claimMain.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - s2blockchain.expectMsg(TxPublisher.PublishFinalTx(claimMain, claimMain.fee, None)) - }) + val publishedMainTx_opt = remoteCommitPublished.claimMainOutputTx.map(_ => s2blockchain.expectFinalTxPublished("remote-main-delayed").tx) + publishedMainTx_opt.foreach(tx => Transaction.correctlySpends(tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // all htlcs success/timeout should be claimed - val claimHtlcTxs = remoteCommitPublished.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcTx) => tx }.toSeq - claimHtlcTxs.foreach(claimHtlc => Transaction.correctlySpends(claimHtlc.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val publishedClaimHtlcTxs = claimHtlcTxs.map(_ => s2blockchain.expectMsgType[PublishReplaceableTx]) - assert(publishedClaimHtlcTxs.map(_.input).toSet == claimHtlcTxs.map(_.input.outPoint).toSet) - - // we watch the confirmation of the "final" transactions that send funds to our wallets (main delayed output and 2nd stage htlc transactions) - assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == rCommitTx.txid) - remoteCommitPublished.claimMainOutputTx.foreach(claimMain => assert(s2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid)) - - // we watch outputs of the commitment tx that both parties may spend - val htlcOutputIndexes = remoteCommitPublished.claimHtlcTxs.keySet.map(_.index) - val spentHtlcWatches = htlcOutputIndexes.map(_ => s2blockchain.expectMsgType[WatchOutputSpent]) - spentHtlcWatches.foreach(ws => assert(ws.txId == rCommitTx.txid)) - assert(spentHtlcWatches.map(_.outputIndex) == htlcOutputIndexes) - anchorTx_opt.foreach(anchor => assert(s2blockchain.expectMsgType[WatchOutputSpent].outputIndex == anchor.input.index)) + val publishedClaimHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => + val claimHtlcTx = s2blockchain.expectMsgType[PublishReplaceableTx] + assert(claimHtlcTx.tx.commitTx == rCommitTx) + assert(remoteCommitPublished.claimHtlcTxs.contains(claimHtlcTx.input)) + Transaction.correctlySpends(claimHtlcTx.tx.txInfo.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx + } + val publishedHtlcSuccessTxs = publishedClaimHtlcTxs.map(_.tx).collect { case tx: ReplaceableClaimHtlcSuccess => tx.txInfo.tx } + assert(publishedHtlcSuccessTxs.size == htlcSuccessCount) + val publishedHtlcTimeoutTxs = publishedClaimHtlcTxs.map(_.tx).collect { case tx: ReplaceableClaimHtlcTimeout => tx.txInfo.tx } + assert(publishedHtlcTimeoutTxs.size == htlcTimeoutCount) + + // we watch the confirmation of the commitment transaction + s2blockchain.expectWatchTxConfirmed(rCommitTx.txid) + + // we watch outputs of the commitment tx that we want to claim + remoteCommitPublished.claimMainOutputTx.foreach(tx => s2blockchain.expectWatchOutputSpent(tx.input.outPoint)) + remoteCommitPublished.claimAnchorTx_opt.foreach(tx => s2blockchain.expectWatchOutputSpent(tx.input.outPoint)) + s2blockchain.expectWatchOutputsSpent(remoteCommitPublished.claimHtlcTxs.keys.toSeq) s2blockchain.expectNoMessage(100 millis) + // once our closing transactions are published, we watch for their confirmation + (publishedMainTx_opt ++ publishedAnchorTx_opt ++ publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(tx => { + s ! WatchOutputSpentTriggered(tx.txOut.headOption.map(_.amount).getOrElse(330 sat), tx) + s2blockchain.expectWatchTxConfirmed(tx.txid) + }) + // s is now in CLOSING state with txs pending for confirmation before going in CLOSED state - remoteCommitPublished + val publishedTxs = PublishedForceCloseTxs(publishedMainTx_opt, publishedAnchorTx_opt, publishedHtlcSuccessTxs, publishedHtlcTimeoutTxs) + (remoteCommitPublished, publishedTxs) } - def channelId(a: TestFSMRef[ChannelState, ChannelData, Channel]): ByteVector32 = a.stateData.channelId - - def getHtlcSuccessTxs(lcp: LocalCommitPublished): Seq[HtlcSuccessTx] = lcp.htlcTxs.values.collect { case Some(tx: HtlcSuccessTx) => tx }.toSeq - - def getHtlcTimeoutTxs(lcp: LocalCommitPublished): Seq[HtlcTimeoutTx] = lcp.htlcTxs.values.collect { case Some(tx: HtlcTimeoutTx) => tx }.toSeq - - def getClaimHtlcSuccessTxs(rcp: RemoteCommitPublished): Seq[ClaimHtlcSuccessTx] = rcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcSuccessTx) => tx }.toSeq - - def getClaimHtlcTimeoutTxs(rcp: RemoteCommitPublished): Seq[ClaimHtlcTimeoutTx] = rcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcTimeoutTx) => tx }.toSeq - } object ChannelStateTestsBase { @@ -669,7 +701,6 @@ object ChannelStateTestsBase { } implicit class PimpTestFSM(private val channel: TestFSMRef[ChannelState, ChannelData, Channel]) { - val nodeParams: NodeParams = channel.underlyingActor.nodeParams def setBitcoinCoreFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates) 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 3fb489ae7e..c76bdf3251 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 @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel.states.c import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, actorRefAdapter} import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} +import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw @@ -28,10 +29,11 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.channel.publish.{ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor, TxPublisher} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} @@ -750,11 +752,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx alice ! WatchFundingSpentTriggered(bobCommitTx.tx) aliceListener.expectMsgType[TransactionPublished] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain.input.txid == bobCommitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.tx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) } @@ -786,11 +788,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture // Bob broadcasts his commit tx. alice ! WatchFundingSpentTriggered(bobCommitTx1) assert(aliceListener.expectMsgType[TransactionPublished].tx.txid == bobCommitTx1.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - 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) + alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) } @@ -806,23 +808,25 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - alice2 ! WatchFundingSpentTriggered(bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMainAlice = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainAlice.input.txid == bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainAlice.tx.txid) + val bobCommitTx = bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + alice2 ! WatchFundingSpentTriggered(bobCommitTx) + alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainAlice.input) 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.latest.localCommit.commitTxAndRemoteSig.commitTx.tx) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMainBob = bob2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainBob.input.txid == aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainBob.tx.txid) + val aliceCommitTx = aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + bob2 ! WatchFundingSpentTriggered(aliceCommitTx) + bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + bob2blockchain.expectWatchOutputSpent(claimMainBob.input) awaitCond(bob2.stateName == CLOSING) } @@ -846,11 +850,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) alice2 ! WatchFundingSpentTriggered(bobCommitTx1) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - 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) + alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainAlice.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice2blockchain.expectWatchOutputSpent(claimMainAlice.input) awaitCond(alice2.stateName == CLOSING) testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) @@ -859,11 +863,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) bob2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) bob2 ! WatchFundingSpentTriggered(aliceCommitTx1) - assert(bob2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - 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) + bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainBob.tx, Seq(aliceCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + bob2blockchain.expectWatchTxConfirmed(aliceCommitTx1.txid) + bob2blockchain.expectWatchOutputSpent(claimMainBob.input) awaitCond(bob2.stateName == CLOSING) } @@ -1109,21 +1113,23 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice ! Error(ByteVector32.Zeroes, "force-closing channel, bye-bye") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableLocalCommitAnchor]) - val claimMainLocal = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainLocal.input.txid == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainLocal.tx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) + val anchorTxLocal = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val claimMainLocal = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMainLocal.tx, Seq(aliceCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainLocal.input) + alice2blockchain.expectWatchOutputSpent(anchorTxLocal.txInfo.input.outPoint) // Bob broadcasts his commit tx as well. val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx) - alice2blockchain.expectMsgType[WatchOutputSpent] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMainRemote = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMainRemote.input.txid == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMainRemote.tx.txid) + val anchorTxRemote = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMainRemote.input) + alice2blockchain.expectWatchOutputSpent(anchorTxRemote.txInfo.input.outPoint) + alice2blockchain.expectNoMessage(100 millis) } test("recv Error (previous tx confirms)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -1143,35 +1149,33 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx2.tx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableLocalCommitAnchor]) - 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) + alice2blockchain.expectFinalTxPublished(aliceCommitTx2.tx.txid) + val anchorTx2 = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val claimMain2 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain2.tx, Seq(aliceCommitTx2.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.tx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx2.txInfo.input.outPoint, claimMain2.input)) // A previous funding transaction confirms, so Alice publishes the corresponding commit tx. alice ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx1) assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) - alice2blockchain.expectMsgType[WatchOutputSpent] assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == aliceCommitTx1.tx.txid) - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableLocalCommitAnchor]) - 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) + alice2blockchain.expectFinalTxPublished(aliceCommitTx1.tx.txid) + val anchorTx1 = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val claimMain1 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain1.tx, Seq(aliceCommitTx1.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx1.tx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx1.txInfo.input.outPoint, claimMain1.input)) testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) // Bob publishes his commit tx, Alice reacts by spending her remote main output. alice ! WatchFundingSpentTriggered(bobCommitTx1.tx) - alice2blockchain.expectMsgType[WatchOutputSpent] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - 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) + val anchorRemote = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx1.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.tx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorRemote.txInfo.input.outPoint, claimMainRemote.input)) assert(alice.stateName == CLOSING) } @@ -1182,8 +1186,8 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture // We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly. awaitCond(bob.stateName == CLOSING) bobListener.expectMsgType[ChannelAborted] - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) + bob2blockchain.expectFinalTxPublished(commitTx.txid) + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) bob ! WatchTxConfirmedTriggered(BlockHeight(42), 1, commitTx) bobListener.expectMsgType[TransactionConfirmed] awaitCond(bob.stateName == CLOSED) @@ -1205,11 +1209,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == commitTx.txid) - val claimMain = alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx] - assert(claimMain.input.txid == commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) + alice2blockchain.expectFinalTxPublished(commitTx.txid) + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + Transaction.correctlySpends(claimMain.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) } 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]) = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index 95458b9730..d5bdd3e0e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -26,7 +26,6 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder -import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.relay.Relayer.RelayForward @@ -452,12 +451,11 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // the HTLC times out, alice needs to close the channel alice ! CurrentBlockHeight(add.cltvExpiry.blockHeight) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - val mainDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - assert(mainDelayedTx.desc == "local-main-delayed") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == htlcTimeoutTx.tx.txid) + alice2blockchain.expectFinalTxPublished(commitTx.txid) + val mainDelayedTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(alice2blockchain.expectFinalTxPublished("htlc-timeout").input == htlcTimeoutTx.input.outPoint) alice2blockchain.expectWatchTxConfirmed(commitTx.txid) - alice2blockchain.expectWatchTxConfirmed(mainDelayedTx.tx.txid) + alice2blockchain.expectWatchOutputSpent(mainDelayedTx.input) alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.input.outPoint) alice2blockchain.expectNoMessage(100 millis) @@ -487,13 +485,12 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL // the HTLC timeout from alice is near, bob needs to close the channel to avoid an on-chain race condition bob ! CurrentBlockHeight(add.cltvExpiry.blockHeight - Bob.nodeParams.channelConf.fulfillSafetyBeforeTimeout.toInt) // bob publishes a set of force-close transactions, including the HTLC-success using the received preimage - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) - val mainDelayedTx = bob2blockchain.expectMsgType[PublishFinalTx] - assert(mainDelayedTx.desc == "local-main-delayed") + bob2blockchain.expectFinalTxPublished(commitTx.txid) + val mainDelayedTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") bob2blockchain.expectWatchTxConfirmed(commitTx.txid) - bob2blockchain.expectWatchTxConfirmed(mainDelayedTx.tx.txid) + bob2blockchain.expectWatchOutputSpent(mainDelayedTx.input) bob2blockchain.expectWatchOutputSpent(htlcSuccessTx.input.outPoint) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == htlcSuccessTx.tx.txid) + assert(bob2blockchain.expectFinalTxPublished("htlc-success").input == htlcSuccessTx.input.outPoint) bob2blockchain.expectNoMessage(100 millis) channelUpdateListener.expectMsgType[LocalChannelDown] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index 0967000036..d6b0204481 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -22,7 +22,7 @@ import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.NumericSatoshi.abs -import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, SatoshiLong, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, SatoshiLong, Transaction} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.SingleKeyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -32,8 +32,8 @@ import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction -import fr.acinq.eclair.channel.publish.ReplaceableRemoteCommitAnchor -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.publish.{ReplaceableClaimHtlcTimeout, ReplaceableHtlcTimeout, ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.db.RevokedHtlcInfoCleaner.ForgetHtlcInfos @@ -2661,16 +2661,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik } } - /** Check type of published transactions */ - def assertPublished(probe: TestProbe, desc: String): Transaction = { - val p = probe.expectMsgType[PublishTx] - assert(desc == p.desc) - p match { - case p: PublishFinalTx => p.tx - case p: PublishReplaceableTx => p.tx.txInfo.tx - } - } - test("force-close with multiple splices (simple)") { f => import f._ @@ -2691,17 +2681,21 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // From Alice's point of view, we now have two unconfirmed splices. alice ! CMD_FORCECLOSE(ActorRef.noSender) alice2bob.expectMsgType[Error] - val commitTx2 = assertPublished(alice2blockchain, "commit-tx") + val commitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx Transaction.correctlySpends(commitTx2, Seq(fundingTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") + val claimMainAlice = alice2blockchain.expectFinalTxPublished("local-main-delayed") // Alice publishes her htlc timeout transactions. - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + val aliceHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectFinalTxPublished("htlc-timeout")) + aliceHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val watchConfirmedCommit2 = alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) - val watchConfirmedClaimMainDelayed2 = alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - val watchHtlcsOut = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + // Bob detects Alice's commit tx. + bob ! WatchFundingSpentTriggered(commitTx2) + val bobHtlcTimeout = htlcs.bobToAlice.map(_ => bob2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout].txInfo) + bobHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.tx, Seq(commitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + bob2blockchain.expectWatchTxConfirmed(commitTx2.txid) + bob2blockchain.expectWatchOutputsSpent(aliceHtlcTimeout.map(_.input) ++ bobHtlcTimeout.map(_.input.outPoint)) + alice2blockchain.expectWatchTxConfirmed(commitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMainAlice.input) ++ aliceHtlcTimeout.map(_.input) ++ bobHtlcTimeout.map(_.input.outPoint)) // The first splice transaction confirms. alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -2711,34 +2705,45 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) alice2blockchain.expectMsgType[WatchFundingSpent] - // The commit confirms, along with Alice's 2nd-stage transactions. - watchConfirmedCommit2.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) - watchConfirmedClaimMainDelayed2.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainDelayed2) - watchHtlcsOut.zip(htlcsTxsOut).foreach { case (watch, tx) => watch.replyTo ! WatchOutputSpentTriggered(watch.amount, tx) } - htlcsTxsOut.foreach { tx => - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } - - // Alice publishes 3rd-stage transactions. - htlcs.aliceToBob.foreach { _ => - val tx = assertPublished(alice2blockchain, "htlc-delayed") - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } - - // Bob's htlc-timeout txs confirm. - bob ! WatchFundingSpentTriggered(commitTx2) - val bobHtlcsTxsOut = htlcs.bobToAlice.map(_ => assertPublished(bob2blockchain, "claim-htlc-timeout")) - val remoteOutpoints = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.map(rcp => rcp.htlcTxs.filter(_._2.isEmpty).keys).toSeq.flatten - assert(remoteOutpoints.size == htlcs.bobToAlice.size) - assert(remoteOutpoints.toSet == bobHtlcsTxsOut.flatMap(_.txIn.map(_.outPoint)).toSet) - bobHtlcsTxsOut.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } + // Alice detects that the commit confirms, along with 2nd-stage and 3rd-stage transactions. + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMainAlice.tx) + aliceHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amount, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) + alice ! WatchOutputSpentTriggered(htlcDelayed.amount, htlcDelayed.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayed.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcDelayed.tx) + }) + bobHtlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) alice2blockchain.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) + + // Bob also detects that the commit confirms, along with 2nd-stage transactions. + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, commitTx2) + bobHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + aliceHtlcTimeout.foreach(htlcTx => { + bob ! WatchOutputSpentTriggered(htlcTx.amount, htlcTx.tx) + bob2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + bob ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + bob2blockchain.expectNoMessage(100 millis) + awaitCond(bob.stateName == CLOSED) checkPostSpliceState(f, spliceOutFee(f, capacity = 1_900_000.sat)) - awaitCond(alice.stateName == CLOSED) assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[LocalClose])) + assert(Helpers.Closing.isClosed(bob.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) } test("force-close with multiple splices (previous active remote)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -2763,14 +2768,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // From Alice's point of view, we now have two unconfirmed splices. alice ! CMD_FORCECLOSE(ActorRef.noSender) alice2bob.expectMsgType[Error] - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - assertPublished(alice2blockchain, "local-main-delayed") - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => assert(tx.txIn.forall(_.outPoint.txid == aliceCommitTx2.txid))) - alice2blockchain.expectMsgType[WatchTxConfirmed] - alice2blockchain.expectMsgType[WatchTxConfirmed] - alice2blockchain.expectMsgType[WatchOutputSpent] + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcTimeout]) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.txInfo.input.outPoint)) htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) @@ -2783,38 +2786,40 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val bobCommitTx1 = bobCommitment1.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) Transaction.correctlySpends(bobCommitTx1, Seq(fundingTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice ! WatchFundingSpentTriggered(bobCommitTx1) - val watchAlternativeConfirmed = alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed] + assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) alice2blockchain.expectNoMessage(100 millis) // Bob's commit tx confirms. - watchAlternativeConfirmed.replyTo ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) // We're back to the normal handling of remote commit. - assertPublished(alice2blockchain, "local-anchor") - val claimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val htlcsTxsOut1 = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "claim-htlc-timeout")) - htlcsTxsOut1.foreach(tx => Transaction.correctlySpends(tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val watchConfirmedRemoteCommit = alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val htlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout].txInfo) + htlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // NB: this one fires immediately, tx is already confirmed. - watchConfirmedRemoteCommit.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, remoteAnchor.txInfo.input.outPoint)) + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // Alice's 2nd-stage transactions confirm. - val watchConfirmedClaimMain = alice2blockchain.expectWatchTxConfirmed(claimMain.txid) - watchConfirmedClaimMain.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMain) - val watchHtlcsOut1 = htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) - alice2blockchain.expectMsgType[WatchOutputSpent] // anchor output - watchHtlcsOut1.zip(htlcsTxsOut1).foreach { case (watch, tx) => watch.replyTo ! WatchOutputSpentTriggered(watch.amount, tx) } - htlcsTxsOut1.foreach { tx => - alice2blockchain.expectWatchTxConfirmed(tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) - } + alice ! WatchOutputSpentTriggered(remoteMain.amount, remoteMain.tx) + alice2blockchain.expectWatchTxConfirmed(remoteMain.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + htlcTimeout.foreach(htlcTx => { + alice ! WatchOutputSpentTriggered(htlcTx.amountIn, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx) + }) + assert(alice.stateName == CLOSING) // Bob's 2nd-stage transactions confirm. bobCommitment1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSig => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSig.htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) checkPostSpliceState(f, spliceOutFee = 0.sat) - awaitCond(alice.stateName == CLOSED) assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) } @@ -2860,27 +2865,24 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // Alice watches bob's revoked commit tx, and force-closes with her latest commitment. assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => assertPublished(alice2blockchain, "htlc-timeout")) + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcTimeout]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.txInfo.input.outPoint)) (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) // Bob's revoked commit tx wins. alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) // Alice reacts by punishing bob. - val aliceClaimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val aliceMainPenalty = assertPublished(alice2blockchain, "main-penalty") - val aliceHtlcsPenalty = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) ++ htlcs.bobToAlice.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) - aliceHtlcsPenalty.foreach(tx => Transaction.correctlySpends(tx, Seq(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + htlcPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, Seq(bobRevokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchTxConfirmed(bobRevokedCommitTx.txid) - alice2blockchain.expectWatchTxConfirmed(aliceClaimMain.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid) - aliceHtlcsPenalty.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) // Alice sends a failure upstream for every outgoing HTLC, including the ones that don't appear in the revoked commitment. @@ -2891,10 +2893,9 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice's penalty txs confirm. alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceClaimMain) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceMainPenalty) - aliceHtlcsPenalty.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } - + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainPenalty.tx) + htlcPenalty.foreach { penalty => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } awaitCond(alice.stateName == CLOSED) assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) } @@ -2962,50 +2963,40 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingSpentTriggered(bobCommitTx1) // alice watches bob's commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - val htlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-timeout")) - htlcsTxsOut.foreach(tx => assert(tx.txIn.forall(_.outPoint.txid == aliceCommitTx2.txid))) - + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcTimeout]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // local-anchor - htlcs.aliceToBob.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) - htlcs.bobToAlice.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.txInfo.input.outPoint)) + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(100 millis) // bob's remote tx wins alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) // we're back to the normal handling of remote commit - val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(anchorTx.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - assert(anchorTx.tx.commitTx == bobCommitTx1) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx - val claimHtlcsTxsOut = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "claim-htlc-timeout")) - claimHtlcsTxsOut.foreach(tx => Transaction.correctlySpends(tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + val remoteAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val claimHtlcTimeout = htlcs.aliceToBob.map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout].txInfo) + claimHtlcTimeout.foreach(htlcTx => Transaction.correctlySpends(htlcTx.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) awaitCond(wallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) - - val watchConfirmedRemoteCommit = alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) // this one fires immediately, tx is already confirmed - watchConfirmedRemoteCommit.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) - val watchConfirmedClaimMain = alice2blockchain.expectWatchTxConfirmed(claimMain.txid) - watchConfirmedClaimMain.replyTo ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, claimMain) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobCommitTx1) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, remoteAnchor.txInfo.input.outPoint)) // watch alice and bob's htlcs and publish alice's htlcs-timeout txs - htlcs.aliceToBob.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobCommitTx1.txid)) - htlcs.bobToAlice.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobCommitTx1.txid)) - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(OutPoint(w.txId, w.outputIndex.toLong) == anchorTx.input) } - claimHtlcsTxsOut.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } - - // publish bob's htlc-timeout txs + htlcs.aliceToBob.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + htlcs.bobToAlice.map(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + claimHtlcTimeout.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) + assert(alice.stateName == CLOSING) + // Bob's htlc-timeout txs confirm. bobCommit1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSigs => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSigs.htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) + awaitCond(alice.stateName == CLOSED) - // alice's final commitment includes the initial htlcs, but not bob's payment checkPostSpliceState(f, spliceOutFee = 0.sat) - - // done - awaitCond(alice.stateName == CLOSED) assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RemoteClose])) } @@ -3074,29 +3065,26 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // alice watches bob's revoked commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) - val aliceCommitTx2 = assertPublished(alice2blockchain, "commit-tx") - assertPublished(alice2blockchain, "local-anchor") - val claimMainDelayed2 = assertPublished(alice2blockchain, "local-main-delayed") - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => assertPublished(alice2blockchain, "htlc-timeout")) + val aliceCommitTx2 = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] + val localMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcTimeout]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainDelayed2.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // local-anchor - (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).foreach(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) - (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).foreach(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == aliceCommitTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(localMain.input, localAnchor.txInfo.input.outPoint)) + (htlcs.aliceToBob.map(_._2) ++ Seq(htlcOut1)).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + (htlcs.bobToAlice.map(_._2) ++ Seq(htlcIn)).foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) alice2blockchain.expectNoMessage(100 millis) // bob's revoked tx wins alice ! WatchAlternativeCommitTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) // alice reacts by punishing bob - val aliceClaimMain = assertPublished(alice2blockchain, "remote-main-delayed") - val aliceMainPenalty = assertPublished(alice2blockchain, "main-penalty") - val aliceHtlcsPenalty = htlcs.aliceToBob.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) ++ htlcs.bobToAlice.map(_ => assertPublished(alice2blockchain, "htlc-penalty")) + val remoteMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (htlcs.aliceToBob ++ htlcs.bobToAlice).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) alice2blockchain.expectWatchTxConfirmed(bobRevokedCommitTx.txid) - alice2blockchain.expectWatchTxConfirmed(aliceClaimMain.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid) // main-penalty - aliceHtlcsPenalty.map(_ => assert(alice2blockchain.expectMsgType[WatchOutputSpent].txId == bobRevokedCommitTx.txid)) - awaitCond(wallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) + alice2blockchain.expectWatchOutputsSpent(Seq(remoteMain.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) + awaitCond(wallet.asInstanceOf[SingleKeyOnChainWallet].abandoned.contains(fundingTx2.txid)) // Alice sends a failure upstream for every outgoing HTLC, including the ones that don't appear in the revoked commitment. val outgoingHtlcs = (htlcs.aliceToBob.map(_._2) ++ Set(htlcOut1, htlcOut2)).map(htlc => (htlc, alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id))) @@ -3106,12 +3094,11 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // all penalty txs confirm alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, bobRevokedCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceClaimMain) - alice ! WatchOutputSpentTriggered(aliceMainPenalty.txOut(0).amount, aliceMainPenalty) - alice2blockchain.expectWatchTxConfirmed(aliceMainPenalty.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, aliceMainPenalty) - aliceHtlcsPenalty.foreach { tx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, tx) } - + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, remoteMain.tx) + alice ! WatchOutputSpentTriggered(mainPenalty.amount, mainPenalty.tx) + alice2blockchain.expectWatchTxConfirmed(mainPenalty.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, mainPenalty.tx) + htlcPenalty.foreach { penalty => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, penalty.tx) } awaitCond(alice.stateName == CLOSED) assert(Helpers.Closing.isClosed(alice.stateData.asInstanceOf[DATA_CLOSING], None).exists(_.isInstanceOf[RevokedClose])) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 7f38840160..b7d612633b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -21,7 +21,7 @@ import akka.testkit.TestProbe import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ @@ -30,8 +30,8 @@ import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel._ -import fr.acinq.eclair.channel.publish.{ReplaceableClaimHtlcSuccess, ReplaceableClaimHtlcTimeout, ReplaceableRemoteCommitAnchor} import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.channel.publish.{ReplaceableClaimHtlcSuccess, ReplaceableClaimHtlcTimeout, ReplaceableHtlcSuccess, ReplaceableHtlcTimeout, ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.Sphinx @@ -39,6 +39,7 @@ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.OutgoingPaymentPacket import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.router.Announcements +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ @@ -3083,11 +3084,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (their commit w/ htlc)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(50), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(60), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, CltvExpiryDelta(55), bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, CltvExpiryDelta(65), bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(50), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(60), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, CltvExpiryDelta(55), bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, CltvExpiryDelta(65), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) @@ -3111,16 +3112,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get assert(rcp.claimHtlcTxs.size == 4) - assert(getClaimHtlcSuccessTxs(rcp).length == 1) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) // in response to that, alice publishes her claim txs - val claimAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimAnchor.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx // in addition to her main output, alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val claimHtlcTxs = (1 to 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.claimHtlcTxs.keys.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { assert(claimHtlcTx.tx.txInfo.tx.txIn.size == 1) assert(claimHtlcTx.tx.txInfo.tx.txOut.size == 1) Transaction.correctlySpends(claimHtlcTx.tx.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -3131,21 +3133,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(amountClaimed == 839_959.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.map(_.tx.commitTx.txid).toSet == Set(bobCommitTx.txid)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcSuccess, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap == Map(htlcb1.id -> ConfirmationTarget.Absolute(htlcb1.cltvExpiry.blockHeight))) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcTimeout, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) - val watchedOutputs = Seq( - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 4 - alice2blockchain.expectMsgType[WatchOutputSpent], // anchor - ) - assert(watchedOutputs.map(w => OutPoint(w.txId, w.outputIndex.toLong)).toSet == rcp.claimHtlcTxs.keySet + claimAnchor.input) - alice2blockchain.expectNoMessage(100 millis) + claimHtlcTxs.foreach(p => assert(p.tx.commitTx.txid == bobCommitTx.txid)) + val htlcSuccessConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcSuccess, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap + assert(htlcSuccessConfirmationTargets == Map(htlcb1.id -> ConfirmationTarget.Absolute(htlcb1.cltvExpiry.blockHeight))) + val htlcTimeoutConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcTimeout, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap + assert(htlcTimeoutConfirmationTargets == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) // assert the feerate of the claim main is what we expect val expectedFeeRate = alice.underlyingActor.nodeParams.onChainFeeConf.getClosingFeerate(alice.underlyingActor.nodeParams.currentBitcoinCoreFeerates) @@ -3173,11 +3165,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (their *next* commit w/ htlc)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(30), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) + val (ra2, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(30), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) @@ -3207,16 +3199,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) // in response to that, alice publishes her claim txs - val claimAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimAnchor.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx // in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.claimHtlcTxs.keys.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { assert(claimHtlcTx.tx.txInfo.tx.txIn.size == 1) assert(claimHtlcTx.tx.txInfo.tx.txOut.size == 1) Transaction.correctlySpends(claimHtlcTx.tx.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -3227,19 +3220,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(amountClaimed == 840_534.sat) // alice sets the confirmation targets to the HTLC expiry - assert(claimHtlcTxs.map(_.tx.commitTx.txid).toSet == Set(bobCommitTx.txid)) - assert(claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcTimeout, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) - val watchedOutputs = Seq( - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent], // anchor - ) - assert(watchedOutputs.map(w => OutPoint(w.txId, w.outputIndex.toLong)).toSet == rcp.claimHtlcTxs.keySet + claimAnchor.input) - alice2blockchain.expectNoMessage(100 millis) + claimHtlcTxs.foreach(p => assert(p.tx.commitTx.txid == bobCommitTx.txid)) + val htlcConfirmationTargets = claimHtlcTxs.collect { case PublishReplaceableTx(tx: ReplaceableClaimHtlcTimeout, confirmationTarget) => (tx.txInfo.htlcId, confirmationTarget) }.toMap + assert(htlcConfirmationTargets == Map(htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight))) } test("recv WatchFundingSpentTriggered (their *next* commit w/ pending unsigned htlcs)") { f => @@ -3268,17 +3251,16 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (revoked commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // initially we have : - // alice = 800 000 + // alice = 800 000 // bob = 200 000 def send(): Transaction = { - // alice sends 8 000 sat - addHtlc(10000000 msat, alice, bob, alice2bob, bob2alice) + // alice sends 10 000 sat + addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx } - val txs = for (_ <- 0 until 10) yield send() + val txs = (0 until 10).map(_ => send()) // bob now has 10 spendable tx, 9 of them being revoked // let's say that bob published this tx @@ -3294,32 +3276,25 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(revokedTx.txOut.size == 8) alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head + assert(rvk.htlcPenaltyTxs.size == 4) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTxs = for (_ <- 0 until 4) yield alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTxs = (0 until 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx) // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) - htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet.size == 4) + (mainTx +: mainPenaltyTx +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent((mainTx +: mainPenaltyTx +: htlcPenaltyTxs).flatMap(_.txIn.map(_.outPoint))) alice2blockchain.expectNoMessage(100 millis) - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - // two main outputs are 760 000 and 200 000 - assert(mainTx.txOut.head.amount == 750390.sat) - assert(mainPenaltyTx.txOut.head.amount == 195160.sat) - assert(htlcPenaltyTxs(0).txOut.head.amount == 4200.sat) - assert(htlcPenaltyTxs(1).txOut.head.amount == 4200.sat) - assert(htlcPenaltyTxs(2).txOut.head.amount == 4200.sat) - assert(htlcPenaltyTxs(3).txOut.head.amount == 4200.sat) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + assert(mainTx.txOut.head.amount == 750_390.sat) + assert(mainPenaltyTx.txOut.head.amount == 195_160.sat) + htlcPenaltyTxs.foreach(tx => assert(tx.txOut.head.amount == 4_200.sat)) } test("recv WatchFundingSpentTriggered (revoked commit with identical htlcs)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -3327,9 +3302,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() // initially we have : - // alice = 800 000 + // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -3360,34 +3334,31 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(revokedTx.txOut.size == 6) alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTxs = for (_ <- 0 until 2) yield alice2blockchain.expectMsgType[PublishFinalTx].tx + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty").tx) // let's make sure that htlc-penalty txs each spend a different output - assert(htlcPenaltyTxs.map(_.txIn.head.outPoint.index).toSet.size == htlcPenaltyTxs.size) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - htlcPenaltyTxs.foreach(_ => alice2blockchain.expectMsgType[WatchOutputSpent]) + assert(htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet.size == htlcPenaltyTxs.size) + (mainTx +: mainPenaltyTx +: htlcPenaltyTxs).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent((mainTx +: mainPenaltyTx +: htlcPenaltyTxs).flatMap(_.txIn.map(_.outPoint))) alice2blockchain.expectNoMessage(100 millis) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - htlcPenaltyTxs.foreach(htlcPenaltyTx => Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) } test("recv WatchFundingSpentTriggered (revoked commit w/ pending unsigned htlcs)") { f => import f._ val sender = TestProbe() - addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) val bobRevokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) - val (_, htlc3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice, sender.ref) + val (_, htlc3) = addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] val aliceData = alice.stateData.asInstanceOf[DATA_NORMAL] assert(aliceData.commitments.changes.localChanges.proposed.size == 1) @@ -3402,21 +3373,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (unrecognized commit)") { f => import f._ - alice ! WatchFundingSpentTriggered(Transaction(0, Nil, Nil, 0)) + alice ! WatchFundingSpentTriggered(Transaction(2, Nil, TxOut(100_000 sat, Script.pay2wpkh(randomKey().publicKey)) :: Nil, 0)) alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == NORMAL) } test("recv Error") { f => import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) + val (ra, htlca) = addHtlc(100_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb, htlcb) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob) - fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) + fulfillHtlc(htlca.id, ra, bob, alice, bob2alice, alice2bob) + fulfillHtlc(htlcb.id, rb, alice, bob, alice2bob, bob2alice) // at this point here is the situation from alice pov and what she should do when she publishes his commit tx: // balances : @@ -3432,15 +3403,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get assert(localCommitPublished.commitTx.txid == aliceCommitTx.txid) assert(localCommitPublished.htlcTxs.size == 4) - assert(getHtlcSuccessTxs(localCommitPublished).length == 1) - assert(getHtlcTimeoutTxs(localCommitPublished).length == 2) assert(localCommitPublished.claimHtlcDelayedTxs.isEmpty) // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage @@ -3448,29 +3417,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // - 1 tx to claim the main delayed output // - 3 txs for each htlc // NB: 3rd-stage txs will only be published once the htlc txs confirm - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx2 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlcTx3 = alice2blockchain.expectMsgType[PublishFinalTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + val htlcTx1 = alice2blockchain.expectFinalTxPublished("htlc-success") + val htlcTx2 = alice2blockchain.expectFinalTxPublished("htlc-timeout") + val htlcTx3 = alice2blockchain.expectFinalTxPublished("htlc-timeout") // the main delayed output and htlc txs spend the commitment transaction Seq(claimMain, htlcTx1, htlcTx2, htlcTx3).foreach(tx => Transaction.correctlySpends(tx.tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc 4 + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localCommitPublished.htlcTxs.keys.toSeq) alice2blockchain.expectNoMessage(100 millis) // 3rd-stage txs are published when htlc txs confirm - Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTimeoutTx => - alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) - alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) + Seq(htlcTx1, htlcTx2, htlcTx3).foreach { htlcTx => + alice ! WatchOutputSpentTriggered(htlcTx.amount, htlcTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcTx.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTx.tx) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayedTx.tx, htlcTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) } awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 3) alice2blockchain.expectNoMessage(100 millis) @@ -3487,48 +3451,41 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def testErrorAnchorOutputsWithHtlcs(f: FixtureParam): Unit = { import f._ - val (ra1, htlca1) = addHtlc(250000000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) - val (ra2, htlca2) = addHtlc(100000000 msat, CltvExpiryDelta(25), alice, bob, alice2bob, bob2alice) - val (ra3, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice) - val (rb1, htlcb1) = addHtlc(50000000 msat, CltvExpiryDelta(30), bob, alice, bob2alice, alice2bob) - val (rb2, htlcb2) = addHtlc(55000000 msat, CltvExpiryDelta(35), bob, alice, bob2alice, alice2bob) + val (_, htlca1) = addHtlc(250_000_000 msat, CltvExpiryDelta(20), alice, bob, alice2bob, bob2alice) + val (_, htlca2) = addHtlc(100_000_000 msat, CltvExpiryDelta(25), alice, bob, alice2bob, bob2alice) + addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice) + val (rb1, htlcb1) = addHtlc(50_000_000 msat, CltvExpiryDelta(30), bob, alice, bob2alice, alice2bob) + addHtlc(55_000_000 msat, CltvExpiryDelta(35), bob, alice, bob2alice, alice2bob) crossSign(alice, bob, alice2bob, bob2alice) fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchors and 4 pending htlcs awaitCond(alice.stateName == CLOSING) + val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] + assert(localAnchor.tx.isInstanceOf[ReplaceableLocalCommitAnchor]) assert(localAnchor.confirmationTarget == ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight)) // the target is set to match the first htlc that expires - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") // alice can only claim 3 out of 4 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val htlcConfirmationTargets = Seq( - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 1 - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 2 - alice2blockchain.expectMsgType[PublishReplaceableTx], // htlc 3 - ).map(p => p.tx.txInfo.asInstanceOf[HtlcTx].htlcId -> p.confirmationTarget).toMap + val htlcTxs = (0 until 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localAnchor.input +: localCommitPublished.htlcTxs.keys.toSeq) + alice2blockchain.expectNoMessage(100 millis) + + // alice sets the confirmation target of each htlc transaction to the htlc expiry + assert(htlcTxs.map(_.tx).collect { case tx: ReplaceableHtlcSuccess => tx }.size == 1) + assert(htlcTxs.map(_.tx).collect { case tx: ReplaceableHtlcTimeout => tx }.size == 2) + val htlcConfirmationTargets = htlcTxs.map(p => p.tx.txInfo.asInstanceOf[HtlcTx].htlcId -> p.confirmationTarget).toMap assert(htlcConfirmationTargets == Map( htlcb1.id -> ConfirmationTarget.Absolute(htlcb1.cltvExpiry.blockHeight), htlca1.id -> ConfirmationTarget.Absolute(htlca1.cltvExpiry.blockHeight), htlca2.id -> ConfirmationTarget.Absolute(htlca2.cltvExpiry.blockHeight) )) - - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - val watchedOutputs = Seq( - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 1 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 2 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 3 - alice2blockchain.expectMsgType[WatchOutputSpent], // htlc 4 - alice2blockchain.expectMsgType[WatchOutputSpent], // local anchor - ).map(w => OutPoint(w.txId, w.outputIndex)).toSet - val localCommitPublished = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(watchedOutputs == localCommitPublished.htlcTxs.keySet + localAnchor.tx.txInfo.input.outPoint) - alice2blockchain.expectNoMessage(100 millis) } test("recv Error (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -3546,24 +3503,21 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors awaitCond(alice.stateName == CLOSING) - val currentBlockHeight = alice.underlyingActor.nodeParams.currentBlockHeight if (commitFeeBumpDisabled) { - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.input) alice2blockchain.expectNoMessage(100 millis) } else { - val localAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx] // When there are no pending HTLCs, there is no absolute deadline to get the commit tx confirmed, we use priority - assert(localAnchor.confirmationTarget == ConfirmationTarget.Priority(ConfirmationPriority.Medium)) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === claimMain.tx.txid) - assert(alice2blockchain.expectMsgType[WatchOutputSpent].outputIndex === localAnchor.input.index) + val localAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor](ConfirmationTarget.Priority(ConfirmationPriority.Medium)) + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(localAnchor.txInfo.input.outPoint, claimMain.input)) alice2blockchain.expectNoMessage(100 millis) } } @@ -3585,7 +3539,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // an error occurs and alice publishes her commit tx val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx bob ! Error(ByteVector32.Zeroes, "oops") - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == bobCommitTx.txid) + bob2blockchain.expectFinalTxPublished(bobCommitTx.txid) assert(bobCommitTx.txOut.size == 1) // only one main output alice2blockchain.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index c13bf27dca..e52a89930e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -628,13 +628,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) - val mainDelayedTx = bob2blockchain.expectMsgType[PublishFinalTx] - assert(mainDelayedTx.desc == "local-main-delayed") + bob2blockchain.expectFinalTxPublished(initialCommitTx.txid) + val mainDelayedTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") bob2blockchain.expectWatchTxConfirmed(initialCommitTx.txid) - bob2blockchain.expectWatchTxConfirmed(mainDelayedTx.tx.txid) + bob2blockchain.expectWatchOutputSpent(mainDelayedTx.input) bob2blockchain.expectWatchOutputSpent(htlcSuccessTx.input.outPoint) - val publishHtlcTx = bob2blockchain.expectMsgType[PublishFinalTx] + val publishHtlcTx = bob2blockchain.expectFinalTxPublished("htlc-success") assert(publishHtlcTx.input == htlcSuccessTx.input.outPoint) bob2blockchain.expectNoMessage(100 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index 5c603b83c0..e94638e389 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -20,21 +20,21 @@ import akka.testkit.TestProbe import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, SatoshiLong, Script, Transaction} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Script, Transaction} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.blockchain.{CurrentBlockHeight, CurrentFeerates} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.channel.publish.ReplaceableRemoteCommitAnchor -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishTx} +import fr.acinq.eclair.channel.publish.{ReplaceableClaimHtlcTimeout, ReplaceableRemoteCommitAnchor} import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.payment.send.SpontaneousRecipient +import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, FailureReason, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32, randomKey} -import org.scalatest.Inside.inside import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -723,36 +723,29 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx assert(bobCommitTx.txOut.size == 6) // two main outputs and 2 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get + assert(rcp.claimHtlcTxs.size == 2) // in response to that, alice publishes her claim txs - val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(anchorTx.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx].tx + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") // in addition to her main output, alice can only claim 2 out of 3 htlcs, she can't do anything regarding the htlc sent by bob for which she does not have the preimage - val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx].tx.txInfo.tx) - val htlcAmountClaimed = (for (claimHtlcTx <- claimHtlcTxs) yield { - assert(claimHtlcTx.txIn.size == 1) - assert(claimHtlcTx.txOut.size == 1) - Transaction.correctlySpends(claimHtlcTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - claimHtlcTx.txOut.head.amount + val claimHtlcTxs = (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout]) + val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { + assert(claimHtlcTx.txInfo.tx.txIn.size == 1) + assert(claimHtlcTx.txInfo.tx.txOut.size == 1) + Transaction.correctlySpends(claimHtlcTx.txInfo.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + claimHtlcTx.txInfo.tx.txOut.head.amount }).sum // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat = 1000000 - 200000 = 800000 (because fees) - val amountClaimed = htlcAmountClaimed + claimMain.txOut.head.amount + val amountClaimed = htlcAmountClaimed + claimMain.tx.txOut.head.amount assert(amountClaimed == 790_974.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.txid) - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(claimHtlcTxs.flatMap(_.txIn).map(_.outPoint).contains(OutPoint(w.txId, w.outputIndex.toLong))) } - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(claimHtlcTxs.flatMap(_.txIn).map(_.outPoint).contains(OutPoint(w.txId, w.outputIndex.toLong))) } - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(anchorTx.input == OutPoint(w.txId, w.outputIndex.toLong)) } + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx.txInfo.input.outPoint, claimMain.input) ++ claimHtlcTxs.map(_.txInfo.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 2) - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 2) } test("recv WatchFundingSpentTriggered (their next commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => @@ -775,16 +768,19 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx assert(bobCommitTx.txOut.size == 5) // two anchor outputs, two main outputs and 1 pending htlc alice ! WatchFundingSpentTriggered(bobCommitTx) + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) + val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get + assert(rcp.claimHtlcTxs.size == 1) // in response to that, alice publishes her claim txs - val anchorTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(anchorTx.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimTxs = Seq( - alice2blockchain.expectMsgType[PublishFinalTx].tx, + alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx, // there is only one htlc to claim in the commitment bob published - alice2blockchain.expectMsgType[PublishReplaceableTx].tx.txInfo.tx + alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout].txInfo.tx ) - val amountClaimed = (for (claimTx <- claimTxs) yield { + val amountClaimed = claimTxs.map(claimTx => { assert(claimTx.txIn.size == 1) assert(claimTx.txOut.size == 1) Transaction.correctlySpends(claimTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -793,18 +789,9 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // htlc will timeout and be eventually refunded so we have a little less than fundingSatoshis - pushMsat - htlc1 = 1000000 - 200000 - 300 000 = 500000 (because fees) assert(amountClaimed == 491_542.sat) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == bobCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(claimTxs(1).txIn.head.outPoint == OutPoint(w.txId, w.outputIndex.toLong)) } - inside(alice2blockchain.expectMsgType[WatchOutputSpent]) { w => assert(anchorTx.input == OutPoint(w.txId, w.outputIndex.toLong)) } + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(anchorTx.txInfo.input.outPoint +: claimTxs.flatMap(_.txIn.map(_.outPoint))) alice2blockchain.expectNoMessage(100 millis) - - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.isDefined) - val rcp = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 1) - assert(getClaimHtlcSuccessTxs(rcp).length == 0) - assert(getClaimHtlcTimeoutTxs(rcp).length == 1) } test("recv WatchFundingSpentTriggered (revoked tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => @@ -822,31 +809,23 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // bob published the revoked tx alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlc1PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlc2PenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc1-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc2-penalty - alice2blockchain.expectNoMessage(100 millis) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc1PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlc2PenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlc1PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + val htlc2PenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + Seq(mainTx, mainPenaltyTx, htlc1PenaltyTx, htlc2PenaltyTx).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 assert(mainTx.txOut.head.amount == 291_250.sat) assert(mainPenaltyTx.txOut.head.amount == 195_160.sat) assert(htlc1PenaltyTx.txOut.head.amount == 194_200.sat) assert(htlc2PenaltyTx.txOut.head.amount == 294_200.sat) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx, mainPenaltyTx, htlc1PenaltyTx, htlc2PenaltyTx).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => @@ -868,27 +847,21 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // bob published the revoked tx alice ! WatchFundingSpentTriggered(revokedTx) alice2bob.expectMsgType[Error] + awaitCond(alice.stateName == CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - val mainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val mainPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == revokedTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == mainTx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] // main-penalty - alice2blockchain.expectMsgType[WatchOutputSpent] // htlc-penalty - alice2blockchain.expectNoMessage(100 millis) - - Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx + val htlcPenaltyTx = alice2blockchain.expectFinalTxPublished("htlc-penalty").tx + Seq(mainTx, mainPenaltyTx, htlcPenaltyTx).foreach(tx => Transaction.correctlySpends(tx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000 assert(mainTx.txOut(0).amount == 291_680.sat) assert(mainPenaltyTx.txOut(0).amount == 495_160.sat) assert(htlcPenaltyTx.txOut(0).amount == 194_200.sat) - awaitCond(alice.stateName == CLOSING) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx, mainPenaltyTx, htlcPenaltyTx).flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectNoMessage(100 millis) } test("recv CMD_CLOSE") { f => @@ -943,31 +916,28 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_FORCECLOSE]] - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get assert(lcp.htlcTxs.size == 2) - assert(lcp.claimHtlcDelayedTxs.isEmpty) // 3rd-stage txs will be published once htlc txs confirm - val claimMain = alice2blockchain.expectMsgType[PublishFinalTx] - val htlc1 = alice2blockchain.expectMsgType[PublishFinalTx] - val htlc2 = alice2blockchain.expectMsgType[PublishFinalTx] + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") + val htlc1 = alice2blockchain.expectFinalTxPublished("htlc-timeout") + val htlc2 = alice2blockchain.expectFinalTxPublished("htlc-timeout") Seq(claimMain, htlc1, htlc2).foreach(tx => Transaction.correctlySpends(tx.tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimMain.tx.txid) - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(claimMain, htlc1, htlc2).map(_.input)) alice2blockchain.expectNoMessage(100 millis) // 3rd-stage txs are published when htlc txs confirm Seq(htlc1, htlc2).foreach(htlcTimeoutTx => { alice ! WatchOutputSpentTriggered(htlcTimeoutTx.amount, htlcTimeoutTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == htlcTimeoutTx.tx.txid) + alice2blockchain.expectWatchTxConfirmed(htlcTimeoutTx.tx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 3, htlcTimeoutTx.tx) - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimHtlcDelayedTx, htlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimHtlcDelayedTx.txid) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayedTx.tx, htlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) }) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 2) alice2blockchain.expectNoMessage(100 millis) @@ -984,7 +954,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs awaitCond(alice.stateName == CLOSING) assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.isDefined) @@ -993,13 +963,11 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // - 1 tx to claim the main delayed output // - 2 txs for each htlc // NB: 3rd-stage txs will only be published once the htlc txs confirm - val claimTxs = for (_ <- 0 until 3) yield alice2blockchain.expectMsgType[PublishFinalTx].tx + val claimTxs = (0 until 3).map(_ => alice2blockchain.expectMsgType[PublishFinalTx].tx) // the main delayed output and htlc txs spend the commitment transaction claimTxs.foreach(tx => Transaction.correctlySpends(tx, aliceCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == aliceCommitTx.txid) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId == claimTxs(0).txid) // main-delayed - alice2blockchain.expectMsgType[WatchOutputSpent] - alice2blockchain.expectMsgType[WatchOutputSpent] + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(claimTxs.flatMap(_.txIn.map(_.outPoint))) alice2blockchain.expectNoMessage(100 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index 02831fa7b6..ef3b1d327e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -22,13 +22,12 @@ import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxIn, TxOut} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.DummyOnChainWallet import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{ConfirmationPriority, ConfirmationTarget, FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} -import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx, SetChannelId} +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, SetChannelId} import fr.acinq.eclair.channel.publish._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} @@ -145,8 +144,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed + alice2blockchain.expectFinalTxPublished("commit-tx") + alice2blockchain.expectFinalTxPublished("local-main-delayed") eventListener.expectMsgType[ChannelAborted] // test starts here @@ -160,8 +159,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed + alice2blockchain.expectFinalTxPublished("commit-tx") + alice2blockchain.expectFinalTxPublished("local-main-delayed") eventListener.expectMsgType[ChannelAborted] // test starts here @@ -177,10 +176,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed - alice2blockchain.expectMsgType[WatchTxConfirmed] // commitment - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -197,16 +196,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) alice2bob.expectMsgType[Error] - alice2blockchain.expectMsgType[PublishTx] - alice2blockchain.expectMsgType[PublishTx] // claim-main-delayed - alice2blockchain.expectMsgType[WatchTxConfirmed] // commitment - alice2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = alice2blockchain.expectFinalTxPublished("commit-tx").tx + val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed").tx + alice2blockchain.expectWatchTxConfirmed(commitTx.txid) + alice2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here alice ! GetTxWithMetaResponse(fundingTx.txid, None, TimestampSecond.now()) alice2bob.expectNoMessage(100 millis) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == fundingTx) // we republish the funding tx + alice2blockchain.expectNoMessage(100 millis) assert(alice.stateName == CLOSING) // the above expectNoMsg will make us wait, so this checks that we are still in CLOSING } @@ -217,10 +217,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -237,10 +237,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -257,10 +257,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FORCECLOSE(sender.ref) awaitCond(bob.stateName == CLOSING) bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] // claim-main-delayed - bob2blockchain.expectMsgType[WatchTxConfirmed] // commitment - bob2blockchain.expectMsgType[WatchTxConfirmed] // claim-main-delayed + val commitTx = bob2blockchain.expectFinalTxPublished("commit-tx").tx + val claimMain = bob2blockchain.expectFinalTxPublished("local-main-delayed").tx + bob2blockchain.expectWatchTxConfirmed(commitTx.txid) + bob2blockchain.expectWatchOutputSpent(claimMain.txIn.head.outPoint) eventListener.expectMsgType[ChannelAborted] // test starts here @@ -293,8 +293,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val c = CMD_FULFILL_HTLC(42, randomBytes32(), replyTo_opt = Some(sender.ref)) alice ! c sender.expectMsg(RES_FAILURE(c, UnknownHtlcId(channelId(alice), 42))) - - // NB: nominal case is tested in IntegrationSpec } def testMutualCloseBeforeConverge(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { @@ -340,7 +338,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val mutualCloseTx = alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last - // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mutualCloseTx.tx) awaitCond(alice.stateName == CLOSED) } @@ -365,7 +362,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.isDefined) - // actual test starts here // we are notified afterwards from our watcher about the tx that we just published alice ! WatchFundingSpentTriggered(aliceCommitTx) assert(alice.stateData == initialState) // this was a no-op @@ -395,24 +391,26 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob has the preimage for those HTLCs, but Alice force-closes before receiving it. bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored - val lcp = localClose(alice, alice2blockchain) + val (lcp, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 2) + assert(lcp.htlcTxs.size == 2) + assert(closingTxs.htlcTimeoutTxs.size == 2) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.contains(lcp)) // Bob claims the htlc output from Alice's commit tx using its preimage. bob ! WatchFundingSpentTriggered(lcp.commitTx) - if (initialState.commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(bob2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") + initialState.commitments.params.commitmentFormat match { + case DefaultCommitmentFormat => () + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + bob2blockchain.expectFinalTxPublished("remote-main-delayed") } - val claimHtlcSuccessTx1 = bob2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlcSuccessTx1.tx.isInstanceOf[ReplaceableClaimHtlcSuccess]) - val claimHtlcSuccessTx2 = bob2blockchain.expectMsgType[PublishReplaceableTx] - assert(claimHtlcSuccessTx2.tx.isInstanceOf[ReplaceableClaimHtlcSuccess]) - assert(claimHtlcSuccessTx1.input != claimHtlcSuccessTx2.input) + val claimHtlcSuccessTx1 = bob2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess] + val claimHtlcSuccessTx2 = bob2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess] + assert(Seq(claimHtlcSuccessTx1, claimHtlcSuccessTx2).map(_.txInfo.input.outPoint).toSet == lcp.htlcTxs.keySet) // Alice extracts the preimage and forwards it upstream. - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, claimHtlcSuccessTx1.tx.txInfo.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, claimHtlcSuccessTx1.txInfo.tx) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -421,8 +419,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData == initialState) // this was a no-op // The Claim-HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - alice2blockchain.expectWatchTxConfirmed(claimHtlcSuccessTx1.tx.txInfo.tx.txid) - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, claimHtlcSuccessTx1.tx.txInfo.tx) + alice2blockchain.expectWatchTxConfirmed(claimHtlcSuccessTx1.txInfo.tx.txid) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, claimHtlcSuccessTx1.txInfo.tx) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) } @@ -447,17 +445,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob has the preimage for those HTLCs, but he force-closes before Alice receives it. bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored - val rcp = localClose(bob, bob2blockchain) + val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) // Bob claims the htlc outputs from his own commit tx using its preimage. assert(rcp.htlcTxs.size == 2) - rcp.htlcTxs.values.foreach(tx_opt => assert(tx_opt.nonEmpty)) - val htlcSuccessTxs = rcp.htlcTxs.values.flatten - htlcSuccessTxs.foreach(tx => assert(tx.isInstanceOf[HtlcSuccessTx])) + assert(closingTxs.htlcSuccessTxs.size == 2) // Alice extracts the preimage and forwards it upstream. alice ! WatchFundingSpentTriggered(rcp.commitTx) - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, htlcSuccessTxs.head.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, closingTxs.htlcSuccessTxs.head) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -465,7 +461,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with }) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, htlcSuccessTxs.head.tx) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, closingTxs.htlcSuccessTxs.head) alice2relayer.expectNoMessage(100 millis) } @@ -506,31 +502,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored // Bob claims the htlc outputs from his previous commit tx using its preimage. - val rcp = localClose(bob, bob2blockchain) + val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) assert(rcp.htlcTxs.size == 3) - val htlcSuccessTxs = rcp.htlcTxs.values.flatten - assert(htlcSuccessTxs.size == 2) // Bob doesn't have the preimage for the last HTLC. - htlcSuccessTxs.foreach(tx => assert(tx.isInstanceOf[HtlcSuccessTx])) + assert(closingTxs.htlcSuccessTxs.size == 2) // Bob doesn't have the preimage for the last HTLC. // Alice prepares Claim-HTLC-timeout transactions for each HTLC. alice ! WatchFundingSpentTriggered(rcp.commitTx) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - assert(alice2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") + val (anchorTx_opt, mainTx_opt) = bobStateWithHtlc.commitments.params.commitmentFormat match { + case DefaultCommitmentFormat => (None, None) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + (Some(anchorTx), Some(mainTx)) } - Seq(htlc1, htlc2, htlc3).foreach(_ => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableClaimHtlcTimeout])) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get) - assert(claimHtlcTimeoutTxs.size == 3) + val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout]) + assert(claimHtlcTimeoutTxs.map(_.txInfo.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id)) alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - val claimMainTx = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.get - alice2blockchain.expectWatchTxConfirmed(claimMainTx.tx.txid) - } - alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint)) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - val anchorOutput = alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimAnchorTx_opt.get.input.outPoint - alice2blockchain.expectWatchOutputSpent(anchorOutput) - } + mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.input)) + anchorTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txInfo.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.txInfo.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment confirms. @@ -539,7 +529,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // Alice extracts the preimage from Bob's HTLC-success and forwards it upstream. - alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, htlcSuccessTxs.head.tx) + alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, closingTxs.htlcSuccessTxs.head) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) @@ -548,12 +538,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, htlcSuccessTxs.head.tx) + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, closingTxs.htlcSuccessTxs.head) alice2relayer.expectNoMessage(100 millis) // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. - val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.htlcId == htlc3.id).get - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.tx) + val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.txInfo.htlcId == htlc3.id).get + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.txInfo.tx) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { fail => assert(fail.htlc == htlc3) assert(fail.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id)) @@ -592,36 +582,37 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice.setState(NORMAL, aliceStateWithoutHtlcs) // At that point, the HTLCs are not in Alice's commitment yet. - val rcp = localClose(bob, bob2blockchain) + val (rcp, closingTxs) = localClose(bob, bob2blockchain) assert(rcp.htlcTxs.size == 3) // Bob doesn't have the preimage yet for any of those HTLCs. - rcp.htlcTxs.values.foreach(tx_opt => assert(tx_opt.isEmpty)) + assert(closingTxs.htlcTxs.isEmpty) // Bob receives the preimage for the first two HTLCs. bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) - awaitCond(bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcTxs.values.exists(_.nonEmpty)) - val htlcSuccessTxs = bob.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcTxs.values.flatten.filter(_.isInstanceOf[HtlcSuccessTx]).toSeq - assert(htlcSuccessTxs.map(_.htlcId).toSet == Set(htlc1.id, htlc2.id)) - val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap(_.tx.txIn), htlcSuccessTxs.flatMap(_.tx.txOut), 0) + val htlcSuccessTxs = aliceStateWithoutHtlcs.commitments.params.commitmentFormat match { + case DefaultCommitmentFormat => (0 until 2).map(_ => bob2blockchain.expectFinalTxPublished("htlc-success").tx) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + val htlcSuccess = (0 until 2).map(_ => bob2blockchain.expectReplaceableTxPublished[ReplaceableHtlcSuccess]) + assert(htlcSuccess.map(_.txInfo.htlcId).toSet == Set(htlc1.id, htlc2.id)) + htlcSuccess.map(_.txInfo.tx) + } + bob2blockchain.expectNoMessage(100 millis) + val batchHtlcSuccessTx = Transaction(2, htlcSuccessTxs.flatMap(_.txIn), htlcSuccessTxs.flatMap(_.txOut), 0) // Alice prepares Claim-HTLC-timeout transactions for each HTLC. alice ! WatchFundingSpentTriggered(rcp.commitTx) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - assert(alice2blockchain.expectMsgType[PublishFinalTx].desc == "remote-main-delayed") + val (anchorTx_opt, mainTx_opt) = aliceStateWithoutHtlcs.commitments.params.commitmentFormat match { + case DefaultCommitmentFormat => (None, None) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + (Some(anchorTx), Some(mainTx)) } - Seq(htlc1, htlc2, htlc3).foreach(_ => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.isInstanceOf[ReplaceableClaimHtlcTimeout])) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get) - assert(claimHtlcTimeoutTxs.size == 3) + val claimHtlcTimeoutTxs = Seq(htlc1, htlc2, htlc3).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout]) + assert(claimHtlcTimeoutTxs.map(_.txInfo.htlcId).toSet == Set(htlc1, htlc2, htlc3).map(_.id)) alice2blockchain.expectWatchTxConfirmed(rcp.commitTx.txid) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - val claimMainTx = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimMainOutputTx.get - alice2blockchain.expectWatchTxConfirmed(claimMainTx.tx.txid) - } - alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint)) - if (alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.hasFeature(Features.AnchorOutputsZeroFeeHtlcTx)) { - val anchorOutput = alice.stateData.asInstanceOf[DATA_CLOSING].nextRemoteCommitPublished.get.claimAnchorTx_opt.get.input.outPoint - alice2blockchain.expectWatchOutputSpent(anchorOutput) - } + mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.input)) + anchorTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txInfo.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.txInfo.input.outPoint)) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment confirms. @@ -631,24 +622,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice extracts the preimage from Bob's batched HTLC-success and forwards it upstream. alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, batchHtlcSuccessTx) + alice2blockchain.expectWatchTxConfirmed(batchHtlcSuccessTx.txid) Seq(htlc1, htlc2).foreach(htlc => inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFulfill]]) { fulfill => assert(fulfill.htlc == htlc) assert(fulfill.result.paymentPreimage == preimage) assert(fulfill.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)) }) - alice2relayer.expectNoMessage(100 millis) // The HTLC-success transaction confirms: nothing to do, preimage has already been relayed. alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 6, batchHtlcSuccessTx) alice2relayer.expectNoMessage(100 millis) // Alice's Claim-HTLC-timeout transaction confirms: we relay the failure upstream. - val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.htlcId == htlc3.id).get - alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.tx) + val claimHtlcTimeout = claimHtlcTimeoutTxs.find(_.txInfo.htlcId == htlc3.id).get + alice ! WatchTxConfirmedTriggered(alice.nodeParams.currentBlockHeight, 13, claimHtlcTimeout.txInfo.tx) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { fail => assert(fail.htlc == htlc3) assert(fail.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc3.id)) } + alice2relayer.expectNoMessage(100 millis) } test("recv WatchOutputSpentTriggered (extract preimage for next batch of HTLCs)") { f => @@ -662,11 +654,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_BUMP_FORCE_CLOSE_FEE (local commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - localClose(alice, alice2blockchain) + val (localCommitPublished, closingTxs) = localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.nonEmpty) - val localCommitPublished = initialState.localCommitPublished.get - assert(localCommitPublished.claimAnchorTxs.nonEmpty) + assert(closingTxs.anchorTx_opt.nonEmpty) val replyTo = TestProbe() alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Fast)) @@ -693,14 +684,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val amountBelowDust = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.dustLimit - 100.msat val (_, htlca2) = addHtlc(amountBelowDust, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) + assert(closingTxs.mainTx_opt.isDefined) assert(closingState.htlcTxs.size == 1) - assert(getHtlcSuccessTxs(closingState).isEmpty) - assert(getHtlcTimeoutTxs(closingState).length == 1) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head.tx + assert(closingTxs.htlcTimeoutTxs.size == 1) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head assert(closingState.claimHtlcDelayedTxs.isEmpty) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == closingState.commitTx) @@ -712,9 +703,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(settled.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlca2.id)) } alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingTxs.mainTx_opt.get) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingState.claimMainDelayedOutputTx.get.tx, htlcTimeoutTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.irrevocablySpent.values.toSet == Set(closingState.commitTx, closingTxs.mainTx_opt.get, htlcTimeoutTx)) inside(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]]) { settled => assert(settled.htlc == htlca1) assert(settled.origin == alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlca1.id)) @@ -722,11 +713,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2relayer.expectNoMessage(100 millis) // We claim the htlc-delayed output now that the HTLC tx has been confirmed. - val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - Transaction.correctlySpends(claimHtlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 1) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcDelayedTx.tx) - + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcTimeoutTx, 0)) + Transaction.correctlySpends(htlcDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + alice ! WatchOutputSpentTriggered(htlcDelayedTx.amount, htlcDelayedTx.tx) + alice2blockchain.expectWatchTxConfirmed(htlcDelayedTx.tx.txid) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 1) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcDelayedTx.tx) awaitCond(alice.stateName == CLOSED) } @@ -754,41 +749,46 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, cmd4) = makeCmdAdd(20_000_000 msat, bob.nodeParams.nodeId, alice.nodeParams.currentBlockHeight + 1, ra1) val htlca4 = addHtlc(cmd4, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 4) // actual test starts here assert(closingState.claimMainDelayedOutputTx.isDefined) + assert(closingTxs.mainTx_opt.isDefined) assert(closingState.htlcTxs.size == 4) - assert(getHtlcSuccessTxs(closingState).isEmpty) - val htlcTimeoutTxs = getHtlcTimeoutTxs(closingState).map(_.tx) - assert(htlcTimeoutTxs.length == 4) + assert(closingTxs.htlcTimeoutTxs.size == 4) assert(closingState.claimHtlcDelayedTxs.isEmpty) // if commit tx and htlc-timeout txs end up in the same block, we may receive the htlc-timeout confirmation before the commit tx confirmation - alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, htlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, closingState.commitTx) assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == dust) alice2relayer.expectNoMessage(100 millis) alice ! WatchTxConfirmedTriggered(BlockHeight(200), 0, closingState.claimMainDelayedOutputTx.get.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, htlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, htlcTimeoutTxs(3)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, closingTxs.htlcTimeoutTxs(3)) val forwardedFail4 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3, forwardedFail4) == Set(htlca1, htlca2, htlca3, htlca4)) alice2relayer.expectNoMessage(100 millis) + val htlcDelayedTxs = closingTxs.htlcTimeoutTxs.map(htlcTx => { + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcTx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + htlcDelayedTx + }) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) - val claimHtlcDelayedTxs = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, claimHtlcDelayedTxs(0).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcDelayedTxs(1).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 2, claimHtlcDelayedTxs(2).tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 3, claimHtlcDelayedTxs(3).tx) + htlcDelayedTxs.foreach(tx => { + alice ! WatchOutputSpentTriggered(tx.amount, tx.tx) + alice2blockchain.expectWatchTxConfirmed(tx.tx.txid) + }) + htlcDelayedTxs.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(203), 0, tx.tx)) awaitCond(alice.stateName == CLOSED) } @@ -804,11 +804,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // when the commit tx is confirmed, alice knows that the htlc she sent right before the unilateral close will never reach the chain alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) // so she fails it @@ -832,10 +833,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx // Note that alice has not signed the htlc yet! // We make her unilaterally close the channel. - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // Alice should ignore the htlc (she hasn't relayed it yet): it is Bob's responsibility to claim it. // Once the commit tx and her main output are confirmed, she can consider the channel closed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, aliceCommitTx) @@ -860,10 +862,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx // We make Bob unilaterally close the channel. - val rcp = remoteClose(bobCommitTx, alice, alice2blockchain) + val (rcp, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] assert(rcp.claimHtlcTxs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // Alice should ignore the htlc (she hasn't relayed it yet): it is Bob's responsibility to claim it. // Once the commit tx and her main output are confirmed, she can consider the channel closed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) @@ -893,10 +896,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // note that bob doesn't receive the new sig! // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) + val (closingState, _) = localClose(alice, alice2blockchain, htlcSuccessCount = 1) assert(closingState.commitTx.txid == aliceCommitTx.txid) - assert(getHtlcTimeoutTxs(closingState).isEmpty) - assert(getHtlcSuccessTxs(closingState).length == 1) } test("recv WatchTxConfirmedTriggered (local commit with fail not acked by remote)") { f => @@ -916,12 +917,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // note that alice doesn't receive the last revocation // then we make alice unilaterally close the channel - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) assert(closingState.commitTx.txOut.length == 2) // htlc has been removed // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // when the commit tx is confirmed, alice knows that the htlc will never reach the chain alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) // so she fails it @@ -942,12 +944,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice2relayer.expectMsgType[RelayForward].add == htlc2) // Alice force-closes. - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain) assert(closingState.commitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 htlcs assert(closingState.claimMainDelayedOutputTx.nonEmpty) assert(closingState.htlcTxs.size == 2) - assert(getHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getHtlcTimeoutTxs(closingState).isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet // Alice's commitment and main transaction confirm: she waits for the HTLC outputs to be spent. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.commitTx) @@ -956,12 +957,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.tx.isInstanceOf[ReplaceableHtlcSuccess]) - assert(publishHtlcSuccessTx.tx.asInstanceOf[ReplaceableHtlcSuccess].preimage == r1) - assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) - val htlcSuccessTx = publishHtlcSuccessTx.tx.txInfo.tx - Transaction.correctlySpends(htlcSuccessTx, closingState.commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcSuccess](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + Transaction.correctlySpends(htlcSuccess.txInfo.tx, closingState.commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectNoMessage(100 millis) // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. @@ -975,13 +973,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) // Alice republishes the HTLC-success transaction, which then confirms. - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].input == publishHtlcSuccessTx.input) - alice2blockchain.expectWatchOutputSpent(publishHtlcSuccessTx.input) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccessTx) + assert(alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcSuccess].txInfo.input == htlcSuccess.txInfo.input) + closingTxs.anchorTx_opt.foreach(anchorTx => alice2blockchain.expectWatchOutputSpent(anchorTx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(htlcSuccess.txInfo.input.outPoint) + alice ! WatchOutputSpentTriggered(htlcSuccess.txInfo.amountIn, htlcSuccess.txInfo.tx) + alice2blockchain.expectWatchTxConfirmed(htlcSuccess.txInfo.tx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccess.txInfo.tx) // Alice publishes a 3rd-stage HTLC transaction. - val htlcDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx] - assert(htlcDelayedTx.input == OutPoint(publishHtlcSuccessTx.tx.txInfo.tx, 0)) - alice2blockchain.expectWatchTxConfirmed(htlcDelayedTx.tx.txid) + val htlcDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcDelayedTx.input == OutPoint(htlcSuccess.txInfo.tx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) alice2blockchain.expectNoMessage(100 millis) // Alice restarts again before the 3rd-stage HTLC transaction confirmed. @@ -990,8 +991,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! INPUT_RESTORED(beforeRestart2) alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) + closingTxs.anchorTx_opt.foreach(anchorTx => alice2blockchain.expectWatchOutputSpent(anchorTx.txIn.head.outPoint)) // Alice republishes the 3rd-stage HTLC transaction, which then confirms. - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == htlcDelayedTx.tx.txid) + alice2blockchain.expectFinalTxPublished(htlcDelayedTx.tx.txid) + alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) + alice ! WatchOutputSpentTriggered(htlcDelayedTx.amount, htlcDelayedTx.tx) alice2blockchain.expectWatchTxConfirmed(htlcDelayedTx.tx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcDelayedTx.tx) alice2blockchain.expectNoMessage(100 millis) @@ -1005,8 +1009,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice sends an htlc to bob addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val closingState = localClose(alice, alice2blockchain) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState).head + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // simulate a node restart after a feerate increase val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1019,21 +1023,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == closingState.commitTx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcTimeoutTx.tx) + alice2blockchain.expectFinalTxPublished(closingState.commitTx.txid) + closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")) + assert(alice2blockchain.expectFinalTxPublished("htlc-timeout").input == htlcTimeoutTx.txIn.head.outPoint) alice2blockchain.expectWatchTxConfirmed(closingState.commitTx.txid) - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.tx.txid)) - alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.input.outPoint) + closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.txIn.head.outPoint) // the htlc transaction confirms, so we publish a 3rd-stage transaction alice ! WatchTxConfirmedTriggered(BlockHeight(2701), 1, closingState.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(2702), 0, htlcTimeoutTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(2702), 0, htlcTimeoutTx) + val htlcDelayed = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.nonEmpty) val beforeSecondRestart = alice.stateData.asInstanceOf[DATA_CLOSING] - val claimHtlcTimeoutTx = beforeSecondRestart.localCommitPublished.get.claimHtlcDelayedTxs.head - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) - alice2blockchain.expectWatchTxConfirmed(claimHtlcTimeoutTx.tx.txid) // simulate another node restart alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) @@ -1042,15 +1045,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainDelayedOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutTx.tx) - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.tx.txid)) - alice2blockchain.expectWatchTxConfirmed(claimHtlcTimeoutTx.tx.txid) + closingTxs.mainTx_opt.foreach(mainTx => { + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchOutputSpent(mainTx.txIn.head.outPoint) + }) + assert(alice2blockchain.expectFinalTxPublished("htlc-delayed").input == htlcDelayed.input) + alice2blockchain.expectWatchOutputSpent(htlcDelayed.input) // the main transaction confirms - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(2801), 5, claimMain.tx)) + closingTxs.mainTx_opt.foreach(mainTx => alice ! WatchTxConfirmedTriggered(BlockHeight(2801), 5, mainTx)) assert(alice.stateName == CLOSING) // the htlc delayed transaction confirms - alice ! WatchTxConfirmedTriggered(BlockHeight(2802), 5, claimHtlcTimeoutTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(2802), 5, htlcDelayed.tx) awaitCond(alice.stateName == CLOSED) } @@ -1063,37 +1068,33 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Alice force-closes. - val closingState1 = localClose(alice, alice2blockchain) - assert(closingState1.claimMainDelayedOutputTx.nonEmpty) - val claimMainTx = closingState1.claimMainDelayedOutputTx.get.tx - assert(getHtlcSuccessTxs(closingState1).isEmpty) - assert(getHtlcTimeoutTxs(closingState1).length == 1) - val htlcTimeoutTx = getHtlcTimeoutTxs(closingState1).head.tx + val (closingState1, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.mainTx_opt.nonEmpty) + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // The commit tx confirms. alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState1.commitTx) + closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(42), 1, tx)) alice2blockchain.expectNoMessage(100 millis) // Alice receives the preimage for the incoming HTLC. alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true) - val htlcSuccessTx = inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { p => - assert(p.tx.isInstanceOf[ReplaceableHtlcSuccess]) - assert(p.tx.asInstanceOf[ReplaceableHtlcSuccess].preimage == preimage) - p.tx.txInfo.tx - } + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcSuccess] + assert(htlcSuccess.preimage == preimage) + val htlcSuccessTx = htlcSuccess.txInfo.tx alice2blockchain.expectNoMessage(100 millis) - val closingState2 = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - assert(getHtlcSuccessTxs(closingState2).length == 1) // The HTLC txs confirms, so we publish 3rd-stage txs. alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcTimeoutTx) - val claimHtlcTimeoutDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - alice2blockchain.expectWatchTxConfirmed(claimHtlcTimeoutDelayedTx.txid, parentTxId = htlcTimeoutTx.txid) - Transaction.correctlySpends(claimHtlcTimeoutDelayedTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcTimeoutDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcTimeoutDelayedTx.input == OutPoint(htlcTimeoutTx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutDelayedTx.input) + Transaction.correctlySpends(htlcTimeoutDelayedTx.tx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, htlcSuccessTx) - val claimHtlcSuccessDelayedTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - alice2blockchain.expectWatchTxConfirmed(claimHtlcSuccessDelayedTx.txid, parentTxId = htlcSuccessTx.txid) - Transaction.correctlySpends(claimHtlcSuccessDelayedTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccessDelayedTx = alice2blockchain.expectFinalTxPublished("htlc-delayed") + assert(htlcSuccessDelayedTx.input == OutPoint(htlcSuccessTx, 0)) + alice2blockchain.expectWatchOutputSpent(htlcSuccessDelayedTx.input) + Transaction.correctlySpends(htlcSuccessDelayedTx.tx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) // We simulate a node restart after a feerate increase. val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1104,21 +1105,152 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // We re-publish closing transactions. - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimMainTx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcTimeoutDelayedTx.txid) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == claimHtlcSuccessDelayedTx.txid) - alice2blockchain.expectWatchTxConfirmed(claimMainTx.txid) - alice2blockchain.expectWatchTxConfirmed(claimHtlcTimeoutDelayedTx.txid) - alice2blockchain.expectWatchTxConfirmed(claimHtlcSuccessDelayedTx.txid) + val mainTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchOutputSpent(mainTx.input) + val htlcDelayedTxs = Seq( + alice2blockchain.expectFinalTxPublished("htlc-delayed"), + alice2blockchain.expectFinalTxPublished("htlc-delayed"), + ) + assert(htlcDelayedTxs.map(_.input).toSet == Seq(htlcTimeoutDelayedTx, htlcSuccessDelayedTx).map(_.input).toSet) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedTxs.map(_.input)) // We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction. alice ! CMD_FULFILL_HTLC(incomingHtlc.id, preimage, commit = true) alice2blockchain.expectNoMessage(100 millis) // The remaining transactions confirm. - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 0, claimMainTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 1, claimHtlcTimeoutDelayedTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(43), 2, claimHtlcSuccessDelayedTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 0, mainTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 1, htlcTimeoutDelayedTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(43), 2, htlcSuccessDelayedTx.tx) + awaitCond(alice.stateName == CLOSED) + } + + test("recv INPUT_RESTORED (htlcs claimed by both local and remote)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + // Alice and Bob each sends 3 HTLCs: + // - one of them will be fulfilled and claimed with the preimage on-chain + // - one of them will be fulfilled but will lose the race with the htlc-timeout on-chain + // - the other will be timed out on-chain + val (r1a, htlc1a) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) + val (r2a, htlc2a) = addHtlc(55_000_000 msat, alice, bob, alice2bob, bob2alice) + addHtlc(60_000_000 msat, alice, bob, alice2bob, bob2alice) + val (r1b, htlc1b) = addHtlc(75_000_000 msat, bob, alice, bob2alice, alice2bob) + val (r2b, htlc2b) = addHtlc(55_000_000 msat, bob, alice, bob2alice, alice2bob) + addHtlc(40_000_000 msat, bob, alice, bob2alice, alice2bob) + crossSign(alice, bob, alice2bob, bob2alice) + // Bob has the preimage for 2 of the 3 HTLCs he received. + bob ! CMD_FULFILL_HTLC(htlc1a.id, r1a) + bob2alice.expectMsgType[UpdateFulfillHtlc] + bob ! CMD_FULFILL_HTLC(htlc2a.id, r2a) + bob2alice.expectMsgType[UpdateFulfillHtlc] + // Alice has the preimage for 2 of the 3 HTLCs she received. + alice ! CMD_FULFILL_HTLC(htlc1b.id, r1b) + alice2bob.expectMsgType[UpdateFulfillHtlc] + alice ! CMD_FULFILL_HTLC(htlc2b.id, r2b) + alice2bob.expectMsgType[UpdateFulfillHtlc] + + // Alice force-closes. + val (closingStateAlice, closingTxsAlice) = localClose(alice, alice2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 3) + assert(closingStateAlice.htlcTxs.size == 6) + assert(closingTxsAlice.htlcSuccessTxs.size == 2) + assert(closingTxsAlice.htlcTimeoutTxs.size == 3) + + // Bob detects Alice's force-close. + val (closingStateBob, closingTxsBob) = remoteClose(closingStateAlice.commitTx, bob, bob2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 3) + assert(closingStateBob.claimHtlcTxs.size == 6) + assert(closingTxsBob.htlcSuccessTxs.size == 2) + assert(closingTxsBob.htlcTimeoutTxs.size == 3) + + // The commit transaction and main transactions confirm. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 3, closingStateAlice.commitTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 5, closingTxsAlice.anchorTx_opt.get) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 1, closingTxsAlice.mainTx_opt.get) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_001), 2, closingTxsBob.mainTx_opt.get) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_000), 3, closingStateAlice.commitTx) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_001), 1, closingTxsAlice.mainTx_opt.get) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_001), 2, closingTxsBob.mainTx_opt.get) + bob2blockchain.expectNoMessage(100 millis) + + // One of Alice's HTLC-success transactions confirms. + val htlcSuccessAlice = closingTxsAlice.htlcSuccessTxs.head + alice ! WatchTxConfirmedTriggered(BlockHeight(750_005), 0, htlcSuccessAlice) + val htlcDelayed1 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + Transaction.correctlySpends(htlcDelayed1.tx, Seq(htlcSuccessAlice), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcDelayed1.input) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_005), 0, htlcSuccessAlice) + bob2blockchain.expectNoMessage(100 millis) + + // One of Bob's HTLC-success transactions confirms. + val htlcSuccessBob = closingTxsBob.htlcSuccessTxs.last + alice ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcSuccessBob) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcSuccessBob) + bob2blockchain.expectNoMessage(100 millis) + + // Alice and Bob have two remaining HTLC-timeout transactions, one of which conflicts with an HTLC-success transaction. + val htlcTimeoutTxsAlice = closingTxsAlice.htlcTimeoutTxs.filter(_.txIn.head.outPoint != htlcSuccessBob.txIn.head.outPoint) + assert(htlcTimeoutTxsAlice.size == 2) + val htlcTimeoutTxsBob = closingTxsBob.htlcTimeoutTxs.filter(_.txIn.head.outPoint != htlcSuccessAlice.txIn.head.outPoint) + assert(htlcTimeoutTxsBob.size == 2) + val htlcTimeoutTxBob1 = htlcTimeoutTxsBob.find(_.txIn.head.outPoint == closingTxsAlice.htlcSuccessTxs.last.txIn.head.outPoint).get + val htlcTimeoutTxBob2 = htlcTimeoutTxsBob.find(_.txIn.head.outPoint != closingTxsAlice.htlcSuccessTxs.last.txIn.head.outPoint).get + + // Bob's HTLC-timeout transaction which conflicts with Alice's HTLC-success transaction confirms. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcTimeoutTxBob1) + alice2blockchain.expectNoMessage(100 millis) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_008), 13, htlcTimeoutTxBob1) + bob2blockchain.expectNoMessage(100 millis) + val remainingHtlcOutputs = htlcTimeoutTxBob2.txIn.head.outPoint +: htlcTimeoutTxsAlice.map(_.txIn.head.outPoint) + + // We simulate a node restart after a feerate decrease. + Seq(alice, bob).foreach { peer => + val beforeRestart = peer.stateData.asInstanceOf[DATA_CLOSING] + peer.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2500 sat))) + peer.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + peer ! INPUT_RESTORED(beforeRestart) + awaitCond(peer.stateName == CLOSING) + } + Seq(alice2blockchain, bob2blockchain).foreach(_.expectMsgType[SetChannelId]) + + // Alice re-publishes closing transactions: her remaining HTLC-success transaction has been double-spent, so she + // only has HTLC-timeout transactions left. + val republishedHtlcTxsAlice = (1 to 2).map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableHtlcTimeout]) + alice2blockchain.expectWatchOutputsSpent(remainingHtlcOutputs) + assert(republishedHtlcTxsAlice.map(_.txInfo.input.outPoint).toSet == htlcTimeoutTxsAlice.map(_.txIn.head.outPoint).toSet) + assert(alice2blockchain.expectFinalTxPublished("htlc-delayed").input == htlcDelayed1.input) + alice2blockchain.expectWatchOutputSpent(htlcDelayed1.input) + alice2blockchain.expectNoMessage(100 millis) + + // Bob re-publishes closing transactions: he has 1 HTLC-success and 1 HTLC-timeout transactions left. + val republishedHtlcTxsBob = (1 to 2).map(_ => bob2blockchain.expectMsgType[PublishReplaceableTx]) + bob2blockchain.expectWatchOutputsSpent(remainingHtlcOutputs ++ closingTxsBob.anchorTx_opt.map(_.txIn.head.outPoint).toSeq) + assert(republishedHtlcTxsBob.map(_.input).toSet == Set(htlcTimeoutTxBob2.txIn.head.outPoint, closingTxsBob.htlcSuccessTxs.head.txIn.head.outPoint)) + bob2blockchain.expectNoMessage(100 millis) + + // Bob's previous HTLC-timeout transaction confirms. + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 21, htlcTimeoutTxBob2) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 21, htlcTimeoutTxBob2) + + // Alice's re-published HTLC-timeout transactions confirm. + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 25, republishedHtlcTxsAlice.head.txInfo.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 25, republishedHtlcTxsAlice.head.txInfo.tx) + val htlcDelayed2 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed2.input) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + bob ! WatchTxConfirmedTriggered(BlockHeight(750_009), 26, republishedHtlcTxsAlice.last.txInfo.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(750_009), 26, republishedHtlcTxsAlice.last.txInfo.tx) + val htlcDelayed3 = alice2blockchain.expectFinalTxPublished("htlc-delayed") + alice2blockchain.expectWatchOutputSpent(htlcDelayed3.input) + assert(alice.stateName == CLOSING) + awaitCond(bob.stateName == CLOSED) + + // Alice's 3rd-stage transactions confirm. + Seq(htlcDelayed1, htlcDelayed2, htlcDelayed3).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(750_100), 0, p.tx)) + alice2blockchain.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } @@ -1132,7 +1264,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // and signs it (but bob doesn't sign it) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, _) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] @@ -1200,10 +1332,10 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(bobCommitTx.txOut.exists(_.publicKeyScript == Script.write(Script.pay2wpkh(DummyOnChainWallet.dummyReceivePubkey)))) // bob's commit tx sends directly to our wallet + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.isEmpty) assert(closingState.claimHtlcTxs.isEmpty) + assert(closingTxs.mainTx_opt.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) @@ -1228,8 +1360,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) // Bob publishes his last current commit tx, the one it had when entering NEGOTIATING state. val bobCommitTx = bobCommitTxs.last.commitTx.tx - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTxs.isEmpty) + assert(closingTxs.mainTx_opt.isEmpty) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit @@ -1239,8 +1372,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val bobCommitTx = bobCommitTxs.last.commitTx.tx - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimAnchorTxs.nonEmpty) + assert(closingTxs.anchorTx_opt.nonEmpty) val replyTo = TestProbe() alice ! CMD_BUMP_FORCE_CLOSE_FEE(replyTo.ref, ConfirmationTarget.Priority(ConfirmationPriority.Fast)) @@ -1259,15 +1393,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingTxs.mainTx_opt.nonEmpty) assert(closingState.claimHtlcTxs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) txListener.expectMsgType[TransactionPublished] alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.mainTx_opt.get) assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) awaitCond(alice.stateName == CLOSED) } @@ -1297,14 +1432,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingTxs.mainTx_opt.nonEmpty) assert(closingState.claimHtlcTxs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingState.claimMainOutputTx.get.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.mainTx_opt.get) awaitCond(alice.stateName == CLOSED) } @@ -1328,22 +1464,21 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) assert(closingState.claimHtlcTxs.size == 3) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) - assert(claimHtlcTimeoutTxs.length == 3) + assert(closingTxs.htlcTimeoutTxs.length == 3) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) // for static_remote_key channels there is no claimMainOutputTx (bob's commit tx directly sends to our wallet) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == Set(htlca1, htlca2, htlca3)) alice2relayer.expectNoMessage(100 millis) @@ -1379,27 +1514,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the first commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx assert(bobCommitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 HTLCs - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimMainOutputTx.nonEmpty) assert(closingState.claimHtlcTxs.size == 2) - assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).isEmpty) + assert(closingTxs.htlcTxs.isEmpty) // we don't have the preimage to claim the htlc-success yet // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.tx.isInstanceOf[ReplaceableClaimHtlcSuccess]) - assert(publishHtlcSuccessTx.tx.asInstanceOf[ReplaceableClaimHtlcSuccess].preimage == r1) - assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) - val claimHtlcSuccessTx = publishHtlcSuccessTx.tx.txInfo.tx - Transaction.correctlySpends(claimHtlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + val htlcSuccessTx = htlcSuccess.txInfo.tx + Transaction.correctlySpends(htlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment confirms: the third htlc was not included in the commit tx published on-chain, so we can consider it failed. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) + closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 1, tx)) assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc3) // Alice's main transaction confirms. - closingState.claimMainOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. alice ! CMD_FAIL_HTLC(htlc2.id, FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None) @@ -1412,9 +1545,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) // Alice republishes the HTLC-success transaction, which then confirms. - assert(alice2blockchain.expectMsgType[PublishReplaceableTx].input == publishHtlcSuccessTx.input) - alice2blockchain.expectWatchOutputSpent(publishHtlcSuccessTx.input) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx) + assert(alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess].txInfo.input == htlcSuccess.txInfo.input) + alice2blockchain.expectWatchOutputSpent(htlcSuccess.txInfo.input.outPoint) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccessTx) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) @@ -1424,11 +1557,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ // alice sends an htlc to bob - val (_, htlca) = addHtlc(50000000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) + val (_, htlc) = addHtlc(50_000_000 msat, CltvExpiryDelta(24), alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - val htlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head + val (_, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.htlcTimeoutTxs.size == 1) + val htlcTimeoutTx = closingTxs.htlcTxs.head alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // simulate a node restart @@ -1439,15 +1573,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSING) // we should re-publish unconfirmed transactions - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - val publishClaimHtlcTimeoutTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishClaimHtlcTimeoutTx.tx.txInfo == htlcTimeoutTx) - assert(publishClaimHtlcTimeoutTx.confirmationTarget == ConfirmationTarget.Absolute(htlca.cltvExpiry.blockHeight)) - closingState.claimMainOutputTx.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.tx.txid)) - alice2blockchain.expectWatchOutputSpent(htlcTimeoutTx.input.outPoint) + closingTxs.mainTx_opt.foreach(tx => { + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint) + }) + val htlcTimeout = alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout](ConfirmationTarget.Absolute(htlc.cltvExpiry.blockHeight)) + assert(htlcTimeout.txInfo.input.outPoint == htlcTimeoutTx.txIn.head.outPoint) + alice2blockchain.expectWatchOutputSpent(htlcTimeout.txInfo.input.outPoint) } - private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { + private def testNextRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, PublishedForceCloseTxs, Set[UpdateAddHtlc]) = { import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) @@ -1473,29 +1608,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(getClaimHtlcTimeoutTxs(closingState).length == 3) - (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) + assert(closingState.claimHtlcTxs.size == 3) + assert(closingTxs.htlcTimeoutTxs.size == 3) + (bobCommitTx, closingTxs, Set(htlca1, htlca2, htlca3)) } test("recv WatchTxConfirmedTriggered (next remote commit)") { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the remote commit - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(100 millis) @@ -1504,18 +1639,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (next remote commit, static_remotekey)", Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output + assert(closingTxs.mainTx_opt.isEmpty) // with static_remotekey we don't claim out main output alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(100 millis) @@ -1524,18 +1658,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchTxConfirmedTriggered (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState).map(_.tx) + val (bobCommitTx, closingTxs, htlcs) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, claimMainOutputTx.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(45), 0, tx)) alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, claimHtlcTimeoutTxs(0)) + alice ! WatchTxConfirmedTriggered(BlockHeight(201), 0, closingTxs.htlcTimeoutTxs(0)) val forwardedFail1 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, claimHtlcTimeoutTxs(1)) + alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, claimHtlcTimeoutTxs(2)) + alice ! WatchTxConfirmedTriggered(BlockHeight(203), 1, closingTxs.htlcTimeoutTxs(2)) val forwardedFail3 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) == htlcs) alice2relayer.expectNoMessage(100 millis) @@ -1562,26 +1695,24 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Now Bob publishes the next commit tx (force-close). val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx assert(bobCommitTx.txOut.length == 7) // 2 main outputs + 2 anchor outputs + 3 HTLCs - val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) + val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) assert(closingState.claimMainOutputTx.nonEmpty) assert(closingState.claimHtlcTxs.size == 3) - assert(getClaimHtlcSuccessTxs(closingState).isEmpty) // we don't have the preimage to claim the htlc-success yet - assert(getClaimHtlcTimeoutTxs(closingState).length == 1) - val claimHtlcTimeoutTx = getClaimHtlcTimeoutTxs(closingState).head + assert(closingTxs.htlcTxs.size == 1) // we don't have the preimage to claim the htlc-success yet + val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. alice ! CMD_FULFILL_HTLC(htlc1.id, r1, commit = true) - val publishHtlcSuccessTx = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(publishHtlcSuccessTx.tx.isInstanceOf[ReplaceableClaimHtlcSuccess]) - assert(publishHtlcSuccessTx.tx.asInstanceOf[ReplaceableClaimHtlcSuccess].preimage == r1) - assert(publishHtlcSuccessTx.confirmationTarget == ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) - val claimHtlcSuccessTx = publishHtlcSuccessTx.tx.txInfo.asInstanceOf[ClaimHtlcSuccessTx] - Transaction.correctlySpends(claimHtlcSuccessTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcSuccess = alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess](ConfirmationTarget.Absolute(htlc1.cltvExpiry.blockHeight)) + assert(htlcSuccess.preimage == r1) + val htlcSuccessTx = htlcSuccess.txInfo.tx + Transaction.correctlySpends(htlcSuccessTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectNoMessage(100 millis) // Bob's commitment and Alice's main transaction confirm. alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - closingState.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainOutputTx.tx)) + closingTxs.anchorTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) // Alice receives a failure for the second HTLC from downstream; she can stop watching the corresponding HTLC output. alice ! CMD_FAIL_HTLC(htlc2.id, FailureReason.EncryptedDownstreamFailure(ByteVector.empty, None), None) @@ -1596,12 +1727,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice republishes the HTLC transactions, which then confirm. val htlcTx1 = alice2blockchain.expectMsgType[PublishReplaceableTx] val htlcTx2 = alice2blockchain.expectMsgType[PublishReplaceableTx] - assert(Set(htlcTx1.input, htlcTx2.input) == Set(claimHtlcTimeoutTx.input.outPoint, claimHtlcSuccessTx.input.outPoint)) - alice2blockchain.expectWatchOutputsSpent(htlcTx1.input :: htlcTx2.input :: Nil) + assert(Set(htlcTx1.input, htlcTx2.input) == Set(htlcTimeoutTx.txIn.head.outPoint, htlcSuccessTx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputsSpent(Seq(htlcTx1.input, htlcTx2.input)) alice2blockchain.expectNoMessage(100 millis) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcSuccessTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcSuccessTx) assert(alice.stateName == CLOSING) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimHtlcTimeoutTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, htlcTimeoutTx) assert(alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc == htlc3) alice2blockchain.expectNoMessage(100 millis) alice2relayer.expectNoMessage(100 millis) @@ -1611,8 +1742,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_RESTORED (next remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val (bobCommitTx, closingState, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) - val claimHtlcTimeoutTxs = getClaimHtlcTimeoutTxs(closingState) + val (bobCommitTx, closingTxs, _) = testNextRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // simulate a node restart val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1624,15 +1754,15 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - inside(alice2blockchain.expectMsgType[PublishReplaceableTx]) { publish => - assert(publish.tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) - assert(publish.tx.commitTx == bobCommitTx) - } - closingState.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - claimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(alice2blockchain.expectMsgType[PublishReplaceableTx].tx.txInfo.tx == claimHtlcTimeout.tx)) + val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] + assert(anchorTx.commitTx == bobCommitTx) + closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("remote-main-delayed")) + val htlcTimeoutTxs = closingTxs.htlcTxs.map(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcTimeout]) + assert(htlcTimeoutTxs.map(_.txInfo.input.outPoint).toSet == closingTxs.htlcTxs.map(_.txIn.head.outPoint).toSet) alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) - closingState.claimMainOutputTx.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.tx.txid)) - alice2blockchain.expectWatchOutputsSpent(claimHtlcTimeoutTxs.map(_.input.outPoint)) + closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(anchorTx.txInfo.input.outPoint) + alice2blockchain.expectWatchOutputsSpent(htlcTimeoutTxs.map(_.txInfo.input.outPoint)) } private def testFutureRemoteCommitTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Transaction = { @@ -1701,32 +1831,31 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) // using option_static_remotekey alice doesn't need to sweep her output - awaitCond(alice.stateName == CLOSING, 10 seconds) + awaitCond(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // after the commit tx is confirmed the channel is closed, no claim transactions needed - awaitCond(alice.stateName == CLOSED, 10 seconds) + awaitCond(alice.stateName == CLOSED) } test("recv WatchTxConfirmedTriggered (future remote commit, anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // alice is able to claim its main output - val claimMainTx = alice2blockchain.expectMsgType[PublishFinalTx].tx - Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + Transaction.correctlySpends(mainTx.tx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) - alice2blockchain.expectWatchTxConfirmed(claimMainTx.txid) + alice2blockchain.expectWatchOutputSpent(mainTx.input) alice2blockchain.expectNoMessage(100 millis) // alice ignores the htlc-timeout + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].futureRemoteCommitPublished.isDefined) // actual test starts here alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMainTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, mainTx.tx) awaitCond(alice.stateName == CLOSED) } test("recv INPUT_RESTORED (future remote commit)") { f => import f._ - val bobCommitTx = testFutureRemoteCommitTxConfirmed(f, ChannelFeatures(Features.StaticRemoteKey)) // simulate a node restart @@ -1737,6 +1866,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob's commit tx sends funds directly to our wallet alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) + awaitCond(alice.stateName == CLOSED) } case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) @@ -1800,7 +1931,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) } - private def setupFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RevokedCommitPublished) = { + case class RevokedCloseTxs(mainTx_opt: Option[Transaction], mainPenaltyTx: Transaction, htlcPenaltyTxs: Seq[Transaction]) + + private def setupFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): (Transaction, RevokedCloseTxs) = { import f._ val revokedCloseFixture = prepareRevokedClose(f, channelFeatures) @@ -1820,48 +1953,35 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(rvk.mainPenaltyTx.nonEmpty) assert(rvk.htlcPenaltyTxs.size == 2) assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) - val penaltyTxs = rvk.claimMainOutputTx.toList ++ rvk.mainPenaltyTx.toList ++ rvk.htlcPenaltyTxs // alice publishes the penalty txs - if (!channelFeatures.paysDirectlyToWallet) { - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.claimMainOutputTx.get.tx) - } - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) - assert(Set(alice2blockchain.expectMsgType[PublishFinalTx].tx, alice2blockchain.expectMsgType[PublishFinalTx].tx) == rvk.htlcPenaltyTxs.map(_.tx).toSet) - for (penaltyTx <- penaltyTxs) { - Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } + val mainTx_opt = if (!channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectFinalTxPublished("remote-main-delayed")) else None + val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty") + Transaction.correctlySpends(mainPenaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val htlcPenaltyTxs = (0 until 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + assert(htlcPenaltyTxs.map(_.input).toSet == rvk.htlcPenaltyTxs.map(_.input.outPoint).toSet) + htlcPenaltyTxs.foreach(penaltyTx => Transaction.correctlySpends(penaltyTx.tx, bobRevokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // alice spends all outpoints of the revoked tx, except her main output when it goes directly to our wallet - val spentOutpoints = penaltyTxs.flatMap(_.tx.txIn.map(_.outPoint)).toSet - assert(spentOutpoints.forall(_.txid == bobRevokedTx.txid)) - if (channelFeatures.commitmentFormat.isInstanceOf[AnchorOutputsCommitmentFormat]) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors - } - else if (channelFeatures.paysDirectlyToWallet) { - assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet - } else { - assert(spentOutpoints.size == bobRevokedTx.txOut.size) + val spentOutpoints = mainTx_opt.map(_.input) ++ Seq(mainPenaltyTx.input) ++ htlcPenaltyTxs.map(_.input) + channelFeatures.commitmentFormat match { + case DefaultCommitmentFormat if channelFeatures.paysDirectlyToWallet => assert(spentOutpoints.size == bobRevokedTx.txOut.size - 1) // we don't claim our main output, it directly goes to our wallet + case DefaultCommitmentFormat => assert(spentOutpoints.size == bobRevokedTx.txOut.size) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(spentOutpoints.size == bobRevokedTx.txOut.size - 2) // we don't claim the anchors } - // alice watches confirmation for the outputs only her can claim + // alice watches on-chain transactions alice2blockchain.expectWatchTxConfirmed(bobRevokedTx.txid) - if (!channelFeatures.paysDirectlyToWallet) { - alice2blockchain.expectWatchTxConfirmed(rvk.claimMainOutputTx.get.tx.txid) - } - - // alice watches outputs that can be spent by both parties - alice2blockchain.expectWatchOutputSpent(rvk.mainPenaltyTx.get.input.outPoint) - alice2blockchain.expectWatchOutputsSpent(rvk.htlcPenaltyTxs.map(_.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(spentOutpoints.toSeq) alice2blockchain.expectNoMessage(100 millis) - (bobRevokedTx, rvk) + (bobRevokedTx, RevokedCloseTxs(mainTx_opt.map(_.tx), mainPenaltyTx.tx, htlcPenaltyTxs.map(_.tx))) } private def testFundingSpentRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) + val (bobRevokedTx, closingTxs) = setupFundingSpentRevokedTx(f, channelFeatures) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobRevokedTx) assert(txPublished.miningFee > 0.sat) // alice is funder, she pays the fee for the revoked commit @@ -1869,13 +1989,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // once all txs are confirmed, alice can move to the closed state alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, bobRevokedTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == bobRevokedTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.mainPenaltyTx.get.tx) - if (!channelFeatures.paysDirectlyToWallet) { - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, rvk.claimMainOutputTx.get.tx) - } - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk.htlcPenaltyTxs(0).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, closingTxs.mainPenaltyTx) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, tx)) + closingTxs.htlcPenaltyTxs.dropRight(1).foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, tx)) assert(alice.stateName == CLOSING) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk.htlcPenaltyTxs(1).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs.last) awaitCond(alice.stateName == CLOSED) } @@ -1896,52 +2014,44 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey)) assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct - def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCommitPublished = { + def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCloseTxs = { alice ! WatchFundingSpentTriggered(revokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == revokedCount) assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last.commitTx == revokedTx) // alice publishes penalty txs - val mainPenalty = alice2blockchain.expectMsgType[PublishFinalTx].tx - val claimMain_opt = if (!alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectMsgType[PublishFinalTx].tx) else None - val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectMsgType[PublishFinalTx].tx) - (mainPenalty +: (claimMain_opt.toList ++ htlcPenaltyTxs)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - - // alice watches confirmation for the outputs only her can claim + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenaltyTxs = (1 to htlcCount).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + (mainPenalty.tx +: htlcPenaltyTxs.map(_.tx)).foreach(tx => Transaction.correctlySpends(tx, revokedTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchTxConfirmed(revokedTx.txid) - claimMain_opt.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.txid)) - - // alice watches outputs that can be spent by both parties - alice2blockchain.expectWatchOutputSpent(mainPenalty.txIn.head.outPoint) - alice2blockchain.expectWatchOutputsSpent(htlcPenaltyTxs.flatMap(_.txIn.map(_.outPoint))) + alice2blockchain.expectWatchOutputsSpent(mainPenalty.input +: htlcPenaltyTxs.map(_.input)) alice2blockchain.expectNoMessage(100 millis) - alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.last + RevokedCloseTxs(None, mainPenalty.tx, htlcPenaltyTxs.map(_.tx)) } // bob publishes a first revoked tx (no htlc in that commitment) broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTxAndRemoteSig.commitTx.tx, 0, 1) // bob publishes a second revoked tx - val rvk2 = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx, 2, 2) + val closingTxs = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx, 2, 2) // bob publishes a third revoked tx broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTxAndRemoteSig.commitTx.tx, 4, 3) // bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state // NB: if multiple txs confirm in the same block, we may receive the events in any order - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, rvk2.mainPenaltyTx.get.tx) - rvk2.claimMainOutputTx.foreach(claimMainOutputTx => alice ! WatchTxConfirmedTriggered(BlockHeight(100), 2, claimMainOutputTx.tx)) - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk2.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, rvk2.htlcPenaltyTxs(0).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, closingTxs.mainPenaltyTx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, closingTxs.htlcPenaltyTxs(0)) assert(alice.stateName == CLOSING) - alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, rvk2.htlcPenaltyTxs(1).tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs(1)) awaitCond(alice.stateName == CLOSED) } def testInputRestoredRevokedTx(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ - val (bobRevokedTx, rvk) = setupFundingSpentRevokedTx(f, channelFeatures) + val (bobRevokedTx, closingTxs) = setupFundingSpentRevokedTx(f, channelFeatures) // simulate a node restart val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -1953,13 +2063,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the commit tx hasn't been confirmed yet, so we watch the funding output first alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions - rvk.claimMainOutputTx.foreach(claimMain => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimMain.tx)) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == rvk.mainPenaltyTx.get.tx) - rvk.htlcPenaltyTxs.foreach(htlcPenalty => assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == htlcPenalty.tx)) + closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("remote-main-delayed")) + assert(alice2blockchain.expectFinalTxPublished("main-penalty").input == closingTxs.mainPenaltyTx.txIn.head.outPoint) + val htlcPenaltyTxs = closingTxs.htlcPenaltyTxs.map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) + assert(htlcPenaltyTxs.map(_.input).toSet == closingTxs.htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet) alice2blockchain.expectWatchTxConfirmed(bobRevokedTx.txid) - rvk.claimMainOutputTx.foreach(claimMain => alice2blockchain.expectWatchTxConfirmed(claimMain.tx.txid)) - alice2blockchain.expectWatchOutputSpent(rvk.mainPenaltyTx.get.input.outPoint) - alice2blockchain.expectWatchOutputsSpent(rvk.htlcPenaltyTxs.map(_.input.outPoint)) + closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) + alice2blockchain.expectWatchOutputSpent(closingTxs.mainPenaltyTx.txIn.head.outPoint) + alice2blockchain.expectWatchOutputsSpent(htlcPenaltyTxs.map(_.input)) } test("recv INPUT_RESTORED (one revoked tx)") { f => @@ -1997,22 +2108,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs - val claimTxsCount = if (channelFeatures.paysDirectlyToWallet) 5 else 6 // 2 main outputs and 4 htlcs - (1 to claimTxsCount).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) + val mainTx_opt = if (!channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectFinalTxPublished("remote-main-delayed")) else None + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid) - if (!channelFeatures.paysDirectlyToWallet) { - alice2blockchain.expectWatchTxConfirmed(rvk.claimMainOutputTx.get.tx.txid) - } - alice2blockchain.expectWatchOutputSpent(rvk.mainPenaltyTx.get.input.outPoint) - alice2blockchain.expectWatchOutputsSpent(rvk.htlcPenaltyTxs.map(_.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(mainTx_opt.map(_.input).toSeq ++ Seq(mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) // the revoked commit and main penalty transactions confirm alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, rvk.commitTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, rvk.mainPenaltyTx.get.tx) - if (!channelFeatures.paysDirectlyToWallet) { - alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, rvk.claimMainOutputTx.get.tx) - } + alice ! WatchTxConfirmedTriggered(BlockHeight(110), 0, mainPenalty.tx) + mainTx_opt.foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(110), 1, p.tx)) // bob publishes one of his HTLC-success transactions val (fulfilledHtlc, _) = revokedCloseFixture.htlcsAlice.head @@ -2037,19 +2143,17 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob's HTLC-timeout confirms: alice reacts by publishing a penalty tx alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, bobHtlcTimeoutTx.tx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 1) - val claimHtlcTimeoutPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.head - Transaction.correctlySpends(claimHtlcTimeoutPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcTimeoutPenalty.tx) - alice2blockchain.expectWatchOutputSpent(claimHtlcTimeoutPenalty.input.outPoint) + val htlcTimeoutDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") + Transaction.correctlySpends(htlcTimeoutDelayedPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcTimeoutDelayedPenalty.input) alice2blockchain.expectNoMessage(100 millis) // bob's htlc-success RBF confirms: alice reacts by publishing a penalty tx alice ! WatchTxConfirmedTriggered(BlockHeight(115), 1, bobHtlcSuccessTx2) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) - val claimHtlcSuccessPenalty = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.last - Transaction.correctlySpends(claimHtlcSuccessPenalty.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - assert(alice2blockchain.expectMsgType[PublishFinalTx].tx == claimHtlcSuccessPenalty.tx) - alice2blockchain.expectWatchOutputSpent(claimHtlcSuccessPenalty.input.outPoint) + val htlcSuccessDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") + Transaction.correctlySpends(htlcSuccessDelayedPenalty.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputSpent(htlcSuccessDelayedPenalty.input) alice2blockchain.expectNoMessage(100 millis) // transactions confirm: alice can move to the closed state @@ -2058,10 +2162,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(remainingHtlcPenaltyTxs.size == 2) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, remainingHtlcPenaltyTxs.head.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, remainingHtlcPenaltyTxs.last.tx) - alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, claimHtlcTimeoutPenalty.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(120), 0, htlcTimeoutDelayedPenalty.tx) assert(alice.stateName == CLOSING) - - alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, claimHtlcSuccessPenalty.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(121), 0, htlcSuccessDelayedPenalty.tx) awaitCond(alice.stateName == CLOSED) } @@ -2093,11 +2196,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) // alice publishes the penalty txs and watches outputs - (1 to 6).foreach(_ => alice2blockchain.expectMsgType[PublishTx]) // 2 main outputs and 4 htlcs + val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + val mainPenalty = alice2blockchain.expectFinalTxPublished("main-penalty") + val htlcPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-penalty")) alice2blockchain.expectWatchTxConfirmed(rvk.commitTx.txid) - alice2blockchain.expectWatchTxConfirmed(rvk.claimMainOutputTx.get.tx.txid) - alice2blockchain.expectWatchOutputSpent(rvk.mainPenaltyTx.get.input.outPoint) - alice2blockchain.expectWatchOutputsSpent(rvk.htlcPenaltyTxs.map(_.input.outPoint)) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) alice2blockchain.expectNoMessage(100 millis) // bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures @@ -2135,18 +2238,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchTxConfirmed(bobHtlcTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(129), 7, bobHtlcTx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4) - val claimHtlcDelayedPenaltyTxs = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs + val htlcDelayedPenalty = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) val spentOutpoints = Seq(OutPoint(bobHtlcTx, 1), OutPoint(bobHtlcTx, 2), OutPoint(bobHtlcTx, 3), OutPoint(bobHtlcTx, 4)) - assert(claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints.toSet) - claimHtlcDelayedPenaltyTxs.foreach(claimHtlcPenalty => Transaction.correctlySpends(claimHtlcPenalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) - val publishedPenaltyTxs = Set( - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx], - alice2blockchain.expectMsgType[PublishFinalTx] - ) - assert(publishedPenaltyTxs.map(_.tx) == claimHtlcDelayedPenaltyTxs.map(_.tx).toSet) - assert(publishedPenaltyTxs.map(_.input) == spentOutpoints.toSet) + assert(htlcDelayedPenalty.map(_.input).toSet == spentOutpoints.toSet) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints.toSet) + htlcDelayedPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchOutputsSpent(spentOutpoints) alice2blockchain.expectNoMessage(100 millis) } @@ -2218,9 +2314,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] val bobCommitments = bob.stateData.asInstanceOf[DATA_CLOSING].commitments val bobCurrentPerCommitmentPoint = bob.underlyingActor.channelKeys.commitmentPoint(bobCommitments.localCommitIndex) - alice ! ChannelReestablish(channelId(bob), 42, 42, PrivateKey(ByteVector32.Zeroes), bobCurrentPerCommitmentPoint) - val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) == FundingTxSpent(channelId(alice), initialState.spendingTxs.head.txid).getMessage) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala index fb5ea241ad..24f2a95e79 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PostRestartHtlcCleanerSpec.scala @@ -423,14 +423,14 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit alice2bob.expectMsgType[CommitSig] } - val closingState = localClose(alice, alice2blockchain) + val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 4) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) // All committed htlcs timed out except the last two; one will be fulfilled later and the other will timeout later. assert(closingState.htlcTxs.size == 4) - assert(getHtlcTimeoutTxs(closingState).length == 4) - val htlcTxs = getHtlcTimeoutTxs(closingState).sortBy(_.tx.txOut.map(_.amount).sum) + assert(closingTxs.htlcTxs.size == 4) + val htlcTxs = closingTxs.htlcTxs.sortBy(_.txOut.map(_.amount).sum) htlcTxs.reverse.drop(2).zipWithIndex.foreach { - case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx.tx) + case (htlcTx, i) => alice ! WatchTxConfirmedTriggered(BlockHeight(201), i, htlcTx) } (alice.stateData.asInstanceOf[DATA_CLOSING], htlc_2_2) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala index b3bd4e6d68..ff75a26337 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/testutils/PimpTestProbe.scala @@ -4,7 +4,10 @@ import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, TxId} import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ +import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.AvailableBalanceChanged +import fr.acinq.eclair.channel.publish.ReplaceableTx +import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx} import org.scalatest.Assertions import scala.reflect.ClassTag @@ -17,11 +20,34 @@ case class PimpTestProbe(probe: TestProbe) extends Assertions { * @param asserts should contains asserts on the message */ def expectMsgTypeHaving[T](asserts: T => Unit)(implicit t: ClassTag[T]): T = { - val msg = probe.expectMsgType[T] + val msg = probe.expectMsgType[T](t) asserts(msg) msg } + def expectFinalTxPublished(desc: String): PublishFinalTx = + expectMsgTypeHaving[PublishFinalTx](p => assert(p.desc == desc)) + + def expectFinalTxPublished(txId: TxId): PublishFinalTx = + expectMsgTypeHaving[PublishFinalTx](p => assert(p.tx.txid == txId)) + + private def expectReplaceableTx[T <: ReplaceableTx](tx: ReplaceableTx)(implicit t: ClassTag[T]): T = { + val c = t.runtimeClass.asInstanceOf[Class[T]] + assert(c.isInstance(tx), s"expected published tx of type ${c.getSimpleName} but got ${tx.getClass.getSimpleName}") + tx.asInstanceOf[T] + } + + def expectReplaceableTxPublished[T <: ReplaceableTx](implicit t: ClassTag[T]): T = { + val p = probe.expectMsgType[PublishReplaceableTx] + expectReplaceableTx(p.tx)(t) + } + + def expectReplaceableTxPublished[T <: ReplaceableTx](confirmationTarget: ConfirmationTarget)(implicit t: ClassTag[T]): T = { + val p = probe.expectMsgType[PublishReplaceableTx] + assert(p.confirmationTarget == confirmationTarget) + expectReplaceableTx(p.tx)(t) + } + def expectWatchFundingSpent(txid: TxId, hints_opt: Option[Set[TxId]] = None): WatchFundingSpent = expectMsgTypeHaving[WatchFundingSpent](w => { assert(w.txId == txid, "txid") @@ -44,12 +70,6 @@ case class PimpTestProbe(probe: TestProbe) extends Assertions { def expectWatchTxConfirmed(txid: TxId): WatchTxConfirmed = expectMsgTypeHaving[WatchTxConfirmed](w => assert(w.txId == txid, "txid")) - def expectWatchTxConfirmed(txid: TxId, parentTxId: TxId): WatchTxConfirmed = - expectMsgTypeHaving[WatchTxConfirmed](w => { - assert(w.txId == txid, "txid") - assert(w.delay_opt.map(_.parentTxId).contains(parentTxId)) - }) - def expectWatchPublished(txid: TxId): WatchPublished = expectMsgTypeHaving[WatchPublished](w => assert(w.txId == txid, "txid"))