diff --git a/docs/FAQ.md b/docs/FAQ.md index 802216d480..b41872fef3 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,6 @@ # FAQ -## What does it mean for a channel to be "enabled" or "disabled" ? +## What does it mean for a channel to be "enabled" or "disabled"? A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled. @@ -13,6 +13,16 @@ There are other cases when a channel becomes disabled, for example when its bala Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific). -## How should you stop an Eclair node ? +## How do I make my closing transactions confirm faster? + +When channels are unilaterally closed, there is a delay before which closing transactions can be published: you must wait for this delay before you can get your funds back. + +Once published, transactions will be automatically RBF-ed by `eclair` based on your configuration values for the [`eclair.on-chain-fees` section](../eclair-core/src/main/resources/reference.conf). + +Note that there is an upper bound on the feerate that will be used, configured by the `eclair.on-chain-fees.max-closing-feerate` parameter. +If the current feerate is higher than this value, your transactions will not confirm. +You should update `eclair.on-chain-fees.max-closing-feerate` in your `eclair.conf` and restart your node: your transactions will automatically be RBF-ed using the new feerate. + +## How should you stop an Eclair node? To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres. diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index a3f03628ac..9605da8b1b 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -36,6 +36,12 @@ It can be enabled by setting `eclair.features.option_attributable_failure = opti ### Miscellaneous improvements and bug fixes +#### Add `max-closing-feerate` configuration parameter + +We added a new configuration value to `eclair.conf` to limit the feerate used for force-close transactions where funds aren't at risk: `eclair.on-chain-fees.max-closing-feerate`. +This ensures that you won't end up paying a lot of fees during mempool congestion: your node will wait for the feerate to decrease to get your non-urgent transactions confirmed. +If you need those transactions to confirm because you are low on liquidity, you should update `eclair.on-chain-fees.max-closing-feerate` and restart your node: `eclair` will automatically RBF all available transactions. + #### Remove confirmation scaling based on funding amount We previously scaled the number of confirmations based on the channel funding amount. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 5fe58f38db..9916a4cdf4 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -271,6 +271,14 @@ eclair { closing = medium } + // Maximum feerate that will be used when closing channels for outputs that aren't at risk (main balance and HTLC 3rd-stage transactions). + // Using a low value here ensures that you won't be paying high fees when the mempool is congested and you're not in + // a hurry to get your channel funds back. + // If closing transactions don't confirm and you need to get the funds back quickly, you should increase this value + // and restart your node: closing transactions will automatically be RBF-ed to match the current feerate. + // This value is in satoshis per byte. + max-closing-feerate = 10 + feerate-tolerance { ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate (only enforced when not using anchor outputs) ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate (for all commitment formats) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 5b7850222b..2360688332 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -607,6 +607,7 @@ object NodeParams extends Logging { ), onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, + maxClosingFeerate = FeeratePerKw(FeeratePerByte(Satoshi(config.getLong("on-chain-fees.max-closing-feerate")))), safeUtxosThreshold = config.getInt("on-chain-fees.safe-utxos-threshold"), spendAnchorWithoutHtlcs = config.getBoolean("on-chain-fees.spend-anchor-without-htlcs"), anchorWithoutHtlcsMaxFee = Satoshi(config.getLong("on-chain-fees.anchor-without-htlcs-max-fee-satoshis")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala index 458c81235a..43a98722b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala @@ -23,7 +23,6 @@ import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Helpers.Closing._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.DirectedHtlc.incoming -import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, HtlcSuccessTx, HtlcTimeoutTx} import fr.acinq.eclair.wire.protocol.UpdateAddHtlc import scala.concurrent.{ExecutionContext, Future} @@ -69,31 +68,26 @@ object CheckBalance { def addLocalClose(lcp: LocalCommitPublished): MainAndHtlcBalance = { // If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance. // Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance. - val additionalToLocal = lcp.claimMainDelayedOutputTx.map(_.input.outPoint) match { + val additionalToLocal = lcp.localOutput_opt match { case Some(outpoint) if !lcp.irrevocablySpent.contains(outpoint) => lcp.commitTx.txOut(outpoint.index.toInt).amount case _ => 0 sat } - val additionalHtlcs = lcp.htlcTxs.map { - case (outpoint, htlcTx_opt) => - val htlcAmount = lcp.commitTx.txOut(outpoint.index.toInt).amount - lcp.irrevocablySpent.get(outpoint) match { - case Some(spendingTx) => - // If the HTLC was spent by us, there will be an entry in our 3rd-stage transactions. - // Otherwise it was spent by the remote and we don't have anything to add to our balance. - val delayedHtlcOutpoint = OutPoint(spendingTx.txid, 0) - val htlcSpentByUs = lcp.claimHtlcDelayedTxs.map(_.input.outPoint).contains(delayedHtlcOutpoint) - // If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance. - // Once confirmed, we should ignore it since it will appear in our on-chain balance. - val htlcDelayedPending = !lcp.irrevocablySpent.contains(delayedHtlcOutpoint) - if (htlcSpentByUs && htlcDelayedPending) htlcAmount else 0 sat - case None => - // We assume that HTLCs will be fulfilled, so we only count incoming HTLCs in our off-chain balance. - htlcTx_opt match { - case Some(_: HtlcSuccessTx) => htlcAmount - case Some(_: HtlcTimeoutTx) => 0 sat - case None => htlcAmount // incoming HTLC for which we don't have the preimage yet - } - } + val additionalHtlcs = lcp.htlcOutputs.map { outpoint => + val htlcAmount = lcp.commitTx.txOut(outpoint.index.toInt).amount + lcp.irrevocablySpent.get(outpoint) match { + case Some(spendingTx) => + // If the HTLC was spent by us, there will be an entry in our 3rd-stage transactions. + // Otherwise it was spent by the remote and we don't have anything to add to our balance. + val delayedHtlcOutpoint = OutPoint(spendingTx.txid, 0) + val htlcSpentByUs = lcp.htlcDelayedOutputs.contains(delayedHtlcOutpoint) + // If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance. + // Once confirmed, we should ignore it since it will appear in our on-chain balance. + val htlcDelayedPending = !lcp.irrevocablySpent.contains(delayedHtlcOutpoint) + if (htlcSpentByUs && htlcDelayedPending) htlcAmount else 0 sat + case None => + // We assume that HTLCs will be fulfilled, so we only count incoming HTLCs in our off-chain balance. + if (lcp.incomingHtlcs.contains(outpoint)) htlcAmount else 0 sat + } }.sum MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs) } @@ -102,16 +96,15 @@ object CheckBalance { def addRemoteClose(rcp: RemoteCommitPublished): MainAndHtlcBalance = { // If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance. // Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance. - val additionalToLocal = rcp.claimMainOutputTx.map(_.input.outPoint) match { + val additionalToLocal = rcp.localOutput_opt match { case Some(outpoint) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount case _ => 0 sat } // If HTLC transactions are confirmed, they will appear in our on-chain balance if we were the one to claim them. // We only need to include incoming HTLCs that haven't been claimed yet (since we assume that they will be fulfilled). // Note that it is their commitment, so incoming/outgoing are inverted. - val additionalHtlcs = rcp.claimHtlcTxs.map { - case (outpoint, Some(_: ClaimHtlcSuccessTx)) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount - case (outpoint, None) if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount // incoming HTLC for which we don't have the preimage yet + val additionalHtlcs = rcp.incomingHtlcs.keys.map { + case outpoint if !rcp.irrevocablySpent.contains(outpoint) => rcp.commitTx.txOut(outpoint.index.toInt).amount case _ => 0 sat }.sum MainAndHtlcBalance(toLocal = toLocal + additionalToLocal, htlcs = htlcs + additionalHtlcs) @@ -122,15 +115,15 @@ object CheckBalance { // If our main transaction isn't deeply confirmed yet, we count it in our off-chain balance. // Once it confirms, it will be included in our on-chain balance, so we ignore it in our off-chain balance. // We do the same thing for our main penalty transaction claiming their main output. - val additionalToLocal = rvk.claimMainOutputTx.map(_.input.outPoint) match { + val additionalToLocal = rvk.localOutput_opt match { case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount case _ => 0 sat } - val additionalToRemote = rvk.mainPenaltyTx.map(_.input.outPoint) match { + val additionalToRemote = rvk.remoteOutput_opt match { case Some(outpoint) if !rvk.irrevocablySpent.contains(outpoint) => rvk.commitTx.txOut(outpoint.index.toInt).amount case _ => 0 sat } - val additionalHtlcs = rvk.htlcPenaltyTxs.map(_.input.outPoint).map(htlcOutpoint => { + val additionalHtlcs = rvk.htlcOutputs.map(htlcOutpoint => { val htlcAmount = rvk.commitTx.txOut(htlcOutpoint.index.toInt).amount rvk.irrevocablySpent.get(htlcOutpoint) match { case Some(spendingTx) => @@ -139,7 +132,7 @@ object CheckBalance { case Some(outputIndex) => // If they managed to get their HTLC transaction confirmed, we published an HTLC-delayed penalty transaction. val delayedHtlcOutpoint = OutPoint(spendingTx.txid, outputIndex) - val htlcSpentByThem = rvk.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).contains(delayedHtlcOutpoint) + val htlcSpentByThem = rvk.htlcDelayedOutputs.contains(delayedHtlcOutpoint) // If our 3rd-stage transaction isn't confirmed yet, we should count it in our off-chain balance. // Once confirmed, we should ignore it since it will appear in our on-chain balance. val htlcDelayedPending = !rvk.irrevocablySpent.contains(delayedHtlcOutpoint) @@ -194,7 +187,7 @@ object CheckBalance { // In the recovery case, we can only claim our main output, HTLC outputs are lost. // Once our main transaction confirms, the channel will transition to the CLOSED state and our channel funds // will appear in our on-chain balance (minus on-chain fees). - case Some(c: RecoveryClose) => c.remoteCommitPublished.claimMainOutputTx.map(_.input.outPoint) match { + case Some(c: RecoveryClose) => c.remoteCommitPublished.localOutput_opt match { case Some(localOutput) => val localBalance = c.remoteCommitPublished.commitTx.txOut(localOutput.index.toInt).amount this.copy(closing = this.closing.copy(toLocal = this.closing.toLocal + localBalance)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala index 8edcb9b868..6ba1dd6b74 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala @@ -91,6 +91,7 @@ case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMax } case class OnChainFeeConf(feeTargets: FeeTargets, + maxClosingFeerate: FeeratePerKw, safeUtxosThreshold: Int, spendAnchorWithoutHtlcs: Boolean, anchorWithoutHtlcsMaxFee: Satoshi, @@ -128,5 +129,5 @@ case class OnChainFeeConf(feeTargets: FeeTargets, } } - def getClosingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.closing.getFeerate(feerates) + def getClosingFeerate(feerates: FeeratesPerKw): FeeratePerKw = feeTargets.closing.getFeerate(feerates).min(maxClosingFeerate) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 196f5c3497..ac8c226acf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -308,10 +308,28 @@ final case class RES_GET_CHANNEL_INFO(nodeId: PublicKey, channelId: ByteVector32 case class ClosingTxProposed(unsignedTx: ClosingTx, localClosingSigned: ClosingSigned) +/** + * When a commitment is published, we keep track of all outputs that can be spent (even if we don't yet have the data + * to spend them, for example the preimage for received HTLCs). Once all of those outputs have been spent by a confirmed + * transaction, the channel close is complete. + * + * Note that we only store transactions after they have been confirmed: we're using RBF to get transactions confirmed, + * and it would be wasteful to store previous versions of the transactions that have been replaced. + */ sealed trait CommitPublished { /** Commitment tx. */ def commitTx: Transaction - /** Map of relevant outpoints that have been spent and the confirmed transaction that spends them. */ + /** Our main output, if we had some balance in the channel. */ + def localOutput_opt: Option[OutPoint] + /** Our anchor output, if one is available to CPFP the [[commitTx]]. */ + def anchorOutput_opt: Option[OutPoint] + /** + * Outputs corresponding to HTLCs that we may be able to claim (even when we don't have the preimage yet). + * Note that some HTLC outputs of the [[commitTx]] may not be included, if we know that we will never claim them + * (such as HTLCs that we didn't relay or that were failed downstream). + */ + def htlcOutputs: Set[OutPoint] + /** Map of outpoints that have been spent and the confirmed transaction that spends them. */ def irrevocablySpent: Map[OutPoint, Transaction] /** Returns true if the commitment transaction is confirmed. */ def isConfirmed: Boolean = { @@ -320,101 +338,57 @@ sealed trait CommitPublished { // the type of closing. irrevocablySpent.values.exists(tx => tx.txid == commitTx.txid) || irrevocablySpent.keys.exists(_.txid == commitTx.txid) } + /** + * Returns true when all outputs that can be claimed have been spent: we can forget the channel at that point. + * Note that some of those outputs may be claimed by our peer (e.g. HTLCs that reached their expiry). + */ + def isDone: Boolean } /** * Details about a force-close where we published our commitment. * - * @param claimMainDelayedOutputTx tx claiming our main output (if we have one). - * @param htlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be - * None only for incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimHtlcDelayedTxs 3rd-stage txs (spending the output of HTLC txs). - * @param claimAnchorTxs txs spending our anchor output to bump the feerate of the commitment tx (if applicable). + * @param htlcDelayedOutputs when an HTLC transaction confirms, we must claim its output using a 3rd-stage delayed + * transaction. An entry containing the corresponding output must be added to this set to + * ensure that we don't forget the channel too soon, and correctly wait until we've spent it. */ -case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[ClaimLocalDelayedOutputTx], htlcTxs: Map[OutPoint, Option[HtlcTx]], claimHtlcDelayedTxs: List[HtlcDelayedTx], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - // We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates. - val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption - - /** - * A local commit is considered done when: - * - all commitment tx outputs that we can spend have been spent and confirmed (even if the spending tx was not ours) - * - all 3rd stage txs (txs spending htlc txs) have been confirmed - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainDelayedOutputTx.forall(tx => irrevocablySpent.contains(tx.input.outPoint)) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (htlcTxs.keySet -- irrevocablySpent.keys).isEmpty - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedTxs.map(_.input.outPoint) - // only the txs which parents are already confirmed may get confirmed (note that this eliminates outputs that have been double-spent by a competing tx) - .filter(input => confirmedTxs.contains(input.txid)) - // has the tx already been confirmed? - .filterNot(input => irrevocablySpent.contains(input)) - isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent && unconfirmedHtlcDelayedTxs.isEmpty +case class LocalCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], anchorOutput_opt: Option[OutPoint], incomingHtlcs: Map[OutPoint, Long], outgoingHtlcs: Map[OutPoint, Long], htlcDelayedOutputs: Set[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = incomingHtlcs.keySet ++ outgoingHtlcs.keySet + override val isDone: Boolean = { + val mainOutputSpent = localOutput_opt.forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + val allHtlcTxsSpent = (htlcDelayedOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputSpent && allHtlcsSpent && allHtlcTxsSpent } } /** - * Details about a force-close where they published their commitment. - * - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param claimHtlcTxs txs claiming HTLCs. There will be one entry for each pending HTLC. The value will be None - * only for incoming HTLCs for which we don't have the preimage (we can't claim them yet). - * @param claimAnchorTxs txs spending our anchor output to bump the feerate of the commitment tx (if applicable). + * Details about a force-close where they published their commitment (current or next). */ -case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], claimHtlcTxs: Map[OutPoint, Option[ClaimHtlcTx]], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - // We previously used a list of anchor transactions because we included the confirmation target, but that's obsolete and should be overridden on updates. - val claimAnchorTx_opt: Option[ClaimAnchorOutputTx] = claimAnchorTxs.headOption - - /** - * A remote commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // is our main output confirmed (if we have one)? - val isMainOutputConfirmed = claimMainOutputTx.forall(tx => irrevocablySpent.contains(tx.input.outPoint)) - // are all htlc outputs from the commitment tx spent (we need to check them all because we may receive preimages later)? - val allHtlcsSpent = (claimHtlcTxs.keySet -- irrevocablySpent.keys).isEmpty - isCommitTxConfirmed && isMainOutputConfirmed && allHtlcsSpent +case class RemoteCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], anchorOutput_opt: Option[OutPoint], incomingHtlcs: Map[OutPoint, Long], outgoingHtlcs: Map[OutPoint, Long], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = incomingHtlcs.keySet ++ outgoingHtlcs.keySet + override val isDone: Boolean = { + val mainOutputSpent = localOutput_opt.forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputSpent && allHtlcsSpent } } /** * Details about a force-close where they published one of their revoked commitments. + * In that case, we're able to spend every output of the commitment transaction (if economical). * - * @param claimMainOutputTx tx claiming our main output (if we have one). - * @param mainPenaltyTx penalty tx claiming their main output (if they have one). - * @param htlcPenaltyTxs penalty txs claiming every HTLC output. - * @param claimHtlcDelayedPenaltyTxs penalty txs claiming the output of their HTLC txs (if they managed to get them confirmed before our htlcPenaltyTxs). + * @param htlcDelayedOutputs if our peer manages to get some of their HTLC transactions confirmed before our penalty + * transactions, we must spend the output(s) of their HTLC transactions. */ -case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], mainPenaltyTx: Option[MainPenaltyTx], htlcPenaltyTxs: List[HtlcPenaltyTx], claimHtlcDelayedPenaltyTxs: List[ClaimHtlcDelayedOutputPenaltyTx], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { - /** - * A revoked commit is considered done when all commitment tx outputs that we can spend have been spent and confirmed - * (even if the spending tx was not ours). - */ - def isDone: Boolean = { - val confirmedTxs = irrevocablySpent.values.map(_.txid).toSet - // is the commitment tx confirmed (we need to check this because we may not have any outputs)? - val isCommitTxConfirmed = confirmedTxs.contains(commitTx.txid) - // are there remaining spendable outputs from the commitment tx? - val unspentCommitTxOutputs = { - val commitOutputsSpendableByUs = (claimMainOutputTx.toSeq ++ mainPenaltyTx.toSeq ++ htlcPenaltyTxs).map(_.input.outPoint) - commitOutputsSpendableByUs.toSet -- irrevocablySpent.keys - } - // are all outputs from htlc txs spent? - val unconfirmedHtlcDelayedTxs = claimHtlcDelayedPenaltyTxs.map(_.input.outPoint) - // only the txs which parents are already confirmed may get confirmed (note that this eliminates outputs that have been double-spent by a competing tx) - .filter(input => confirmedTxs.contains(input.txid)) - // if one of the tx inputs has been spent, the tx has already been confirmed or a competing tx has been confirmed - .filterNot(input => irrevocablySpent.contains(input)) - isCommitTxConfirmed && unspentCommitTxOutputs.isEmpty && unconfirmedHtlcDelayedTxs.isEmpty +case class RevokedCommitPublished(commitTx: Transaction, localOutput_opt: Option[OutPoint], remoteOutput_opt: Option[OutPoint], htlcOutputs: Set[OutPoint], htlcDelayedOutputs: Set[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + // We don't use the anchor output, we can CPFP the commitment with any other output. + override val anchorOutput_opt: Option[OutPoint] = None + override val isDone: Boolean = { + val mainOutputsSpent = (localOutput_opt.toSeq ++ remoteOutput_opt.toSeq).forall(o => irrevocablySpent.contains(o)) + val allHtlcsSpent = (htlcOutputs -- irrevocablySpent.keySet).isEmpty + val allHtlcTxsSpent = (htlcDelayedOutputs -- irrevocablySpent.keySet).isEmpty + isConfirmed && mainOutputsSpent && allHtlcsSpent && allHtlcTxsSpent } } 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 857579d2ae..fe5f43efe8 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 @@ -890,22 +890,25 @@ object Helpers { val mainDelayedTx_opt = withTxGenerationLog("local-main-delayed") { ClaimLocalDelayedOutputTx.createSignedTx(commitmentKeys, commitTx, commitment.localParams.dustLimit, commitment.remoteParams.toSelfDelay, finalScriptPubKey, feerateDelayed, commitment.params.commitmentFormat) } - val htlcTxs = claimHtlcOutputs(commitmentKeys, commitment) - val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs - val anchorTx_opt = if (spendAnchors) { + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitmentKeys, commitment) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitmentKeys, commitment) + val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitmentKeys, commitment.params.commitmentFormat).toOption + val spendAnchor = incomingHtlcs.nonEmpty || outgoingHtlcs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val anchorTx_opt = if (spendAnchor) { claimAnchor(fundingKey, commitmentKeys, commitTx, commitment.params.commitmentFormat) } else { None } val lcp = LocalCommitPublished( commitTx = commitTx, - claimMainDelayedOutputTx = mainDelayedTx_opt, - htlcTxs = htlcTxs, - claimHtlcDelayedTxs = Nil, // we will claim these once the htlc txs are confirmed - claimAnchorTxs = anchorTx_opt.toList, + localOutput_opt = mainDelayedTx_opt.map(_.input.outPoint), + anchorOutput_opt = anchorOutput_opt.map(_.outPoint), + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, + htlcDelayedOutputs = Set.empty, // we will add these once the htlc txs are confirmed irrevocablySpent = Map.empty ) - val txs = SecondStageTransactions(mainDelayedTx_opt, anchorTx_opt, htlcTxs.values.flatten.toSeq) + val txs = SecondStageTransactions(mainDelayedTx_opt, anchorTx_opt, htlcSuccessTxs ++ htlcTimeoutTxs) (lcp, txs) } @@ -916,10 +919,10 @@ object Helpers { } /** - * Claim the outputs of a local commit tx corresponding to HTLCs. If we don't have the preimage for a received - * * HTLC, we still include an entry in the map because we may receive that preimage later. + * Claim the outputs of a local commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. */ - private def claimHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, Option[HtlcTx]] = { + private def claimIncomingHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[HtlcSuccessTx]) = { // We collect all the preimages available. val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage @@ -932,15 +935,16 @@ object Helpers { // We collect incoming HTLCs that we haven't relayed: they may have been signed by our peer, but we haven't // received their revocation yet. val nonRelayedIncomingHtlcs: Set[Long] = commitment.changes.remoteChanges.all.collect { case add: UpdateAddHtlc => add.id }.toSet - commitment.localCommit.htlcTxsAndRemoteSigs.collect { + val incomingHtlcs = commitment.localCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, remoteSig) => if (preimages.contains(txInfo.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. val preimage = preimages(txInfo.paymentHash) - Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { + val htlcTx_opt = withTxGenerationLog("htlc-success") { val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map.empty) Right(txInfo.addSigs(commitKeys, localSig, remoteSig, preimage, commitment.params.commitmentFormat)) - }) + } + Some(txInfo.input.outPoint, txInfo.htlcId, htlcTx_opt) } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. @@ -952,39 +956,50 @@ object Helpers { // For all other incoming htlcs, we may receive the preimage later from downstream. We thus want to track // the corresponding outputs to ensure we don't move to the CLOSED state until they've been spent, either // by us if we receive the preimage, or by our peer after the timeout. - Some(txInfo.input.outPoint -> None) + Some(txInfo.input.outPoint, txInfo.htlcId, None) } + }.flatten + val htlcOutputs = incomingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = incomingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) + } + + /** + * Claim the outputs of a local commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private def claimOutgoingHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[HtlcTimeoutTx]) = { + val outgoingHtlcs = commitment.localCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. - Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { + val htlcTx_opt = withTxGenerationLog("htlc-timeout") { val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map.empty) Right(txInfo.addSigs(commitKeys, localSig, remoteSig, commitment.params.commitmentFormat)) - }) - }.flatten.toMap + } + (txInfo.input.outPoint, txInfo.htlcId, htlcTx_opt) + } + val htlcOutputs = outgoingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = outgoingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) } /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ - def claimHtlcsWithPreimage(commitKeys: LocalCommitmentKeys, localCommitPublished: LocalCommitPublished, commitment: FullCommitment, preimage: ByteVector32)(implicit log: LoggingAdapter): (LocalCommitPublished, Seq[TxPublisher.PublishTx]) = { - val (htlcTxs, toPublish) = commitment.localCommit.htlcTxsAndRemoteSigs.collect { + def claimHtlcsWithPreimage(commitKeys: LocalCommitmentKeys, localCommitPublished: LocalCommitPublished, commitment: FullCommitment, preimage: ByteVector32)(implicit log: LoggingAdapter): Seq[TxPublisher.PublishTx] = { + commitment.localCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, remoteSig) if txInfo.paymentHash == Crypto.sha256(preimage) => withTxGenerationLog("htlc-success") { val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map.empty) Right(txInfo.addSigs(commitKeys, localSig, remoteSig, preimage, commitment.params.commitmentFormat)) }.map(signedTx => { - val toPublish = commitment.params.commitmentFormat match { + commitment.params.commitmentFormat match { case DefaultCommitmentFormat => TxPublisher.PublishFinalTx(signedTx, signedTx.fee, Some(localCommitPublished.commitTx.txid)) case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => val confirmationTarget = ConfirmationTarget.Absolute(txInfo.htlcExpiry.blockHeight) TxPublisher.PublishReplaceableTx(ReplaceableHtlcSuccess(signedTx, commitKeys, preimage, remoteSig, localCommitPublished.commitTx, commitment), confirmationTarget) } - (signedTx, toPublish) }) - }.flatten.unzip - val additionalHtlcTxs = htlcTxs.map(tx => tx.input.outPoint -> Some(tx)).toMap[OutPoint, Option[HtlcTx]] - val localCommitPublished1 = localCommitPublished.copy(htlcTxs = localCommitPublished.htlcTxs ++ additionalHtlcTxs) - (localCommitPublished1, toPublish) + }.flatten } /** @@ -1001,7 +1016,7 @@ object Helpers { val outpoints = commitment.localCommit.htlcTxsAndRemoteSigs.collect { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == htlcId && !preimages.contains(txInfo.paymentHash) => txInfo.input.outPoint }.toSet - localCommitPublished.copy(htlcTxs = localCommitPublished.htlcTxs -- outpoints) + localCommitPublished.copy(incomingHtlcs = localCommitPublished.incomingHtlcs -- outpoints) } /** @@ -1011,7 +1026,7 @@ object Helpers { * 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, ThirdStageTransactions) = { - if (tx.txIn.exists(txIn => localCommitPublished.htlcTxs.contains(txIn.outPoint))) { + if (tx.txIn.exists(txIn => localCommitPublished.htlcOutputs.contains(txIn.outPoint))) { 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 @@ -1019,13 +1034,22 @@ object Helpers { val htlcDelayedTx_opt = withTxGenerationLog("htlc-delayed") { 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) + val localCommitPublished1 = localCommitPublished.copy(htlcDelayedOutputs = localCommitPublished.htlcDelayedOutputs ++ htlcDelayedTx_opt.map(_.input.outPoint).toSeq) (localCommitPublished1, ThirdStageTransactions(htlcDelayedTx_opt.toSeq)) } else { (localCommitPublished, ThirdStageTransactions(Nil)) } } + /** + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. + */ + def claimHtlcDelayedOutputs(localCommitPublished: LocalCommitPublished, channelKeys: ChannelKeys, commitment: FullCommitment, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = { + val confirmedHtlcTxs = localCommitPublished.htlcOutputs.flatMap(htlcOutput => localCommitPublished.irrevocablySpent.get(htlcOutput)) + val htlcDelayedTxs = confirmedHtlcTxs.flatMap(tx => claimHtlcDelayedOutput(localCommitPublished, channelKeys, commitment, tx, feerates, onChainFeeConf, finalScriptPubKey)._2.htlcDelayedTxs) + ThirdStageTransactions(htlcDelayedTxs.toSeq) + } + } object RemoteClose { @@ -1038,22 +1062,26 @@ object Helpers { 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) + val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) val mainTx_opt = claimMainOutput(commitment.params, commitKeys, commitTx, feerates, onChainFeeConf, finalScriptPubKey) - val htlcTxs = claimHtlcOutputs(channelKeys, commitKeys, commitment, remoteCommit, finalScriptPubKey) - val spendAnchors = htlcTxs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs - val anchorTx_opt = if (spendAnchors) { + val (incomingHtlcs, htlcSuccessTxs) = claimIncomingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey) + val (outgoingHtlcs, htlcTimeoutTxs) = claimOutgoingHtlcOutputs(commitKeys, commitTx, outputs, commitment, remoteCommit, finalScriptPubKey) + val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitKeys, commitment.params.commitmentFormat).toOption + val spendAnchor = incomingHtlcs.nonEmpty || outgoingHtlcs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs + val anchorTx_opt = if (spendAnchor) { claimAnchor(fundingKey, commitKeys, commitTx, commitment.params.commitmentFormat) } else { None } val rcp = RemoteCommitPublished( commitTx = commitTx, - claimMainOutputTx = mainTx_opt, - claimHtlcTxs = htlcTxs, - claimAnchorTxs = anchorTx_opt.toList, + localOutput_opt = mainTx_opt.map(_.input.outPoint), + anchorOutput_opt = anchorOutput_opt.map(_.outPoint), + incomingHtlcs = incomingHtlcs, + outgoingHtlcs = outgoingHtlcs, irrevocablySpent = Map.empty ) - val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcTxs.values.flatten.toSeq) + val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcSuccessTxs ++ htlcTimeoutTxs) (rcp, txs) } @@ -1083,16 +1111,12 @@ object Helpers { } /** - * Claim the outputs of a remote commit tx corresponding to HTLCs. If we don't have the preimage for a received - * * HTLC, we still include an entry in the map because we may receive that preimage later. + * Claim the outputs of a remote commit tx corresponding to incoming HTLCs. If we don't have the preimage for an + * incoming HTLC, we still include an entry in the map because we may receive that preimage later. */ - private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, Option[ClaimHtlcTx]] = { - val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) - val remoteCommitTx = makeCommitTx(commitment.commitInput, remoteCommit.index, commitment.params.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !commitment.params.localParams.isChannelOpener, outputs) - require(remoteCommitTx.tx.txid == remoteCommit.txid, "txid mismatch, cannot recompute the current remote commit tx") + private def claimIncomingHtlcOutputs(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, outputs: Seq[CommitmentOutput], commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[ClaimHtlcSuccessTx]) = { // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. val feerate = FeeratePerKw(FeeratePerByte(1 sat)) - // We collect all the preimages available. val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage @@ -1105,16 +1129,15 @@ object Helpers { // We collect incoming HTLCs that we haven't relayed: they may have been signed by our peer, but they haven't // sent their revocation yet. val nonRelayedIncomingHtlcs: Set[Long] = commitment.changes.remoteChanges.all.collect { case add: UpdateAddHtlc => add.id }.toSet - // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. - remoteCommit.spec.htlcs.collect { + val incomingHtlcs = remoteCommit.spec.htlcs.collect { case OutgoingHtlc(add: UpdateAddHtlc) => if (preimages.contains(add.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. val preimage = preimages(add.paymentHash) withTxGenerationLog("claim-htlc-success") { - ClaimHtlcSuccessTx.createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.params.commitmentFormat) - }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> Some(claimHtlcTx)) + ClaimHtlcSuccessTx.createSignedTx(commitKeys, commitTx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.params.commitmentFormat) + }.map(claimHtlcTx => (claimHtlcTx.input.outPoint, add.id, Some(claimHtlcTx))) } else if (failedIncomingHtlcs.contains(add.id)) { // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. // We don't track those outputs because we want to move to the CLOSED state even if our peer never claims them. @@ -1126,25 +1149,42 @@ object Helpers { // For all other incoming htlcs, we may receive the preimage later from downstream. We thus want to track // the corresponding outputs to ensure we don't move to the CLOSED state until they've been spent, either // by us if we receive the preimage, or by our peer after the timeout. - ClaimHtlcSuccessTx.findInput(remoteCommitTx.tx, outputs, add).map(input => input.outPoint -> None) + ClaimHtlcSuccessTx.findInput(commitTx, outputs, add).map(input => (input.outPoint, add.id, None)) } + }.flatten.toSeq + val htlcOutputs = incomingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = incomingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) + } + + /** + * Claim the outputs of a remote commit tx corresponding to outgoing HTLCs, after their timeout. + */ + private def claimOutgoingHtlcOutputs(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, outputs: Seq[CommitmentOutput], commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (Map[OutPoint, Long], Seq[ClaimHtlcTimeoutTx]) = { + // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. + val feerate = FeeratePerKw(FeeratePerByte(1 sat)) + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val outgoingHtlcs = remoteCommit.spec.htlcs.collect { case IncomingHtlc(add: UpdateAddHtlc) => // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. withTxGenerationLog("claim-htlc-timeout") { - ClaimHtlcTimeoutTx.createSignedTx(commitKeys, remoteCommitTx.tx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, feerate, commitment.params.commitmentFormat) - }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> Some(claimHtlcTx)) - }.flatten.toMap + ClaimHtlcTimeoutTx.createSignedTx(commitKeys, commitTx, commitment.localParams.dustLimit, outputs, finalScriptPubKey, add, feerate, commitment.params.commitmentFormat) + }.map(claimHtlcTx => (claimHtlcTx.input.outPoint, add.id, Some(claimHtlcTx))) + }.flatten.toSeq + val htlcOutputs = outgoingHtlcs.collect { case (outpoint, htlcId, _) => outpoint -> htlcId }.toMap + val htlcTxs = outgoingHtlcs.collect { case (_, _, htlcTx_opt) => htlcTx_opt }.flatten + (htlcOutputs, htlcTxs) } /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ - def claimHtlcsWithPreimage(channelKeys: ChannelKeys, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit, preimage: ByteVector32, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RemoteCommitPublished, Seq[TxPublisher.PublishReplaceableTx]) = { + def claimHtlcsWithPreimage(channelKeys: ChannelKeys, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit, preimage: ByteVector32, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[TxPublisher.PublishReplaceableTx] = { val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. val feerate = FeeratePerKw(FeeratePerByte(1 sat)) - val toPublish = remoteCommit.spec.htlcs.collect { + remoteCommit.spec.htlcs.collect { // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. case OutgoingHtlc(add: UpdateAddHtlc) if add.paymentHash == Crypto.sha256(preimage) => withTxGenerationLog("claim-htlc-success") { @@ -1154,9 +1194,6 @@ object Helpers { TxPublisher.PublishReplaceableTx(ReplaceableClaimHtlcSuccess(signedTx, commitKeys, preimage, remoteCommitPublished.commitTx, commitment), confirmationTarget) } }.flatten.toSeq - val additionalHtlcTxs = toPublish.map(p => p.input -> Some(p.tx.txInfo.asInstanceOf[ClaimHtlcSuccessTx])).toMap[OutPoint, Option[ClaimHtlcTx]] - val remoteCommitPublished1 = remoteCommitPublished.copy(claimHtlcTxs = remoteCommitPublished.claimHtlcTxs ++ additionalHtlcTxs) - (remoteCommitPublished1, toPublish) } /** @@ -1165,19 +1202,19 @@ object Helpers { * We stop tracking the corresponding output because we want to move to the CLOSED state even if our peer never * claims it (which may happen if the HTLC amount is low and on-chain fees are high). */ - def ignoreFailedIncomingHtlc(channelKeys: ChannelKeys, htlcId: Long, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit): RemoteCommitPublished = { + def ignoreFailedIncomingHtlc(htlcId: Long, remoteCommitPublished: RemoteCommitPublished, commitment: FullCommitment, remoteCommit: RemoteCommit): RemoteCommitPublished = { // If we have the preimage (e.g. for partially fulfilled multi-part payments), we keep the HTLC-success tx. val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage }.toMap - val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) - val outputs = makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) - val outpoints = remoteCommit.spec.htlcs.collect { - // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. - case OutgoingHtlc(add: UpdateAddHtlc) if add.id == htlcId && !preimages.contains(add.paymentHash) => - ClaimHtlcSuccessTx.findInput(remoteCommitPublished.commitTx, outputs, add).map(_.outPoint) - }.flatten - remoteCommitPublished.copy(claimHtlcTxs = remoteCommitPublished.claimHtlcTxs -- outpoints) + // Remember we are looking at the remote commitment so IN for them is really OUT for us and vice versa. + val htlcsWithPreimage = remoteCommit.spec.htlcs.collect { + case OutgoingHtlc(add: UpdateAddHtlc) if preimages.contains(add.paymentHash) => add.id + } + val outpoints = remoteCommitPublished.incomingHtlcs.collect { + case (outpoint, id) if id == htlcId && !htlcsWithPreimage.contains(id) => outpoint + }.toSet + remoteCommitPublished.copy(incomingHtlcs = remoteCommitPublished.incomingHtlcs -- outpoints) } } @@ -1257,10 +1294,10 @@ object Helpers { val rvk = RevokedCommitPublished( commitTx = commitTx, - claimMainOutputTx = mainTx_opt, - mainPenaltyTx = mainPenaltyTx_opt, - htlcPenaltyTxs = htlcPenaltyTxs.toList, - claimHtlcDelayedPenaltyTxs = Nil, // we will generate and spend those if they publish their HtlcSuccessTx or HtlcTimeoutTx + localOutput_opt = mainTx_opt.map(_.input.outPoint), + remoteOutput_opt = mainPenaltyTx_opt.map(_.input.outPoint), + htlcOutputs = htlcPenaltyTxs.map(_.input.outPoint).toSet, + htlcDelayedOutputs = Set.empty, // we will generate and spend those if their HtlcSuccessTx or HtlcTimeoutTx confirms irrevocablySpent = Map.empty ) val txs = SecondStageTransactions(mainTx_opt, mainPenaltyTx_opt, htlcPenaltyTxs) @@ -1285,26 +1322,14 @@ object Helpers { // 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 // that spend it, which will automatically be skipped if this was instead one of our HTLC-penalty transactions. - val htlcOutputs = revokedCommitPublished.htlcPenaltyTxs.map(_.input.outPoint).toSet - val spendsHtlcOutput = htlcTx.txIn.exists(txIn => htlcOutputs.contains(txIn.outPoint)) + val spendsHtlcOutput = htlcTx.txIn.exists(txIn => revokedCommitPublished.htlcOutputs.contains(txIn.outPoint)) if (spendsHtlcOutput) { getRemotePerCommitmentSecret(params, channelKeys, remotePerCommitmentSecrets, revokedCommitPublished.commitTx).map { case (_, remotePerCommitmentSecret) => val commitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentSecret.publicKey) val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) - // We need to use a high fee when spending HTLC txs because after a delay they can also be spent by the counterparty. - val feeratePenalty = feerates.fastest - val penaltyTxs = ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(commitmentKeys, revocationKey, htlcTx, params.localParams.dustLimit, params.localParams.toSelfDelay, finalScriptPubKey, feeratePenalty, params.commitmentFormat).flatMap(claimHtlcDelayedOutputPenaltyTx => { - withTxGenerationLog("htlc-delayed-penalty") { - claimHtlcDelayedOutputPenaltyTx.map(signedTx => { - // We need to make sure that the tx is indeed valid. - Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - log.warning("txId={} is a 2nd level htlc tx spending revoked commit txId={}: publishing htlc-penalty txId={}", htlcTx.txid, revokedCommitPublished.commitTx.txid, signedTx.tx.txid) - signedTx - }) - } - }) - val revokedCommitPublished1 = revokedCommitPublished.copy(claimHtlcDelayedPenaltyTxs = revokedCommitPublished.claimHtlcDelayedPenaltyTxs ++ penaltyTxs) + val penaltyTxs = claimHtlcTxOutputs(params, commitmentKeys, revocationKey, htlcTx, feerates, finalScriptPubKey) + val revokedCommitPublished1 = revokedCommitPublished.copy(htlcDelayedOutputs = revokedCommitPublished.htlcDelayedOutputs ++ penaltyTxs.map(_.input.outPoint)) val txs = ThirdStageTransactions(penaltyTxs) (revokedCommitPublished1, txs) }.getOrElse((revokedCommitPublished, ThirdStageTransactions(Nil))) @@ -1313,6 +1338,32 @@ object Helpers { } } + private def claimHtlcTxOutputs(params: ChannelParams, commitmentKeys: RemoteCommitmentKeys, revocationKey: PrivateKey, htlcTx: Transaction, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Seq[ClaimHtlcDelayedOutputPenaltyTx] = { + // We need to use a high fee when spending HTLC txs because after a delay they can also be spent by the counterparty. + val feeratePenalty = feerates.fastest + ClaimHtlcDelayedOutputPenaltyTx.createSignedTxs(commitmentKeys, revocationKey, htlcTx, params.localParams.dustLimit, params.localParams.toSelfDelay, finalScriptPubKey, feeratePenalty, params.commitmentFormat).flatMap(penaltyTx => { + withTxGenerationLog("htlc-delayed-penalty") { + penaltyTx.map(signedTx => { + // We need to make sure that the tx is indeed valid. + Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + log.warning("txId={} is a 2nd level htlc tx spending revoked commit txId={}: publishing htlc-penalty txId={}", htlcTx.txid, signedTx.input.outPoint.txid, signedTx.tx.txid) + signedTx + }) + } + }) + } + + /** + * Claim the outputs of all 2nd-stage HTLC transactions that have been confirmed. + */ + def claimHtlcTxsOutputs(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecret: PrivateKey, revokedCommitPublished: RevokedCommitPublished, feerates: FeeratesPerKw, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): ThirdStageTransactions = { + val commitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentSecret.publicKey) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) + val confirmedHtlcTxs = revokedCommitPublished.htlcOutputs.flatMap(htlcOutput => revokedCommitPublished.irrevocablySpent.get(htlcOutput)) + val penaltyTxs = confirmedHtlcTxs.flatMap(htlcTx => claimHtlcTxOutputs(params, commitmentKeys, revocationKey, htlcTx, feerates, finalScriptPubKey)) + ThirdStageTransactions(penaltyTxs.toSeq) + } + } /** @@ -1473,7 +1524,7 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateLocalCommitPublished(localCommitPublished: LocalCommitPublished, tx: Transaction): LocalCommitPublished = { + def updateIrrevocablySpent(localCommitPublished: LocalCommitPublished, tx: Transaction): LocalCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { @@ -1483,7 +1534,7 @@ object Helpers { val spendsTheCommitTx = localCommitPublished.commitTx.txid == outPoint.txid // is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = localCommitPublished.claimHtlcDelayedTxs.map(_.input.outPoint).contains(outPoint) + val is3rdStageDelayedTx = localCommitPublished.htlcDelayedOutputs.contains(outPoint) isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx }) // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint @@ -1500,7 +1551,7 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateRemoteCommitPublished(remoteCommitPublished: RemoteCommitPublished, tx: Transaction): RemoteCommitPublished = { + def updateIrrevocablySpent(remoteCommitPublished: RemoteCommitPublished, tx: Transaction): RemoteCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { @@ -1524,7 +1575,7 @@ object Helpers { * * @param tx a transaction that has been irrevocably confirmed */ - def updateRevokedCommitPublished(revokedCommitPublished: RevokedCommitPublished, tx: Transaction): RevokedCommitPublished = { + def updateIrrevocablySpent(revokedCommitPublished: RevokedCommitPublished, tx: Transaction): RevokedCommitPublished = { // even if our txs only have one input, maybe our counterparty uses a different scheme so we need to iterate // over all of them to check if they are relevant val relevantOutpoints = tx.txIn.map(_.outPoint).filter(outPoint => { @@ -1534,7 +1585,7 @@ object Helpers { val spendsTheCommitTx = revokedCommitPublished.commitTx.txid == outPoint.txid // is the tx one of our 3rd stage delayed txs? (a 3rd stage tx is a tx spending the output of an htlc tx, which // is itself spending the output of the commitment tx) - val is3rdStageDelayedTx = revokedCommitPublished.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).contains(outPoint) + val is3rdStageDelayedTx = revokedCommitPublished.htlcDelayedOutputs.contains(outPoint) isCommitTx || spendsTheCommitTx || is3rdStageDelayedTx }) // then we add the relevant outpoints to the map keeping track of which txid spends which outpoint 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 5a82f46aef..dea503e149 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 @@ -321,11 +321,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall watchFundingConfirmed(fundingTx.tx.txid, Some(nodeParams.channelConf.minDepth), herdDelay_opt) case _: LocalFundingStatus.ConfirmedFundingTx => data match { - case closing: DATA_CLOSING if Closing.nothingAtStake(closing) || Closing.isClosingTypeAlreadyKnown(closing).isDefined => - // no need to do anything - () + case closing: DATA_CLOSING if Closing.nothingAtStake(closing) => () + // No need to watch the funding tx, it has already been spent and the spending tx has already reached mindepth. + case closing: DATA_CLOSING if Closing.isClosingTypeAlreadyKnown(closing).isDefined => () + // In all other cases we need to be ready for any type of closing. case closing: DATA_CLOSING => - // in all other cases we need to be ready for any type of closing watchFundingSpent(commitment, closing.spendingTxs.map(_.txid).toSet, herdDelay_opt) case negotiating: DATA_NEGOTIATING => val closingTxs = negotiating.closingTxProposed.flatten.map(_.unsignedTx.tx.txid).toSet @@ -350,39 +350,63 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall goto(CLOSED) using closing case closing: DATA_CLOSING => val localPaysClosingFees = closing.commitments.params.localParams.paysClosingFees - // we don't put back the WatchSpent if the commitment tx has already been published and the spending tx already reached mindepth val closingType_opt = Closing.isClosingTypeAlreadyKnown(closing) log.info(s"channel is closing (closingType=${closingType_opt.map(c => EventType.Closed(c).label).getOrElse("UnknownYet")})") - // if the closing type is known: - // - there is no need to watch the funding tx because it has already been spent and the spending tx has already reached mindepth - // - there is no need to attempt to publish transactions for other type of closes - // - there is a single commitment, the others have all been invalidated + // If the closing type is known: + // - there is no need to attempt to publish transactions for other type of closes + // - there may be 3rd-stage transactions to publish + // - there is a single commitment, the others have all been invalidated + val commitment = closing.commitments.latest closingType_opt match { case Some(c: Closing.MutualClose) => doPublish(c.tx, localPaysClosingFees) case Some(c: Closing.LocalClose) => - 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) + val (_, secondStageTransactions) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, c.localCommitPublished.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(c.localCommitPublished, secondStageTransactions, commitment) + val thirdStageTransactions = Closing.LocalClose.claimHtlcDelayedOutputs(c.localCommitPublished, channelKeys, commitment, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) doPublish(c.localCommitPublished, thirdStageTransactions) case Some(c: Closing.RemoteClose) => - 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) + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, c.remoteCommit, c.remoteCommitPublished.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(c.remoteCommitPublished, secondStageTransactions, commitment, closing.finalScriptPubKey) case Some(c: Closing.RecoveryClose) => - 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) + // We cannot do anything in that case: we've already published our recovery transaction before restarting, + // and must wait for it to confirm. + doPublish(c.remoteCommitPublished, Closing.RemoteClose.SecondStageTransactions(None, None, Nil), commitment, closing.finalScriptPubKey) case Some(c: Closing.RevokedClose) => - 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) + Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.params, channelKeys, closing.commitments.remotePerCommitmentSecrets, c.revokedCommitPublished.commitTx).foreach { + case (commitmentNumber, remotePerCommitmentSecret) => + val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.params, channelKeys, c.revokedCommitPublished.commitTx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(c.revokedCommitPublished, secondStageTransactions) + val thirdStageTransactions = Closing.RevokedClose.claimHtlcTxsOutputs(closing.commitments.params, channelKeys, remotePerCommitmentSecret, c.revokedCommitPublished, nodeParams.currentBitcoinCoreFeerates, closing.finalScriptPubKey) + doPublish(c.revokedCommitPublished, thirdStageTransactions) + } case None => + // The closing type isn't known yet: + // - we publish transactions for all types of closes that we detected + // - there may be other commitments, but we'll adapt if we receive WatchAlternativeCommitTxConfirmedTriggered + // - there cannot be 3rd-stage transactions yet, no need to re-compute them closing.mutualClosePublished.foreach(mcp => doPublish(mcp, localPaysClosingFees)) - 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)) + closing.localCommitPublished.foreach(lcp => { + val (_, secondStageTransactions) = Closing.LocalClose.claimCommitTxOutputs(channelKeys, commitment, lcp.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(lcp, secondStageTransactions, commitment) + }) + closing.remoteCommitPublished.foreach(rcp => { + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, commitment.remoteCommit, rcp.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(rcp, secondStageTransactions, commitment, closing.finalScriptPubKey) + }) + closing.nextRemoteCommitPublished.foreach(rcp => { + val remoteCommit = commitment.nextRemoteCommit_opt.get.commit + val (_, secondStageTransactions) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, rcp.commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(rcp, secondStageTransactions, commitment, closing.finalScriptPubKey) + }) + closing.revokedCommitPublished.foreach(rvk => { + Closing.RevokedClose.getRemotePerCommitmentSecret(closing.commitments.params, channelKeys, closing.commitments.remotePerCommitmentSecrets, rvk.commitTx).foreach { + case (commitmentNumber, remotePerCommitmentSecret) => + val (_, secondStageTransactions) = Closing.RevokedClose.claimCommitTxOutputs(closing.commitments.params, channelKeys, rvk.commitTx, commitmentNumber, remotePerCommitmentSecret, nodeParams.db.channels, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, closing.finalScriptPubKey) + doPublish(rvk, secondStageTransactions) + } + }) + closing.futureRemoteCommitPublished.foreach(rcp => doPublish(rcp, Closing.RemoteClose.SecondStageTransactions(None, None, Nil), commitment, closing.finalScriptPubKey)) } // no need to go OFFLINE, we can directly switch to CLOSING goto(CLOSING) using closing @@ -1870,27 +1894,16 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.info("htlc #{} with payment_hash={} was fulfilled downstream, recalculating htlc-success transactions", c.id, c.r) // We may be able to publish HTLC-success transactions for which we didn't have the preimage. // We are already watching the corresponding outputs: no need to set additional watches. - val lcp1 = d.localCommitPublished.map(lcp => { - val (lcp1, toPublish) = Closing.LocalClose.claimHtlcsWithPreimage(commitment.localKeys(channelKeys), lcp, commitment, c.r) - toPublish.foreach(publishTx => txPublisher ! publishTx) - lcp1 - }) - val rcp1 = d.remoteCommitPublished.map(rcp => { - val (rcp1, toPublish) = Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, rcp, commitment, commitment.remoteCommit, c.r, d.finalScriptPubKey) - toPublish.foreach(publishTx => txPublisher ! publishTx) - rcp1 - }) - val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => { - val (nrcp1, toPublish) = Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, nrcp, commitment, commitment.nextRemoteCommit_opt.get.commit, c.r, d.finalScriptPubKey) - toPublish.foreach(publishTx => txPublisher ! publishTx) - nrcp1 - }) - d.copy(commitments = commitments1, localCommitPublished = lcp1, remoteCommitPublished = rcp1, nextRemoteCommitPublished = nrcp1) + val localTxs = d.localCommitPublished.map(lcp => Closing.LocalClose.claimHtlcsWithPreimage(commitment.localKeys(channelKeys), lcp, commitment, c.r)).getOrElse(Nil) + val remoteTxs = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, rcp, commitment, commitment.remoteCommit, c.r, d.finalScriptPubKey)).getOrElse(Nil) + val nextRemoteTxs = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.claimHtlcsWithPreimage(channelKeys, nrcp, commitment, commitment.nextRemoteCommit_opt.get.commit, c.r, d.finalScriptPubKey)).getOrElse(Nil) + (localTxs ++ remoteTxs ++ nextRemoteTxs).foreach(publishTx => txPublisher ! publishTx) + d.copy(commitments = commitments1) case _: CMD_FAIL_HTLC | _: CMD_FAIL_MALFORMED_HTLC => log.info("htlc #{} was failed downstream, recalculating watched htlc outputs", c.id) val lcp1 = d.localCommitPublished.map(lcp => Closing.LocalClose.ignoreFailedIncomingHtlc(c.id, lcp, commitment)) - val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(channelKeys, c.id, rcp, commitment, commitment.remoteCommit)) - val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(channelKeys, c.id, nrcp, commitment, commitment.nextRemoteCommit_opt.get.commit)) + val rcp1 = d.remoteCommitPublished.map(rcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(c.id, rcp, commitment, commitment.remoteCommit)) + val nrcp1 = d.nextRemoteCommitPublished.map(nrcp => Closing.RemoteClose.ignoreFailedIncomingHtlc(c.id, nrcp, commitment, commitment.nextRemoteCommit_opt.get.commit)) d.copy(commitments = commitments1, localCommitPublished = lcp1, remoteCommitPublished = rcp1, nextRemoteCommitPublished = nrcp1) } handleCommandSuccess(c, d1) storing() @@ -2075,17 +2088,17 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // 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) + Closing.updateIrrevocablySpent(localCommitPublished1, tx) }), - remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), - nextRemoteCommitPublished = d.nextRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), - futureRemoteCommitPublished = d.futureRemoteCommitPublished.map(Closing.updateRemoteCommitPublished(_, tx)), + remoteCommitPublished = d.remoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), + nextRemoteCommitPublished = d.nextRemoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), + futureRemoteCommitPublished = d.futureRemoteCommitPublished.map(Closing.updateIrrevocablySpent(_, tx)), revokedCommitPublished = d.revokedCommitPublished.map(rvk => { // 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) doPublish(rvk1, penaltyTxs) - Closing.updateRevokedCommitPublished(rvk1, tx) + Closing.updateIrrevocablySpent(rvk1, tx) }) ) // if the local commitment tx just got confirmed, let's send an event telling when we will get the main output refund 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 cddc81545d..691742f400 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 @@ -22,16 +22,17 @@ import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchOutputSpent, WatchTxConfirmed} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget +import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, FeeratePerKw} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel.UnhandledExceptionStrategy import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} import fr.acinq.eclair.channel.publish._ import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys} -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ -import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, UpdateFulfillHtlc, Warning} +import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc, Transactions} +import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, UpdateAddHtlc, UpdateFulfillHtlc, Warning} +import scodec.bits.ByteVector import java.sql.SQLException @@ -251,7 +252,7 @@ trait ErrorHandlers extends CommonHandlers { // 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 + val watchSpentQueue = lcp.localOutput_opt ++ lcp.anchorOutput_opt ++ lcp.htlcOutputs.toSeq watchSpentIfNeeded(lcp.commitTx, watchSpentQueue, lcp.irrevocablySpent) } @@ -304,7 +305,7 @@ trait ErrorHandlers extends CommonHandlers { 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, closingTxs, commitments) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitments, finalScriptPubKey) } def handleRemoteSpentNext(commitTx: Transaction, d: ChannelDataWithCommitments) = { @@ -324,11 +325,11 @@ 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, closingTxs, commitment) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, commitment, finalScriptPubKey) } /** 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 = { + def doPublish(rcp: RemoteCommitPublished, txs: Closing.RemoteClose.SecondStageTransactions, commitment: FullCommitment, finalScriptPubKey: ByteVector): Unit = { val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val remoteCommit = commitment.nextRemoteCommit_opt match { case Some(c) if rcp.commitTx.txid == c.commit.txid => c.commit @@ -342,7 +343,7 @@ trait ErrorHandlers extends CommonHandlers { case _ => None } val publishMainTx_opt = txs.mainTx_opt.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishHtlcTxs = redeemableClaimHtlcTxs(rcp, commitKeys, commitment) + val publishHtlcTxs = if (txs.htlcTxs.nonEmpty) redeemableClaimHtlcTxs(rcp.commitTx, remoteCommit, commitKeys, commitment, finalScriptPubKey) else Nil val publishQueue = publishAnchorTx_opt ++ publishMainTx_opt ++ publishHtlcTxs publishIfNeeded(publishQueue, rcp.irrevocablySpent) @@ -355,34 +356,41 @@ trait ErrorHandlers extends CommonHandlers { // 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 + val watchSpentQueue = rcp.localOutput_opt ++ rcp.anchorOutput_opt ++ rcp.htlcOutputs.toSeq watchSpentIfNeeded(rcp.commitTx, watchSpentQueue, rcp.irrevocablySpent) } - private def redeemableClaimHtlcTxs(remoteCommitPublished: RemoteCommitPublished, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment): Iterable[PublishReplaceableTx] = { + private def redeemableClaimHtlcTxs(commitTx: Transaction, remoteCommit: RemoteCommit, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, finalScriptPubKey: ByteVector): Iterable[PublishReplaceableTx] = { + // The feerate will be set by the publisher actor based on the HTLC expiry, we don't care which feerate is used here. + val feerate = FeeratePerKw(FeeratePerByte(1 sat)) + val dustLimit = commitment.localParams.dustLimit + val outputs = Closing.RemoteClose.makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case fulfill: UpdateFulfillHtlc => Crypto.sha256(fulfill.paymentPreimage) -> fulfill.paymentPreimage }.toMap - remoteCommitPublished.claimHtlcTxs.values.flatten.flatMap { claimHtlcTx => - val confirmationTarget = ConfirmationTarget.Absolute(claimHtlcTx.htlcExpiry.blockHeight) - val preimage_opt = preimages.get(claimHtlcTx.paymentHash) - val replaceableTx_opt = (claimHtlcTx, preimage_opt) match { - case (claimHtlcTx: ClaimHtlcSuccessTx, Some(preimage)) => Some(ReplaceableClaimHtlcSuccess(claimHtlcTx, commitKeys, preimage, remoteCommitPublished.commitTx, commitment)) - case (claimHtlcTx: ClaimHtlcTimeoutTx, _) => Some(ReplaceableClaimHtlcTimeout(claimHtlcTx, commitKeys, remoteCommitPublished.commitTx, commitment)) - case _ => None - } - replaceableTx_opt.map(tx => PublishReplaceableTx(tx, confirmationTarget)) - } + remoteCommit.spec.htlcs.collect { + case OutgoingHtlc(add: UpdateAddHtlc) => + val confirmationTarget = ConfirmationTarget.Absolute(add.cltvExpiry.blockHeight) + for { + preimage <- preimages.get(add.paymentHash) + signedTx <- ClaimHtlcSuccessTx.createSignedTx(commitKeys, commitTx, dustLimit, outputs, finalScriptPubKey, add, preimage, feerate, commitment.params.commitmentFormat).toOption + } yield PublishReplaceableTx(ReplaceableClaimHtlcSuccess(signedTx, commitKeys, preimage, commitTx, commitment), confirmationTarget) + case IncomingHtlc(add: UpdateAddHtlc) => + val confirmationTarget = ConfirmationTarget.Absolute(add.cltvExpiry.blockHeight) + for { + signedTx <- ClaimHtlcTimeoutTx.createSignedTx(commitKeys, commitTx, dustLimit, outputs, finalScriptPubKey, add, feerate, commitment.params.commitmentFormat).toOption + } yield PublishReplaceableTx(ReplaceableClaimHtlcTimeout(signedTx, commitKeys, commitTx, commitment), confirmationTarget) + }.flatten } def handleRemoteSpentOther(tx: Transaction, d: ChannelDataWithCommitments) = { val commitment = d.commitments.latest - log.warning(s"funding tx spent in txid=${tx.txid}") + log.warning("funding tx spent by txid={}", tx.txid) val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) Closing.RevokedClose.getRemotePerCommitmentSecret(d.commitments.params, channelKeys, d.commitments.remotePerCommitmentSecrets, tx) match { case Some((commitmentNumber, remotePerCommitmentSecret)) => 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") + log.warning("txid={} was a revoked commitment, publishing the penalty tx", tx.txid) 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) val error = Error(d.channelId, exc.getMessage) @@ -396,20 +404,22 @@ trait ErrorHandlers extends CommonHandlers { 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}") + log.warning("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) + mainTx_opt.foreach(tx => log.warning("publishing our recovery transaction: tx={}", tx.toString)) val remoteCommitPublished = RemoteCommitPublished( commitTx = tx, - claimMainOutputTx = mainTx_opt, - claimHtlcTxs = Map.empty, - claimAnchorTxs = List.empty, + localOutput_opt = mainTx_opt.map(_.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.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, closingTxs, d.commitments.latest) + goto(CLOSING) using nextData storing() calling doPublish(remoteCommitPublished, closingTxs, d.commitments.latest, finalScriptPubKey) 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!!!") @@ -430,7 +440,7 @@ trait ErrorHandlers extends CommonHandlers { } // 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) + val watchSpentQueue = rvk.localOutput_opt ++ rvk.remoteOutput_opt ++ rvk.htlcOutputs.toSeq watchSpentIfNeeded(rvk.commitTx, watchSpentQueue, rvk.irrevocablySpent) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index 103b7628ee..ec5510db2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -781,11 +781,19 @@ object Transactions { } object ClaimAnchorOutputTx { - def redeemInfo(fundingKey: PublicKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = - redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = true, commitmentFormat) + def redeemInfo(fundingKey: PublicKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = true, commitmentFormat) - def redeemInfo(fundingKey: PublicKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = - redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = false, commitmentFormat) + def findInput(commitTx: Transaction, fundingKey: PublicKey, commitKeys: LocalCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = { + val pubKeyScript = redeemInfo(fundingKey, commitKeys, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)) + } + + def redeemInfo(fundingKey: PublicKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): RedeemInfo = redeemInfo(fundingKey, commitKeys.publicKeys, toLocal = false, commitmentFormat) + + def findInput(commitTx: Transaction, fundingKey: PublicKey, commitKeys: RemoteCommitmentKeys, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, InputInfo] = { + val pubKeyScript = redeemInfo(fundingKey, commitKeys, commitmentFormat).pubkeyScript + findPubKeyScriptIndex(commitTx, pubKeyScript).map(outputIndex => InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty)) + } /** * @@ -806,26 +814,22 @@ object Transactions { } } - private def createUnsignedTx(redeemInfo: RedeemInfo, commitTx: Transaction): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { - val pubkeyScript = redeemInfo.pubkeyScript - findPubKeyScriptIndex(commitTx, pubkeyScript).map { outputIndex => - val input = InputInfo(OutPoint(commitTx, outputIndex), commitTx.txOut(outputIndex), ByteVector.empty) - val unsignedTx = Transaction( - version = 2, - txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, - txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs - lockTime = 0 - ) - ClaimAnchorOutputTx(input, unsignedTx) - } + private def createUnsignedTx(input: InputInfo): ClaimAnchorOutputTx = { + val unsignedTx = Transaction( + version = 2, + txIn = TxIn(input.outPoint, ByteVector.empty, 0) :: Nil, + txOut = Nil, // anchor is only used to bump fees, the output will be added later depending on available inputs + lockTime = 0 + ) + ClaimAnchorOutputTx(input, unsignedTx) } def createUnsignedTx(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { - createUnsignedTx(redeemInfo(fundingKey.publicKey, commitKeys, commitmentFormat), commitTx) + findInput(commitTx, fundingKey.publicKey, commitKeys, commitmentFormat).map(input => createUnsignedTx(input)) } def createUnsignedTx(fundingKey: PrivateKey, commitKeys: RemoteCommitmentKeys, commitTx: Transaction, commitmentFormat: CommitmentFormat): Either[TxGenerationSkipped, ClaimAnchorOutputTx] = { - createUnsignedTx(redeemInfo(fundingKey.publicKey, commitKeys, commitmentFormat), commitTx) + findInput(commitTx, fundingKey.publicKey, commitKeys, commitmentFormat).map(input => createUnsignedTx(input)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala index d23bc3c37b..106af762cd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelTypes0.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.CommitSig -import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InitFeature, MilliSatoshi, UInt64, channel} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InitFeature, MilliSatoshi, UInt64, channel} import scodec.bits.{BitVector, ByteVector} private[channel] object ChannelTypes0 { @@ -41,14 +41,6 @@ private[channel] object ChannelTypes0 { // - the `htlcId` in htlc txs is used to detect timed out htlcs and relay them upstream, but it can be safely set to // 0 because the `timedOutHtlcs` in `Helpers.scala` explicitly handle the case where this information is unavailable. - private def getPartialInputInfo(parentTx: Transaction, childTx: Transaction): InputInfo = { - // When using the default commitment format, spending txs have a single input. These txs are fully signed and never - // modified: we don't use the InputInfo in closing business logic, so we don't need to fill everything (this part - // assumes that we only have standard channels, no anchor output channels - which was the case before version2). - val input = childTx.txIn.head.outPoint - InputInfo(input, parentTx.txOut(input.index.toInt), ByteVector.empty) - } - case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[Transaction], htlcSuccessTxs: List[Transaction], htlcTimeoutTxs: List[Transaction], claimHtlcDelayedTxs: List[Transaction], irrevocablySpent: Map[OutPoint, TxId]) { def migrate(): channel.LocalCommitPublished = { val htlcTxs = htlcSuccessTxs ++ htlcTimeoutTxs @@ -56,16 +48,11 @@ private[channel] object ChannelTypes0 { // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but // the channel will put a watch at start-up which will make us fetch the spending transaction. val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainDelayedOutputTxNew = claimMainDelayedOutputTx.map(tx => ClaimLocalDelayedOutputTx(getPartialInputInfo(commitTx, tx), tx, CltvExpiryDelta(0))) - val htlcSuccessTxsNew = htlcSuccessTxs.map(tx => HtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0, CltvExpiry(0))) - val htlcTimeoutTxsNew = htlcTimeoutTxs.map(tx => HtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0, CltvExpiry(0))) - val htlcTxsNew = (htlcSuccessTxsNew ++ htlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap - val claimHtlcDelayedTxsNew = claimHtlcDelayedTxs.map(tx => { - val htlcTx = htlcTxs.find(_.txid == tx.txIn.head.outPoint.txid) - require(htlcTx.nonEmpty, s"3rd-stage htlc tx doesn't spend one of our htlc txs: claim-htlc-tx=$tx, htlc-txs=${htlcTxs.mkString(",")}") - HtlcDelayedTx(getPartialInputInfo(htlcTx.get, tx), tx, CltvExpiryDelta(0)) - }) - channel.LocalCommitPublished(commitTx, claimMainDelayedOutputTxNew, htlcTxsNew, claimHtlcDelayedTxsNew, Nil, irrevocablySpentNew) + val localOutput_opt = claimMainDelayedOutputTx.map(_.txIn.head.outPoint) + val incomingHtlcs = htlcSuccessTxs.map(tx => tx.txIn.head.outPoint -> 0L).toMap + val outgoingHtlcs = htlcTimeoutTxs.map(tx => tx.txIn.head.outPoint -> 0L).toMap + val htlcDelayedOutputs = claimHtlcDelayedTxs.map(_.txIn.head.outPoint).toSet + channel.LocalCommitPublished(commitTx, localOutput_opt, anchorOutput_opt = None, incomingHtlcs = incomingHtlcs, outgoingHtlcs = outgoingHtlcs, htlcDelayedOutputs, irrevocablySpentNew) } } @@ -76,11 +63,10 @@ private[channel] object ChannelTypes0 { // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but // the channel will put a watch at start-up which will make us fetch the spending transaction. val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val claimHtlcSuccessTxsNew = claimHtlcSuccessTxs.map(tx => ClaimHtlcSuccessTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0, CltvExpiry(0))) - val claimHtlcTimeoutTxsNew = claimHtlcTimeoutTxs.map(tx => ClaimHtlcTimeoutTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, 0, CltvExpiry(0))) - val claimHtlcTxsNew = (claimHtlcSuccessTxsNew ++ claimHtlcTimeoutTxsNew).map(tx => tx.input.outPoint -> Some(tx)).toMap - channel.RemoteCommitPublished(commitTx, claimMainOutputTxNew, claimHtlcTxsNew, Nil, irrevocablySpentNew) + val localOutput_opt = claimMainOutputTx.map(_.txIn.head.outPoint) + val incomingHtlcs = claimHtlcSuccessTxs.map(tx => tx.txIn.head.outPoint -> 0L).toMap + val outgoingHtlcs = claimHtlcTimeoutTxs.map(tx => tx.txIn.head.outPoint -> 0L).toMap + channel.RemoteCommitPublished(commitTx, localOutput_opt, anchorOutput_opt = None, incomingHtlcs = incomingHtlcs, outgoingHtlcs = outgoingHtlcs, irrevocablySpentNew) } } @@ -90,14 +76,11 @@ private[channel] object ChannelTypes0 { // NB: irrevocablySpent may contain transactions that belong to our peer: we will drop them in this migration but // the channel will put a watch at start-up which will make us fetch the spending transaction. val irrevocablySpentNew = irrevocablySpent.collect { case (outpoint, txid) if knownTxs.contains(txid) => (outpoint, knownTxs(txid)) } - val claimMainOutputTxNew = claimMainOutputTx.map(tx => ClaimP2WPKHOutputTx(getPartialInputInfo(commitTx, tx), tx)) - val mainPenaltyTxNew = mainPenaltyTx.map(tx => MainPenaltyTx(getPartialInputInfo(commitTx, tx), tx, CltvExpiryDelta(0))) - val htlcPenaltyTxsNew = htlcPenaltyTxs.map(tx => HtlcPenaltyTx(getPartialInputInfo(commitTx, tx), tx, ByteVector32.Zeroes, CltvExpiry(0))) - val claimHtlcDelayedPenaltyTxsNew = claimHtlcDelayedPenaltyTxs.map(tx => { - // We don't have all the `InputInfo` data, but it's ok: we only use the tx that is fully signed. - ClaimHtlcDelayedOutputPenaltyTx(InputInfo(tx.txIn.head.outPoint, TxOut(Satoshi(0), Nil), ByteVector.empty), tx, CltvExpiryDelta(0)) - }) - channel.RevokedCommitPublished(commitTx, claimMainOutputTxNew, mainPenaltyTxNew, htlcPenaltyTxsNew, claimHtlcDelayedPenaltyTxsNew, irrevocablySpentNew) + val localOutput_opt = claimMainOutputTx.map(_.txIn.head.outPoint) + val remoteOutput_opt = mainPenaltyTx.map(_.txIn.head.outPoint) + val htlcOutputs = htlcPenaltyTxs.map(_.txIn.head.outPoint).toSet + val htlcDelayedOutputs = claimHtlcDelayedPenaltyTxs.map(_.txIn.head.outPoint).toSet + channel.RevokedCommitPublished(commitTx, localOutput_opt, remoteOutput_opt, htlcOutputs, htlcDelayedOutputs, irrevocablySpentNew) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index 0b337d8dd6..dbdc7c838e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -251,14 +251,14 @@ private[channel] object ChannelCodecs2 { ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RemoteCommitPublished].decodeOnly.map[RemoteCommitPublished](_.migrate()).decodeOnly val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( ("commitTx" | txCodec) :: @@ -266,7 +266,7 @@ private[channel] object ChannelCodecs2 { ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly val DATA_WAIT_FOR_FUNDING_CONFIRMED_00_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelTypes2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelTypes2.scala new file mode 100644 index 0000000000..ed66e5fdb3 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelTypes2.scala @@ -0,0 +1,74 @@ +/* + * Copyright 2025 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.wire.internal.channel.version2 + +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction} +import fr.acinq.eclair.channel +import fr.acinq.eclair.transactions.Transactions._ + +private[channel] object ChannelTypes2 { + + case class LocalCommitPublished(commitTx: Transaction, claimMainDelayedOutputTx: Option[ClaimLocalDelayedOutputTx], htlcTxs: Map[OutPoint, Option[HtlcTx]], claimHtlcDelayedTxs: List[HtlcDelayedTx], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) { + def migrate(): channel.LocalCommitPublished = channel.LocalCommitPublished( + commitTx = commitTx, + localOutput_opt = claimMainDelayedOutputTx.map(_.input.outPoint), + anchorOutput_opt = claimAnchorTxs.headOption.map(_.input.outPoint), + incomingHtlcs = htlcTxs.collect { + case (outpoint, Some(htlcTx: HtlcSuccessTx)) => outpoint -> htlcTx.htlcId + // This case only happens for a received HTLC for which we don't yet have the preimage. + // We cannot easily find the htlcId, so we just set it to a high value that won't match existing HTLCs. + // This is fine because it is only used to unwatch HTLC outpoints that were failed downstream, which is just + // an optimization to go to CLOSED more quickly. + case (outpoint, None) => outpoint -> 0x00ffffffffffffffL + }, + outgoingHtlcs = htlcTxs.collect { + case (outpoint, Some(htlcTx: HtlcTimeoutTx)) => outpoint -> htlcTx.htlcId + }, + htlcDelayedOutputs = claimHtlcDelayedTxs.map(_.input.outPoint).toSet, + irrevocablySpent = irrevocablySpent + ) + } + + case class RemoteCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], claimHtlcTxs: Map[OutPoint, Option[ClaimHtlcTx]], claimAnchorTxs: List[ClaimAnchorOutputTx], irrevocablySpent: Map[OutPoint, Transaction]) { + def migrate(): channel.RemoteCommitPublished = channel.RemoteCommitPublished( + commitTx = commitTx, + localOutput_opt = claimMainOutputTx.map(_.input.outPoint), + anchorOutput_opt = claimAnchorTxs.headOption.map(_.input.outPoint), + incomingHtlcs = claimHtlcTxs.collect { + case (outpoint, Some(htlcTx: ClaimHtlcSuccessTx)) => outpoint -> htlcTx.htlcId + // Similarly to LocalCommitPublished above, it is fine to ignore this case. + case (outpoint, None) => outpoint -> 0x00ffffffffffffffL + }, + outgoingHtlcs = claimHtlcTxs.collect { + case (outpoint, Some(htlcTx: ClaimHtlcTimeoutTx)) => outpoint -> htlcTx.htlcId + }, + irrevocablySpent = irrevocablySpent + ) + } + + case class RevokedCommitPublished(commitTx: Transaction, claimMainOutputTx: Option[ClaimRemoteCommitMainOutputTx], mainPenaltyTx: Option[MainPenaltyTx], htlcPenaltyTxs: List[HtlcPenaltyTx], claimHtlcDelayedPenaltyTxs: List[ClaimHtlcDelayedOutputPenaltyTx], irrevocablySpent: Map[OutPoint, Transaction]) { + def migrate(): channel.RevokedCommitPublished = channel.RevokedCommitPublished( + commitTx = commitTx, + localOutput_opt = claimMainOutputTx.map(_.input.outPoint), + remoteOutput_opt = mainPenaltyTx.map(_.input.outPoint), + htlcOutputs = htlcPenaltyTxs.map(_.input.outPoint).toSet, + htlcDelayedOutputs = claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet, + irrevocablySpent = irrevocablySpent + ) + } + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index 8552a82575..e40c519e46 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 +import fr.acinq.eclair.wire.internal.channel.version2.ChannelTypes2 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage @@ -321,14 +322,14 @@ private[channel] object ChannelCodecs3 { ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RemoteCommitPublished].decodeOnly.map[RemoteCommitPublished](_.migrate()).decodeOnly val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( ("commitTx" | txCodec) :: @@ -336,7 +337,7 @@ private[channel] object ChannelCodecs3 { ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly private val shortids: Codec[ShortIdAliases] = ( ("real_opt" | optional(bool8, realshortchannelid)) :: @@ -449,8 +450,7 @@ private[channel] object ChannelCodecs3 { ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx_opt :: waitingSince :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt)) - val closing = DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - ChannelTypes3.fillForceCloseTxData(closing) + DATA_CLOSING(commitments1, waitingSince, commitments1.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val unconfirmedFundingTxCodec: Codec[UnconfirmedFundingTx] = discriminated[UnconfirmedFundingTx].by(uint8) @@ -471,8 +471,7 @@ private[channel] object ChannelCodecs3 { ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec))).map { case commitments :: fundingTx_opt :: waitingSince :: _ :: mutualCloseProposed :: mutualClosePublished :: localCommitPublished :: remoteCommitPublished :: nextRemoteCommitPublished :: futureRemoteCommitPublished :: revokedCommitPublished :: HNil => val commitments1 = ChannelTypes0.setFundingStatus(commitments, SingleFundedUnconfirmedFundingTx(fundingTx_opt.flatMap(_.signedTx_opt))) - val closing = DATA_CLOSING(commitments1, waitingSince, commitments.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) - ChannelTypes3.fillForceCloseTxData(closing) + DATA_CLOSING(commitments1, waitingSince, commitments.params.localParams.upfrontShutdownScript_opt.get, mutualCloseProposed, mutualClosePublished, localCommitPublished, remoteCommitPublished, nextRemoteCommitPublished, futureRemoteCommitPublished, revokedCommitPublished) }.decodeOnly val DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_06_Codec: Codec[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala index a5dc426133..ce14345e8e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelTypes3.scala @@ -21,8 +21,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.eclair.channel import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.DirectedHtlc -import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, HtlcSuccessTx, HtlcTimeoutTx} import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 import fr.acinq.eclair.wire.protocol.CommitSig @@ -55,92 +53,4 @@ private[channel] object ChannelTypes3 { ) } - // Between version4 and version5 (in https://github.com/ACINQ/eclair/pull/3074), we added more fields to our - // TransactionWithInputInfo instances. We fill those missing fields in case nodes upgrade while a channel is closing. - def fillForceCloseTxData(closing: DATA_CLOSING): DATA_CLOSING = { - closing.copy( - localCommitPublished = closing.localCommitPublished.map(lcp => { - // It is *our* commitment transaction: it uses the to_self_delay that *they* set. - val toLocalDelay = closing.commitments.params.remoteParams.toSelfDelay - val incomingHtlcs = closing.commitments.latest.localCommit.spec.htlcs.collect(DirectedHtlc.incoming).map(add => add.id -> add).toMap - val outgoingHtlcs = closing.commitments.latest.localCommit.spec.htlcs.collect(DirectedHtlc.outgoing).map(add => add.id -> add).toMap - lcp.copy( - claimMainDelayedOutputTx = lcp.claimMainDelayedOutputTx.map(_.copy(toLocalDelay = toLocalDelay)), - htlcTxs = lcp.htlcTxs.map { - case (outpoint, Some(htlcTx: HtlcSuccessTx)) => - val htlcTx1 = incomingHtlcs.get(htlcTx.htlcId) match { - case Some(htlc) => htlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => htlcTx - } - (outpoint, Some(htlcTx1)) - case (outpoint, Some(htlcTx: HtlcTimeoutTx)) => - val htlcTx1 = outgoingHtlcs.get(htlcTx.htlcId) match { - case Some(htlc) => htlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => htlcTx - } - (outpoint, Some(htlcTx1)) - case (outpoint, None) => (outpoint, None) - }, - claimHtlcDelayedTxs = lcp.claimHtlcDelayedTxs.map(_.copy(toLocalDelay = toLocalDelay)), - ) - }), - remoteCommitPublished = closing.remoteCommitPublished.map(rcp => { - val incomingHtlcs = closing.commitments.latest.remoteCommit.spec.htlcs.collect(DirectedHtlc.incoming).map(add => add.id -> add).toMap - val outgoingHtlcs = closing.commitments.latest.remoteCommit.spec.htlcs.collect(DirectedHtlc.outgoing).map(add => add.id -> add).toMap - rcp.copy( - claimHtlcTxs = rcp.claimHtlcTxs.map { - case (outpoint, Some(claimHtlcTx: ClaimHtlcSuccessTx)) => - val claimHtlcTx1 = outgoingHtlcs.get(claimHtlcTx.htlcId) match { - case Some(htlc) => claimHtlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => claimHtlcTx - } - (outpoint, Some(claimHtlcTx1)) - case (outpoint, Some(claimHtlcTx: ClaimHtlcTimeoutTx)) => - val claimHtlcTx1 = incomingHtlcs.get(claimHtlcTx.htlcId) match { - case Some(htlc) => claimHtlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => claimHtlcTx - } - (outpoint, Some(claimHtlcTx1)) - case (outpoint, None) => (outpoint, None) - } - ) - }), - nextRemoteCommitPublished = closing.nextRemoteCommitPublished.map(rcp => { - val incomingHtlcs = closing.commitments.latest.nextRemoteCommit_opt.get.commit.spec.htlcs.collect(DirectedHtlc.incoming).map(add => add.id -> add).toMap - val outgoingHtlcs = closing.commitments.latest.nextRemoteCommit_opt.get.commit.spec.htlcs.collect(DirectedHtlc.outgoing).map(add => add.id -> add).toMap - rcp.copy( - claimHtlcTxs = rcp.claimHtlcTxs.map { - case (outpoint, Some(claimHtlcTx: ClaimHtlcSuccessTx)) => - val claimHtlcTx1 = outgoingHtlcs.get(claimHtlcTx.htlcId) match { - case Some(htlc) => claimHtlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => claimHtlcTx - } - (outpoint, Some(claimHtlcTx1)) - case (outpoint, Some(claimHtlcTx: ClaimHtlcTimeoutTx)) => - val claimHtlcTx1 = incomingHtlcs.get(claimHtlcTx.htlcId) match { - case Some(htlc) => claimHtlcTx.copy(paymentHash = htlc.paymentHash, htlcExpiry = htlc.cltvExpiry) - case None => claimHtlcTx - } - (outpoint, Some(claimHtlcTx1)) - case (outpoint, None) => (outpoint, None) - } - ) - }), - revokedCommitPublished = closing.revokedCommitPublished.map(rvk => { - // It is *their* commitment transaction: it uses the to_self_delay that *we* set. - val toRemoteDelay = closing.commitments.params.localParams.toSelfDelay - rvk.copy( - mainPenaltyTx = rvk.mainPenaltyTx.map(_.copy(toRemoteDelay = toRemoteDelay)), - // Ideally, we should fill the payment_hash and htlc_expiry for HTLC-penalty txs. Unfortunately we cannot - // easily do that: we'd need access to the past HTLCs DB and we'd need to recompute the revocation secret. - // In practice, it is fine if we don't migrate those transactions because: - // - we already have a previously signed version of them that pays a high feerate and will likely confirm - // - if they don't confirm and our peer is able to get their HTLC tx confirmed, we will react and use a - // penalty transaction to claim the output of their HTLC tx - claimHtlcDelayedPenaltyTxs = rvk.claimHtlcDelayedPenaltyTxs.map(_.copy(toRemoteDelay = toRemoteDelay)), - ) - }) - ) - } - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala index 3f5f622ead..5cd74594c1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4.scala @@ -12,6 +12,7 @@ import fr.acinq.eclair.channel.fund.{InteractiveTxBuilder, InteractiveTxSigningS import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, OutgoingHtlc} +import fr.acinq.eclair.wire.internal.channel.version2.ChannelTypes2 import fr.acinq.eclair.wire.internal.channel.version3.ChannelTypes3 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ @@ -609,44 +610,69 @@ private[channel] object ChannelCodecs4 { ("unsignedTx" | closingTxCodec) :: ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - private val legacyLocalCommitPublishedCodec: Codec[LocalCommitPublished] = ( + private val localCommitPublishedCodec_07: Codec[LocalCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxNoToSelfDelayCodec)) :: ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxNoToSelfDelayCodec)) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly - val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( + private val localCommitPublishedCodec_1a: Codec[LocalCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainDelayedOutputTx" | optional(bool8, claimLocalDelayedOutputTxCodec)) :: ("htlcTxs" | mapCodec(outPointCodec, optional(bool8, htlcTxCodec))) :: ("claimHtlcDelayedTx" | listOfN(uint16, htlcDelayedTxCodec)) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[LocalCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.LocalCommitPublished].decodeOnly.map[LocalCommitPublished](_.migrate()).decodeOnly - val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( + val localCommitPublishedCodec: Codec[LocalCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("anchorOutput_opt" | optional(bool8, outPointCodec)) :: + ("incomingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("outgoingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("htlcDelayedOutputs" | setCodec(outPointCodec)) :: + ("irrevocablySpent" | spentMapCodec)).as[LocalCommitPublished] + + private val remoteCommitPublishedCodec_07: Codec[RemoteCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: ("claimHtlcTxs" | mapCodec(outPointCodec, optional(bool8, claimHtlcTxCodec))) :: ("claimAnchorTxs" | listOfN(uint16, claimAnchorOutputTxCodec)) :: - ("spent" | spentMapCodec)).as[RemoteCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RemoteCommitPublished].decodeOnly.map[RemoteCommitPublished](_.migrate()).decodeOnly + + val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("anchorOutput_opt" | optional(bool8, outPointCodec)) :: + ("incomingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("outgoingHtlcs" | mapCodec(outPointCodec, uint64overflow)) :: + ("irrevocablySpent" | spentMapCodec)).as[RemoteCommitPublished] - private val legacyRevokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + private val revokedCommitPublishedCodec_07: Codec[RevokedCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: ("mainPenaltyTx" | optional(bool8, mainPenaltyTxNoToSelfDelayCodec)) :: ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxNoPaymentHashCodec)) :: ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxNoToSelfDelayCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly - val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + private val revokedCommitPublishedCodec_1a: Codec[RevokedCommitPublished] = ( ("commitTx" | txCodec) :: ("claimMainOutputTx" | optional(bool8, claimRemoteCommitMainOutputTxCodec)) :: ("mainPenaltyTx" | optional(bool8, mainPenaltyTxCodec)) :: ("htlcPenaltyTxs" | listOfN(uint16, htlcPenaltyTxCodec)) :: ("claimHtlcDelayedPenaltyTxs" | listOfN(uint16, claimHtlcDelayedOutputPenaltyTxCodec)) :: - ("spent" | spentMapCodec)).as[RevokedCommitPublished] + ("spent" | spentMapCodec)).as[ChannelTypes2.RevokedCommitPublished].decodeOnly.map[RevokedCommitPublished](_.migrate()).decodeOnly + + val revokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("remoteOutput_opt" | optional(bool8, outPointCodec)) :: + ("htlcOutputs" | setCodec(outPointCodec)) :: + ("htlcDelayedOutputs" | setCodec(outPointCodec)) :: + ("irrevocablySpent" | spentMapCodec)).as[RevokedCommitPublished] // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel // cannot be used for payments. @@ -856,11 +882,11 @@ private[channel] object ChannelCodecs4 { ("finalScriptPubKey" | lengthDelimited(bytes)) :: ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, legacyLocalCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, legacyRevokedCommitPublishedCodec))).as[DATA_CLOSING].map(closing => ChannelTypes3.fillForceCloseTxData(closing)).decodeOnly + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec_07)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec_07))).as[DATA_CLOSING] val DATA_CLOSING_11_Codec: Codec[DATA_CLOSING] = ( ("commitments" | versionedCommitmentsCodec) :: @@ -868,13 +894,25 @@ private[channel] object ChannelCodecs4 { ("finalScriptPubKey" | lengthDelimited(bytes)) :: ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: - ("localCommitPublished" | optional(bool8, legacyLocalCommitPublishedCodec)) :: - ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec)) :: - ("revokedCommitPublished" | listOfN(uint16, legacyRevokedCommitPublishedCodec))).as[DATA_CLOSING].map(closing => ChannelTypes3.fillForceCloseTxData(closing)).decodeOnly + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec_07)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec_07))).as[DATA_CLOSING] val DATA_CLOSING_1a_Codec: Codec[DATA_CLOSING] = ( + ("commitments" | versionedCommitmentsCodec) :: + ("waitingSince" | blockHeight) :: + ("finalScriptPubKey" | lengthDelimited(bytes)) :: + ("mutualCloseProposed" | listOfN(uint16, closingTxCodec)) :: + ("mutualClosePublished" | listOfN(uint16, closingTxCodec)) :: + ("localCommitPublished" | optional(bool8, localCommitPublishedCodec_1a)) :: + ("remoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("nextRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("futureRemoteCommitPublished" | optional(bool8, remoteCommitPublishedCodec_07)) :: + ("revokedCommitPublished" | listOfN(uint16, revokedCommitPublishedCodec_1a))).as[DATA_CLOSING] + + val DATA_CLOSING_1b_Codec: Codec[DATA_CLOSING] = ( ("commitments" | versionedCommitmentsCodec) :: ("waitingSince" | blockHeight) :: ("finalScriptPubKey" | lengthDelimited(bytes)) :: @@ -897,6 +935,7 @@ private[channel] object ChannelCodecs4 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x1b, Codecs.DATA_CLOSING_1b_Codec) .typecase(0x1a, Codecs.DATA_CLOSING_1a_Codec) .typecase(0x19, Codecs.DATA_SHUTDOWN_19_Codec) .typecase(0x18, Codecs.DATA_NORMAL_18_Codec) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 404394a62b..d348a2ed9d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -158,6 +158,7 @@ object TestConstants { ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(15_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 100_000.sat, @@ -346,6 +347,7 @@ object TestConstants { ), onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(15_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 100_000.sat, 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 207e2f69bf..0f222daff3 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,7 +10,7 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT 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.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx, HtlcSuccessTx} +import fr.acinq.eclair.transactions.Transactions.{ClaimHtlcSuccessTx, ClaimHtlcTimeoutTx} import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck} import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass, ToMilliSatoshiConversion} @@ -177,7 +177,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchTxConfirmedTriggered(BlockHeight(750_000), 1, localCommitPublished.commitTx) awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.isConfirmed)) val mainTx = localClosingTxs.mainTx_opt.get - val mainBalance = localCommitPublished.claimMainDelayedOutputTx.map(_.amountIn).get + val mainBalance = localCommitPublished.localOutput_opt.map(o => localCommitPublished.commitTx.txOut(o.index.toInt).amount).get // We already have an off-chain balance from other channels. val balance = OffChainBalance( @@ -198,13 +198,13 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) htlcDelayedTx }) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.claimHtlcDelayedTxs.size == 2)) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.exists(_.htlcDelayedOutputs.size == 2)) assert(balance.addChannelBalance(alice.stateData.asInstanceOf[DATA_CLOSING]) == expected1) // Bob claims the remaining incoming HTLC using his HTLC-timeout transaction: we remove it from our balance. val (remoteCommitPublished, remoteClosingTxs) = remoteClose(localCommitPublished.commitTx, bob, bob2blockchain, htlcTimeoutCount = 3) - val bobHtlcTimeoutTx = remoteCommitPublished.claimHtlcTxs - .collectFirst { case (outpoint, Some(txInfo: ClaimHtlcTimeoutTx)) if txInfo.htlcId == htlcb1.id => outpoint } + val bobHtlcTimeoutTx = remoteCommitPublished.outgoingHtlcs + .collectFirst { case (outpoint, htlcId) if htlcId == htlcb1.id => outpoint } .flatMap(outpoint => remoteClosingTxs.htlcTimeoutTxs.find(_.txIn.head.outPoint == outpoint)) .get alice ! WatchTxConfirmedTriggered(BlockHeight(760_010), 0, bobHtlcTimeoutTx) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala index 1ad0af7664..9ee105de13 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConfSpec.scala @@ -24,10 +24,11 @@ import org.scalatest.funsuite.AnyFunSuite class OnChainFeeConfSpec extends AnyFunSuite { private val defaultFeeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium) + private val defaultMaxClosingFeerate = FeeratePerKw(10_000 sat) private val defaultFeerateTolerance = FeerateTolerance(0.5, 2.0, FeeratePerKw(2500 sat), DustTolerance(15000 sat, closeOnUpdateFeeOverflow = false)) test("should update fee when diff ratio exceeded") { - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1000 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(900 sat))) assert(!feeConf.shouldUpdateFee(FeeratePerKw(1000 sat), FeeratePerKw(1100 sat))) @@ -37,7 +38,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { test("get commitment feerate") { val commitmentFormat = DefaultCommitmentFormat - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = FeeratePerKw(5000 sat)) assert(feeConf.getCommitmentFeerate(feerates1, randomKey().publicKey, commitmentFormat, 100000 sat) == FeeratePerKw(5000 sat)) @@ -51,7 +52,7 @@ class OnChainFeeConfSpec extends AnyFunSuite { val defaultMaxCommitFeerate = defaultFeerateTolerance.anchorOutputMaxCommitFeerate val overrideNodeId = randomKey().publicKey val overrideMaxCommitFeerate = defaultMaxCommitFeerate * 2 - val feeConf = OnChainFeeConf(defaultFeeTargets, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) + val feeConf = OnChainFeeConf(defaultFeeTargets, defaultMaxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map(overrideNodeId -> defaultFeerateTolerance.copy(anchorOutputMaxCommitFeerate = overrideMaxCommitFeerate))) val feerates1 = FeeratesPerKw.single(FeeratePerKw(10000 sat)).copy(fast = defaultMaxCommitFeerate / 2, minimum = FeeratePerKw(250 sat)) assert(feeConf.getCommitmentFeerate(feerates1, defaultNodeId, UnsafeLegacyAnchorOutputsCommitmentFormat, 100000 sat) == defaultMaxCommitFeerate / 2) @@ -84,6 +85,18 @@ class OnChainFeeConfSpec extends AnyFunSuite { assert(feeConf.getCommitmentFeerate(feerates6, overrideNodeId, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, 100000 sat) == FeeratePerKw(10000 sat) * 1.25) } + test("get closing feerate") { + val maxClosingFeerate = FeeratePerKw(2500 sat) + val feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Fast) + val feeConf = OnChainFeeConf(feeTargets, maxClosingFeerate, safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, closeOnOfflineMismatch = true, updateFeeMinDiffRatio = 0.1, defaultFeerateTolerance, Map.empty) + val feerates1 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(1500 sat)) + assert(feeConf.getClosingFeerate(feerates1) == FeeratePerKw(1500 sat)) + val feerates2 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(500 sat)) + assert(feeConf.getClosingFeerate(feerates2) == FeeratePerKw(500 sat)) + val feerates3 = FeeratesPerKw.single(FeeratePerKw(1000 sat)).copy(fast = FeeratePerKw(3000 sat)) + assert(feeConf.getClosingFeerate(feerates3) == maxClosingFeerate) + } + test("fee difference too high") { val tolerance = FeerateTolerance(ratioLow = 0.5, ratioHigh = 4.0, anchorOutputMaxCommitFeerate = FeeratePerKw(2500 sat), DustTolerance(25000 sat, closeOnUpdateFeeOverflow = false)) val testCases = Seq( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala index e6208252d4..18aa6a5391 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/CommitmentsSpec.scala @@ -44,6 +44,7 @@ class CommitmentsSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with private val feerates = FeeratesPerKw.single(TestConstants.feeratePerKw) private val feeConfNoMismatch = OnChainFeeConf( feeTargets = FeeTargets(funding = ConfirmationPriority.Medium, closing = ConfirmationPriority.Medium), + maxClosingFeerate = FeeratePerKw(10_000 sat), safeUtxosThreshold = 0, spendAnchorWithoutHtlcs = true, anchorWithoutHtlcsMaxFee = 10_000.sat, 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 23e6baabd6..92885e1e92 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 @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT 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} +import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestKitBaseClass, TimestampSecond, TimestampSecondLong, randomKey} import org.scalatest.Tag import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.{ByteVector, HexStringSyntax} @@ -95,32 +95,36 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat awaitCond(alice.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")) + lcp.anchorOutput_opt.foreach(_ => alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor]) + lcp.localOutput_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")) // Alice is missing the preimage for 2 of the HTLCs she received. - assert(lcp.htlcTxs.size == 6) + assert(lcp.htlcOutputs.size == 6) 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) + assert(lcp.outgoingHtlcs.values.toSet == htlcTimeoutTxs.map(_.htlcId).toSet) val htlcSuccessTxs = htlcTxs.map(_.txInfo).collect { case tx: HtlcSuccessTx => tx } assert(htlcSuccessTxs.length == 1) + assert(lcp.incomingHtlcs.values.toSet.contains(htlcSuccessTxs.head.htlcId)) // 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")) + rcp.anchorOutput_opt.foreach(_ => bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor]) + rcp.localOutput_opt.foreach(_ => bob2blockchain.expectFinalTxPublished("remote-main-delayed")) // Bob is missing the preimage for 2 of the HTLCs she received. - assert(rcp.claimHtlcTxs.size == 6) + assert(rcp.htlcOutputs.size == 6) 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) + assert(rcp.outgoingHtlcs.values.toSet == claimHtlcTimeoutTxs.map(_.htlcId).toSet) val claimHtlcSuccessTxs = claimHtlcTxs.map(_.tx.txInfo).collect { case tx: ClaimHtlcSuccessTx => tx } assert(claimHtlcSuccessTxs.length == 1) + assert(rcp.incomingHtlcs.values.toSet.contains(claimHtlcSuccessTxs.head.htlcId)) Fixture(alice, Set(htlca1a, htlca1b, htlca2), htlcSuccessTxs, htlcTimeoutTxs, bob, Set(htlcb1a, htlcb1b, htlcb2), claimHtlcSuccessTxs, claimHtlcTimeoutTxs, probe) } @@ -241,10 +245,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = Some(ClaimLocalDelayedOutputTx(tx3.input, tx3.tx, CltvExpiryDelta(720))), - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -263,10 +268,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = Some(ClaimLocalDelayedOutputTx(tx3.input, tx3.tx, CltvExpiryDelta(720))), - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map(tx2.input.outPoint -> tx2.tx) )), remoteCommitPublished = None, @@ -285,17 +291,19 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), nextRemoteCommitPublished = None, @@ -313,17 +321,19 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx3.input.outPoint -> tx3.tx) )), nextRemoteCommitPublished = None, @@ -343,24 +353,27 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = tx1 :: Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx2.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx3.tx, - claimMainOutputTx = None, - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), nextRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), futureRemoteCommitPublished = None, @@ -380,9 +393,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat nextRemoteCommitPublished = None, futureRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimRemoteDelayedOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map.empty )), revokedCommitPublished = Nil) @@ -401,9 +415,10 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat nextRemoteCommitPublished = None, futureRemoteCommitPublished = Some(RemoteCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - claimHtlcTxs = Map.empty, - claimAnchorTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), revokedCommitPublished = Nil) @@ -419,10 +434,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx1.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -431,26 +447,26 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat revokedCommitPublished = RevokedCommitPublished( commitTx = tx2.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx3.input, tx3.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx6.tx, - claimMainOutputTx = None, - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = None, + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: Nil ) @@ -466,10 +482,11 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat mutualClosePublished = Nil, localCommitPublished = Some(LocalCommitPublished( commitTx = tx1.tx, - claimMainDelayedOutputTx = None, - htlcTxs = Map.empty, - claimHtlcDelayedTxs = Nil, - claimAnchorTxs = Nil, + localOutput_opt = None, + anchorOutput_opt = None, + incomingHtlcs = Map.empty, + outgoingHtlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -478,26 +495,26 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat revokedCommitPublished = RevokedCommitPublished( commitTx = tx2.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx3.input, tx3.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx3.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: RevokedCommitPublished( commitTx = tx4.tx, - claimMainOutputTx = Some(ClaimP2WPKHOutputTx(tx5.input, tx5.tx)), - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = Some(tx5.input.outPoint), + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) ) :: RevokedCommitPublished( commitTx = tx6.tx, - claimMainOutputTx = None, - mainPenaltyTx = None, - htlcPenaltyTxs = Nil, - claimHtlcDelayedPenaltyTxs = Nil, + localOutput_opt = None, + remoteOutput_opt = None, + htlcOutputs = Set.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty ) :: Nil ) 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 ee76294cec..20005436d3 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 @@ -573,7 +573,8 @@ trait ChannelStateTestsBase extends Assertions with Eventually { 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) + assert(localCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assert(localCommitPublished.outgoingHtlcs.size == htlcTimeoutCount) val publishedLocalCommitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx assert(publishedLocalCommitTx.txid == commitTx.txid) @@ -585,14 +586,14 @@ trait ChannelStateTestsBase extends Assertions with Eventually { 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 - val publishedMainTx_opt = localCommitPublished.claimMainDelayedOutputTx.map(_ => s2blockchain.expectFinalTxPublished("local-main-delayed").tx) + val publishedMainTx_opt = localCommitPublished.localOutput_opt.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, 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(localCommitPublished.htlcOutputs.contains(htlcTx.input)) assert(htlcTx.desc == "htlc-success" || htlcTx.desc == "htlc-timeout") htlcTx } @@ -604,7 +605,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { val publishedHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => val htlcTx = s2blockchain.expectReplaceableTxPublished[ReplaceableHtlc] assert(htlcTx.commitTx == publishedLocalCommitTx) - assert(localCommitPublished.htlcTxs.contains(htlcTx.txInfo.input.outPoint)) + assert(localCommitPublished.htlcOutputs.contains(htlcTx.txInfo.input.outPoint)) htlcTx } // the publisher actors will sign those transactions before broadcasting them @@ -616,15 +617,15 @@ trait ChannelStateTestsBase extends Assertions with Eventually { 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) + assert(localCommitPublished.htlcDelayedOutputs.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) + localCommitPublished.localOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + localCommitPublished.anchorOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + s2blockchain.expectWatchOutputsSpent(localCommitPublished.htlcOutputs.toSeq) s2blockchain.expectNoMessage(100 millis) // once our closing transactions are published, we watch for their confirmation @@ -648,8 +649,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually { 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) + // HTLCs that where failed downstream or not relayed. Note that since this is the remote commit, IN/OUT are inverted. + assert(remoteCommitPublished.incomingHtlcs.size >= htlcSuccessCount) + assert(remoteCommitPublished.outgoingHtlcs.size == htlcTimeoutCount) // If anchor outputs is used, we use the anchor output to bump the fees if necessary. val publishedAnchorTx_opt = closingData.commitments.params.commitmentFormat match { @@ -657,13 +659,13 @@ trait ChannelStateTestsBase extends Assertions with Eventually { case Transactions.DefaultCommitmentFormat => None } // if s has a main output in the commit tx (when it has a non-dust balance), it should be claimed - val publishedMainTx_opt = remoteCommitPublished.claimMainOutputTx.map(_ => s2blockchain.expectFinalTxPublished("remote-main-delayed").tx) + val publishedMainTx_opt = remoteCommitPublished.localOutput_opt.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 publishedClaimHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => val claimHtlcTx = s2blockchain.expectMsgType[PublishReplaceableTx] assert(claimHtlcTx.tx.commitTx == rCommitTx) - assert(remoteCommitPublished.claimHtlcTxs.contains(claimHtlcTx.input)) + assert(remoteCommitPublished.htlcOutputs.contains(claimHtlcTx.input)) Transaction.correctlySpends(claimHtlcTx.tx.txInfo.tx, rCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) claimHtlcTx } @@ -676,9 +678,9 @@ trait ChannelStateTestsBase extends Assertions with Eventually { 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) + remoteCommitPublished.localOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + remoteCommitPublished.anchorOutput_opt.foreach(outpoint => s2blockchain.expectWatchOutputSpent(outpoint)) + s2blockchain.expectWatchOutputsSpent(remoteCommitPublished.htlcOutputs.toSeq) s2blockchain.expectNoMessage(100 millis) // once our closing transactions are published, we watch for their confirmation 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 b7d612633b..a5576d60d1 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, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Script, Transaction, TxOut} import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair._ @@ -31,7 +31,7 @@ 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.TxPublisher.{PublishFinalTx, PublishReplaceableTx, PublishTx} -import fr.acinq.eclair.channel.publish.{ReplaceableClaimHtlcSuccess, ReplaceableClaimHtlcTimeout, ReplaceableHtlcSuccess, ReplaceableHtlcTimeout, ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor} +import fr.acinq.eclair.channel.publish._ import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.crypto.Sphinx @@ -2240,7 +2240,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (sender can't afford it)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) + val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100_000_000 sat)) // we first update the feerates so that we don't trigger a 'fee too different' error bob.setBitcoinCoreFeerate(fee.feeratePerKw) bob ! fee @@ -2249,9 +2249,11 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(bob.stateName == CLOSING) // channel should be advertised as down assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId == bob.stateData.asInstanceOf[DATA_CLOSING].channelId) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + // even though the feerate is extremely high, we publish our main transaction with a feerate capped by our max-closing-feerate + val mainTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") + assert(Transactions.fee2rate(mainTx.fee, mainTx.tx.weight()) <= bob.nodeParams.onChainFeeConf.maxClosingFeerate * 1.1) + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighFeerateMismatchTolerance)) { f => @@ -3111,7 +3113,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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 == 4) + assert(rcp.htlcOutputs.size == 4) // in response to that, alice publishes her claim txs val claimAnchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] @@ -3119,7 +3121,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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]) alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) - alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.claimHtlcTxs.keys.toSeq) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.htlcOutputs.toSeq) alice2blockchain.expectNoMessage(100 millis) val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { @@ -3206,7 +3208,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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]) alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) - alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.claimHtlcTxs.keys.toSeq) + alice2blockchain.expectWatchOutputsSpent(Seq(claimAnchor.txInfo.input.outPoint, claimMain.txIn.head.outPoint) ++ rcp.htlcOutputs.toSeq) alice2blockchain.expectNoMessage(100 millis) val htlcAmountClaimed = claimHtlcTxs.map(claimHtlcTx => { @@ -3279,7 +3281,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(rvk.htlcOutputs.size == 4) val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed").tx val mainPenaltyTx = alice2blockchain.expectFinalTxPublished("main-penalty").tx @@ -3409,8 +3411,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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(localCommitPublished.claimHtlcDelayedTxs.isEmpty) + assert(localCommitPublished.htlcOutputs.size == 4) + assert(localCommitPublished.htlcDelayedOutputs.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 // so we expect 4 transactions: @@ -3424,7 +3426,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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)) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) - alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localCommitPublished.htlcTxs.keys.toSeq) + alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localCommitPublished.htlcOutputs.toSeq) alice2blockchain.expectNoMessage(100 millis) // 3rd-stage txs are published when htlc txs confirm @@ -3436,7 +3438,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 3) alice2blockchain.expectNoMessage(100 millis) } @@ -3474,7 +3476,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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 htlcTxs = (0 until 3).map(_ => alice2blockchain.expectMsgType[PublishReplaceableTx]) alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) - alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localAnchor.input +: localCommitPublished.htlcTxs.keys.toSeq) + alice2blockchain.expectWatchOutputsSpent(claimMain.input +: localAnchor.input +: localCommitPublished.htlcOutputs.toSeq) alice2blockchain.expectNoMessage(100 millis) // alice sets the confirmation target of each htlc transaction to the htlc expiry @@ -3506,20 +3508,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors awaitCond(alice.stateName == CLOSING) + val lcp = alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get - if (commitFeeBumpDisabled) { - val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") - alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) - alice2blockchain.expectWatchOutputSpent(claimMain.input) - alice2blockchain.expectNoMessage(100 millis) - } else { - // When there are no pending HTLCs, there is no absolute deadline to get the commit tx confirmed, we use priority - 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) + if (!commitFeeBumpDisabled) { + // When there are no pending HTLCs, there is no absolute deadline to get the commit tx confirmed: we use a medium priority. + alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor](ConfirmationTarget.Priority(ConfirmationPriority.Medium)) } + + alice2blockchain.expectFinalTxPublished("local-main-delayed") + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx.txid) + alice2blockchain.expectWatchOutputsSpent(lcp.anchorOutput_opt.toSeq ++ lcp.localOutput_opt.toSeq) + alice2blockchain.expectNoMessage(100 millis) } test("recv Error (anchor outputs zero fee htlc txs without htlcs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => 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 e94638e389..f7529ddc89 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 @@ -33,6 +33,7 @@ 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.transactions.Transactions 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.funsuite.FixtureAnyFunSuiteLike @@ -49,8 +50,8 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit type FixtureParam = SetupFixture - val r1 = randomBytes32() - val r2 = randomBytes32() + val r1: ByteVector32 = randomBytes32() + val r2: ByteVector32 = randomBytes32() override def withFixture(test: OneArgTest): Outcome = { val setup = init(tags = test.tags) @@ -595,16 +596,18 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee (sender can't afford it)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) + val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100_000_000 sat)) // we first update the feerates so that we don't trigger a 'fee too different' error bob.setBitcoinCoreFeerate(fee.feeratePerKw) bob ! fee val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == CannotAffordFees(channelId(bob), missing = 72120000L sat, reserve = 20000L sat, fees = 72400000L sat).getMessage) awaitCond(bob.stateName == CLOSING) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) // commit tx - //bob2blockchain.expectMsgType[PublishTx] // main delayed (removed because of the high fees) - bob2blockchain.expectMsgType[WatchTxConfirmed] + bob2blockchain.expectFinalTxPublished(tx.txid) + // even though the feerate is extremely high, we publish our main transaction with a feerate capped by our max-closing-feerate + val mainTx = bob2blockchain.expectFinalTxPublished("local-main-delayed") + assert(Transactions.fee2rate(mainTx.fee, mainTx.tx.weight()) <= bob.nodeParams.onChainFeeConf.maxClosingFeerate * 1.1) + bob2blockchain.expectWatchTxConfirmed(tx.txid) } test("recv UpdateFee (local/remote feerates are too different)") { f => @@ -726,7 +729,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit 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(rcp.htlcOutputs.size == 2) // in response to that, alice publishes her claim txs val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] @@ -771,7 +774,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit 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(rcp.htlcOutputs.size == 1) // in response to that, alice publishes her claim txs val anchorTx = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] @@ -920,7 +923,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit 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.htlcOutputs.size == 2) val claimMain = alice2blockchain.expectFinalTxPublished("local-main-delayed") val htlc1 = alice2blockchain.expectFinalTxPublished("htlc-timeout") @@ -939,7 +942,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit 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) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 2) 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 ef3b1d327e..1db3bb4d2c 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 @@ -34,6 +34,7 @@ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsT import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer._ 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._ import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, TimestampSecond, randomBytes32, randomKey} @@ -392,7 +393,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob ! CMD_FULFILL_HTLC(htlc1.id, preimage) bob2alice.expectMsgType[UpdateFulfillHtlc] // ignored val (lcp, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 2) - assert(lcp.htlcTxs.size == 2) + assert(lcp.htlcOutputs.size == 2) assert(closingTxs.htlcTimeoutTxs.size == 2) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.contains(lcp)) @@ -407,7 +408,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } val claimHtlcSuccessTx1 = bob2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess] val claimHtlcSuccessTx2 = bob2blockchain.expectReplaceableTxPublished[ReplaceableClaimHtlcSuccess] - assert(Seq(claimHtlcSuccessTx1, claimHtlcSuccessTx2).map(_.txInfo.input.outPoint).toSet == lcp.htlcTxs.keySet) + assert(Seq(claimHtlcSuccessTx1, claimHtlcSuccessTx2).map(_.txInfo.input.outPoint).toSet == lcp.htlcOutputs) // Alice extracts the preimage and forwards it upstream. alice ! WatchOutputSpentTriggered(htlc1.amountMsat.truncateToSatoshi, claimHtlcSuccessTx1.txInfo.tx) @@ -448,7 +449,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(rcp.htlcOutputs.size == 2) assert(closingTxs.htlcSuccessTxs.size == 2) // Alice extracts the preimage and forwards it upstream. @@ -503,7 +504,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Bob claims the htlc outputs from his previous commit tx using its preimage. val (rcp, closingTxs) = localClose(bob, bob2blockchain, htlcSuccessCount = 2) - assert(rcp.htlcTxs.size == 3) + assert(rcp.htlcOutputs.size == 3) assert(closingTxs.htlcSuccessTxs.size == 2) // Bob doesn't have the preimage for the last HTLC. // Alice prepares Claim-HTLC-timeout transactions for each HTLC. @@ -583,7 +584,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // At that point, the HTLCs are not in Alice's commitment yet. val (rcp, closingTxs) = localClose(bob, bob2blockchain) - assert(rcp.htlcTxs.size == 3) + assert(rcp.htlcOutputs.size == 3) // Bob doesn't have the preimage yet for any of those HTLCs. assert(closingTxs.htlcTxs.isEmpty) // Bob receives the preimage for the first two HTLCs. @@ -687,12 +688,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) // actual test starts here - assert(closingState.claimMainDelayedOutputTx.isDefined) + assert(closingState.localOutput_opt.isDefined) assert(closingTxs.mainTx_opt.isDefined) - assert(closingState.htlcTxs.size == 1) + assert(closingState.htlcOutputs.size == 1) assert(closingTxs.htlcTimeoutTxs.size == 1) val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head - assert(closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcDelayedOutputs.isEmpty) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, closingState.commitTx) assert(txListener.expectMsgType[TransactionConfirmed].tx == closingState.commitTx) assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == BlockHeight(42) + bob.stateData.asInstanceOf[DATA_NORMAL].commitments.params.localParams.toSelfDelay.toInt) @@ -720,7 +721,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 1) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, htlcDelayedTx.tx) awaitCond(alice.stateName == CLOSED) } @@ -752,11 +753,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 4) // actual test starts here - assert(closingState.claimMainDelayedOutputTx.isDefined) + assert(closingState.localOutput_opt.isDefined) assert(closingTxs.mainTx_opt.isDefined) - assert(closingState.htlcTxs.size == 4) + assert(closingState.htlcOutputs.size == 4) assert(closingTxs.htlcTimeoutTxs.size == 4) - assert(closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcDelayedOutputs.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, closingTxs.htlcTimeoutTxs(0)) @@ -765,7 +766,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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(200), 0, closingTxs.mainTx_opt.get) alice ! WatchTxConfirmedTriggered(BlockHeight(202), 0, closingTxs.htlcTimeoutTxs(1)) val forwardedFail2 = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.OnChainFail]].htlc alice2relayer.expectNoMessage(100 millis) @@ -783,7 +784,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectWatchOutputSpent(htlcDelayedTx.input) htlcDelayedTx }) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.claimHtlcDelayedTxs.length == 4) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.size == 4) htlcDelayedTxs.foreach(tx => { alice ! WatchOutputSpentTriggered(tx.amount, tx.tx) alice2blockchain.expectWatchTxConfirmed(tx.tx.txid) @@ -808,7 +809,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.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) @@ -836,12 +837,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (closingState, closingTxs) = localClose(alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.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) - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } @@ -865,12 +866,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (rcp, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) channelUpdateListener.expectMsgType[LocalChannelDown] - assert(rcp.claimHtlcTxs.isEmpty) + assert(rcp.htlcOutputs.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) - rcp.claimMainOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) alice2relayer.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) } @@ -922,7 +923,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.htlcTxs.isEmpty && closingState.claimHtlcDelayedTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty && closingState.htlcDelayedOutputs.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) @@ -946,13 +947,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice force-closes. 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(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 2) 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) - closingState.claimMainDelayedOutputTx.foreach(claimMain => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, claimMain.tx)) + closingTxs.mainTx_opt.foreach(tx => alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, tx)) assert(alice.stateName == CLOSING) // Alice receives the preimage for the first HTLC from downstream; she can now claim the corresponding HTLC output. @@ -1010,12 +1011,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val (closingState, closingTxs) = localClose(alice, alice2blockchain, htlcTimeoutCount = 1) + assert(closingTxs.mainTx_opt.nonEmpty) val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head - // simulate a node restart after a feerate increase + // simulate a node restart after a feerate increase that exceeds our max-closing-feerate val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) - alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(15_000 sat))) + assert(alice.nodeParams.onChainFeeConf.maxClosingFeerate == FeeratePerKw(15_000 sat)) + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(20_000 sat))) alice ! INPUT_RESTORED(beforeRestart) alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) @@ -1024,7 +1027,11 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[WatchFundingSpent] // then we should re-publish unconfirmed transactions alice2blockchain.expectFinalTxPublished(closingState.commitTx.txid) - closingTxs.mainTx_opt.foreach(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")) + // we increase the feerate of our main transaction, but cap it to our max-closing-feerate + val mainTx2 = closingTxs.mainTx_opt.map(_ => alice2blockchain.expectFinalTxPublished("local-main-delayed")).get + assert(mainTx2.tx.txOut.head.amount < closingTxs.mainTx_opt.get.txOut.head.amount) + val mainFeerate = Transactions.fee2rate(mainTx2.fee, mainTx2.tx.weight()) + assert(FeeratePerKw(14_500 sat) <= mainFeerate && mainFeerate <= FeeratePerKw(15_500 sat)) assert(alice2blockchain.expectFinalTxPublished("htlc-timeout").input == htlcTimeoutTx.txIn.head.outPoint) alice2blockchain.expectWatchTxConfirmed(closingState.commitTx.txid) closingTxs.mainTx_opt.foreach(tx => alice2blockchain.expectWatchOutputSpent(tx.txIn.head.outPoint)) @@ -1035,7 +1042,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].localCommitPublished.get.htlcDelayedOutputs.nonEmpty) val beforeSecondRestart = alice.stateData.asInstanceOf[DATA_CLOSING] // simulate another node restart @@ -1104,14 +1111,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) - // We re-publish closing transactions. + // We re-publish closing transactions with a higher feerate. val mainTx = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(mainTx.tx.txOut.head.amount < closingTxs.mainTx_opt.get.txOut.head.amount) 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) + assert(htlcDelayedTxs.flatMap(_.tx.txOut.map(_.amount)).sum < Seq(htlcTimeoutDelayedTx, htlcSuccessDelayedTx).flatMap(_.tx.txOut.map(_.amount)).sum) alice2blockchain.expectWatchOutputsSpent(htlcDelayedTxs.map(_.input)) // We replay the HTLC fulfillment: nothing happens since we already published a 3rd-stage transaction. @@ -1152,13 +1161,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // Alice force-closes. val (closingStateAlice, closingTxsAlice) = localClose(alice, alice2blockchain, htlcSuccessCount = 2, htlcTimeoutCount = 3) - assert(closingStateAlice.htlcTxs.size == 6) + assert(closingStateAlice.htlcOutputs.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(closingStateBob.htlcOutputs.size == 6) assert(closingTxsBob.htlcSuccessTxs.size == 2) assert(closingTxsBob.htlcTimeoutTxs.size == 3) @@ -1268,8 +1277,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here channelUpdateListener.expectMsgType[LocalChannelDown] - assert(closingState.claimMainOutputTx.isEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.localOutput_opt.isEmpty) + assert(closingState.htlcOutputs.isEmpty) // when the commit tx is signed, alice knows that the htlc she sent right before the unilateral close will never reach the chain alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) // so she fails it @@ -1333,8 +1342,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitTx = bobCommitTxs.last.commitTx.tx assert(bobCommitTx.txOut.size == 2) // two main outputs val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimMainOutputTx.isEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.localOutput_opt.isEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(closingTxs.mainTx_opt.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) val txPublished = txListener.expectMsgType[TransactionPublished] @@ -1361,7 +1370,7 @@ 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 val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(closingTxs.mainTx_opt.isEmpty) val txPublished = txListener.expectMsgType[TransactionPublished] assert(txPublished.tx == bobCommitTx) @@ -1373,7 +1382,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitTx = bobCommitTxs.last.commitTx.tx val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimAnchorTxs.nonEmpty) + assert(closingState.anchorOutput_opt.nonEmpty) assert(closingTxs.anchorTx_opt.nonEmpty) val replyTo = TestProbe() @@ -1396,9 +1405,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here - assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingState.localOutput_opt.nonEmpty) assert(closingTxs.mainTx_opt.nonEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) txListener.expectMsgType[TransactionPublished] alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) @@ -1417,7 +1426,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchFundingSpentTriggered(bobCommitTx) // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.localOutput_opt.isEmpty) assert(alice.stateName == CLOSING) // once the remote commit is confirmed the channel is definitively closed alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) @@ -1435,9 +1444,9 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) // actual test starts here - assert(closingState.claimMainOutputTx.nonEmpty) + assert(closingState.localOutput_opt.nonEmpty) assert(closingTxs.mainTx_opt.nonEmpty) - assert(closingState.claimHtlcTxs.isEmpty) + assert(closingState.htlcOutputs.isEmpty) assert(alice.stateData.asInstanceOf[DATA_CLOSING].copy(remoteCommitPublished = None) == initialState) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, closingTxs.mainTx_opt.get) @@ -1465,7 +1474,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) - assert(closingState.claimHtlcTxs.size == 3) + assert(closingState.htlcOutputs.size == 3) assert(closingTxs.htlcTimeoutTxs.length == 3) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, bobCommitTx) @@ -1515,8 +1524,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) - assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size == 2) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 2) 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. @@ -1556,7 +1565,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv INPUT_RESTORED (remote commit)") { f => import f._ - // alice sends an htlc to bob + // Alice sends an htlc to Bob: Bob then force-closes. 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 @@ -1565,20 +1574,25 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val htlcTimeoutTx = closingTxs.htlcTxs.head alice ! WatchTxConfirmedTriggered(BlockHeight(0), 0, bobCommitTx) - // simulate a node restart + // We simulate a node restart with a lower feerate. val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2_500 sat))) alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) alice ! INPUT_RESTORED(beforeRestart) alice2blockchain.expectMsgType[SetChannelId] awaitCond(alice.stateName == CLOSING) - // we should re-publish unconfirmed transactions + // We should re-publish unconfirmed transactions. + // Our main transaction should have a lower feerate. + // HTLC transactions are unchanged: the feerate will be based on their expiry. closingTxs.mainTx_opt.foreach(tx => { - alice2blockchain.expectFinalTxPublished("local-main-delayed") + val tx2 = alice2blockchain.expectFinalTxPublished("local-main-delayed") + assert(tx2.tx.txOut.head.amount > tx.txOut.head.amount) 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) + assert(htlcTimeout.txInfo.tx.txid == htlcTimeoutTx.txid) alice2blockchain.expectWatchOutputSpent(htlcTimeout.txInfo.input.outPoint) } @@ -1609,7 +1623,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs } val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 3) - assert(closingState.claimHtlcTxs.size == 3) + assert(closingState.htlcOutputs.size == 3) assert(closingTxs.htlcTimeoutTxs.size == 3) (bobCommitTx, closingTxs, Set(htlca1, htlca2, htlca3)) } @@ -1696,8 +1710,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) - assert(closingState.claimMainOutputTx.nonEmpty) - assert(closingState.claimHtlcTxs.size == 3) + assert(closingState.localOutput_opt.nonEmpty) + assert(closingState.htlcOutputs.size == 3) assert(closingTxs.htlcTxs.size == 1) // we don't have the preimage to claim the htlc-success yet val htlcTimeoutTx = closingTxs.htlcTimeoutTxs.head @@ -1948,18 +1962,18 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx == bobRevokedTx) if (!channelFeatures.paysDirectlyToWallet) { - assert(rvk.claimMainOutputTx.nonEmpty) + assert(rvk.localOutput_opt.nonEmpty) } - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size == 2) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) + assert(rvk.remoteOutput_opt.nonEmpty) + assert(rvk.htlcOutputs.size == 2) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs 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) + assert(htlcPenaltyTxs.map(_.input).toSet == rvk.htlcOutputs) 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 @@ -2099,13 +2113,13 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) if (channelFeatures.paysDirectlyToWallet) { - assert(rvk.claimMainOutputTx.isEmpty) + assert(rvk.localOutput_opt.isEmpty) } else { - assert(rvk.claimMainOutputTx.nonEmpty) + assert(rvk.localOutput_opt.nonEmpty) } - assert(rvk.mainPenaltyTx.nonEmpty) - assert(rvk.htlcPenaltyTxs.size == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) + assert(rvk.remoteOutput_opt.nonEmpty) + assert(rvk.htlcOutputs.size == 4) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs and watches outputs val mainTx_opt = if (!channelFeatures.paysDirectlyToWallet) Some(alice2blockchain.expectFinalTxPublished("remote-main-delayed")) else None @@ -2142,7 +2156,7 @@ 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) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 1) val htlcTimeoutDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") Transaction.correctlySpends(htlcTimeoutDelayedPenalty.tx, bobHtlcTimeoutTx.tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectWatchOutputSpent(htlcTimeoutDelayedPenalty.input) @@ -2150,7 +2164,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 2) val htlcSuccessDelayedPenalty = alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty") Transaction.correctlySpends(htlcSuccessDelayedPenalty.tx, bobHtlcSuccessTx2 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice2blockchain.expectWatchOutputSpent(htlcSuccessDelayedPenalty.input) @@ -2158,7 +2172,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // transactions confirm: alice can move to the closed state val bobHtlcOutpoints = Set(bobHtlcTimeoutTx.input.outPoint, bobHtlcSuccessTx1.input.outPoint) - val remainingHtlcPenaltyTxs = rvk.htlcPenaltyTxs.filterNot(htlcPenalty => bobHtlcOutpoints.contains(htlcPenalty.input.outPoint)) + val remainingHtlcPenaltyTxs = htlcPenalty.filterNot(tx => bobHtlcOutpoints.contains(tx.input)) assert(remainingHtlcPenaltyTxs.size == 2) alice ! WatchTxConfirmedTriggered(BlockHeight(110), 2, remainingHtlcPenaltyTxs.head.tx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, remainingHtlcPenaltyTxs.last.tx) @@ -2192,8 +2206,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) val rvk = alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head assert(rvk.commitTx == bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) - assert(rvk.htlcPenaltyTxs.size == 4) - assert(rvk.claimHtlcDelayedPenaltyTxs.isEmpty) + assert(rvk.htlcOutputs.size == 4) + assert(rvk.htlcDelayedOutputs.isEmpty) // alice publishes the penalty txs and watches outputs val mainTx = alice2blockchain.expectFinalTxPublished("remote-main-delayed") @@ -2237,16 +2251,121 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! WatchOutputSpentTriggered(bobHtlcTxs(0).amountIn, bobHtlcTx) alice2blockchain.expectWatchTxConfirmed(bobHtlcTx.txid) alice ! WatchTxConfirmedTriggered(BlockHeight(129), 7, bobHtlcTx) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 4) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 4) 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(htlcDelayedPenalty.map(_.input).toSet == spentOutpoints.toSet) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.map(_.input.outPoint).toSet == spentOutpoints.toSet) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs == spentOutpoints.toSet) htlcDelayedPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) alice2blockchain.expectWatchOutputsSpent(spentOutpoints) alice2blockchain.expectNoMessage(100 millis) } + test("recv INPUT_RESTORED (revoked htlc transactions confirmed)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => + import f._ + + // Bob publishes one of his revoked txs. + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(2_500 sat))) + val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) + val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) + val commitTx = bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx + alice ! WatchFundingSpentTriggered(commitTx) + awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) + + // Alice publishes the penalty txs and watches outputs. + 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(commitTx.txid) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx.input, mainPenalty.input) ++ htlcPenalty.map(_.input)) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_000), 2, commitTx) + alice2blockchain.expectNoMessage(100 millis) + + // Bob claims HTLC outputs using aggregated transactions. + val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.map(_.htlcTx) + assert(bobHtlcTxs.map(_.input.outPoint).size == 4) + val bobHtlcTx1 = Transaction( + 2, + Seq( + TxIn(OutPoint(randomTxId(), 4), Nil, 1), // utxo used for fee bumping + bobHtlcTxs(0).tx.txIn.head, + TxIn(OutPoint(randomTxId(), 4), Nil, 1), // unrelated utxo + bobHtlcTxs(1).tx.txIn.head, + ), + Seq( + TxOut(10_000 sat, Script.pay2wpkh(randomKey().publicKey)), // change output + bobHtlcTxs(0).tx.txOut.head, + TxOut(15_000 sat, Script.pay2wpkh(randomKey().publicKey)), // unrelated output + bobHtlcTxs(1).tx.txOut.head, + ), + 0 + ) + val bobHtlcTx2 = Transaction( + 2, + Seq( + bobHtlcTxs(2).tx.txIn.head, + bobHtlcTxs(3).tx.txIn.head, + TxIn(OutPoint(randomTxId(), 0), Nil, 1), // utxo used for fee bumping + ), + Seq( + bobHtlcTxs(2).tx.txOut.head, + bobHtlcTxs(3).tx.txOut.head, + TxOut(20_000 sat, Script.pay2wpkh(randomKey().publicKey)), // change output + ), + 0 + ) + + // Alice reacts by publishing penalty txs that spend bob's htlc transactions. + val htlcDelayedPenalty = Seq(bobHtlcTx1, bobHtlcTx2).flatMap(bobHtlcTx => { + alice ! WatchOutputSpentTriggered(bobHtlcTxs(0).amountIn, bobHtlcTx) + alice2blockchain.expectWatchTxConfirmed(bobHtlcTx.txid) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_004), 7, bobHtlcTx) + val htlcDelayedPenalty = (1 to 2).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedPenalty.map(_.input)) + htlcDelayedPenalty.foreach(penalty => Transaction.correctlySpends(penalty.tx, bobHtlcTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + htlcDelayedPenalty + }) + assert(htlcDelayedPenalty.map(_.input).toSet == Set(OutPoint(bobHtlcTx1, 1), OutPoint(bobHtlcTx1, 3), OutPoint(bobHtlcTx2, 0), OutPoint(bobHtlcTx2, 1))) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.head.htlcDelayedOutputs.size == 4) + + // We simulate a node restart after a feerate increase. + val beforeRestart = alice.stateData.asInstanceOf[DATA_CLOSING] + alice.nodeParams.setBitcoinCoreFeerates(FeeratesPerKw.single(FeeratePerKw(5_000 sat))) + alice.setState(WAIT_FOR_INIT_INTERNAL, Nothing) + alice ! INPUT_RESTORED(beforeRestart) + alice2blockchain.expectMsgType[SetChannelId] + awaitCond(alice.stateName == CLOSING) + + // We re-publish closing transactions with a higher feerate. + val mainTx2 = alice2blockchain.expectFinalTxPublished("remote-main-delayed") + assert(mainTx2.input == mainTx.input) + assert(mainTx2.tx.txOut.head.amount < mainTx.tx.txOut.head.amount) + Transaction.correctlySpends(mainTx2.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + val mainPenalty2 = alice2blockchain.expectFinalTxPublished("main-penalty") + assert(mainPenalty2.input == mainPenalty.input) + assert(mainPenalty2.tx.txOut.head.amount < mainPenalty.tx.txOut.head.amount) + Transaction.correctlySpends(mainPenalty2.tx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchOutputsSpent(Seq(mainTx2.input, mainPenalty2.input)) + val htlcDelayedPenalty2 = (1 to 4).map(_ => alice2blockchain.expectFinalTxPublished("htlc-delayed-penalty")) + alice2blockchain.expectWatchOutputsSpent(htlcDelayedPenalty2.map(_.input)) + assert(htlcDelayedPenalty2.map(_.input).toSet == htlcDelayedPenalty.map(_.input).toSet) + assert(htlcDelayedPenalty2.map(_.tx.txOut.head.amount).sum < htlcDelayedPenalty.map(_.tx.txOut.head.amount).sum) + htlcDelayedPenalty2.foreach { + case txInfo if txInfo.input.txid == bobHtlcTx1.txid => Transaction.correctlySpends(txInfo.tx, Seq(bobHtlcTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + case txInfo => Transaction.correctlySpends(txInfo.tx, Seq(bobHtlcTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // The remaining transactions confirm. + alice ! WatchTxConfirmedTriggered(BlockHeight(700_009), 18, mainTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(700_011), 11, mainPenalty2.tx) + // Some of the updated HTLC-delayed penalty transactions confirm. + htlcDelayedPenalty2.take(3).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(700_015), 0, p.tx)) + assert(alice.stateName == CLOSING) + // The last HTLC-delayed penalty to confirm is the previous version with a lower feerate. + htlcDelayedPenalty.filter(p => !htlcDelayedPenalty2.take(3).map(_.input).contains(p.input)).foreach(p => alice ! WatchTxConfirmedTriggered(BlockHeight(700_016), 0, p.tx)) + awaitCond(alice.stateName == CLOSED) + } + private def testRevokedTxConfirmed(f: FixtureParam, channelFeatures: ChannelFeatures): Unit = { import f._ assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 5db35ffdbf..b86a48f43b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -261,8 +261,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we generate a few blocks to get the commit tx confirmed generateBlocks(8, Some(minerAddress)) // we wait until the htlc-timeout has been broadcast - assert(localCommit.htlcTxs.size == 1) - waitForOutputSpent(localCommit.htlcTxs.keys.head, bitcoinClient, sender) + assert(localCommit.htlcOutputs.size == 1) + waitForOutputSpent(localCommit.htlcOutputs.head, bitcoinClient, sender) // we generate more blocks for the htlc-timeout to reach enough confirmations generateBlocks(8, Some(minerAddress)) // this will fail the htlc @@ -317,8 +317,8 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { generateBlocks((htlc.cltvExpiry.blockHeight - getBlockHeight()).toInt, Some(minerAddress)) // we wait until the claim-htlc-timeout has been broadcast val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - assert(remoteCommit.claimHtlcTxs.size == 1) - waitForOutputSpent(remoteCommit.claimHtlcTxs.keys.head, bitcoinClient, sender) + assert(remoteCommit.htlcOutputs.size == 1) + waitForOutputSpent(remoteCommit.htlcOutputs.head, bitcoinClient, sender) // and we generate blocks for the claim-htlc-timeout to reach enough confirmations generateBlocks(8, Some(minerAddress)) // this will fail the htlc 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 24f2a95e79..fd3c014633 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 @@ -21,7 +21,7 @@ import akka.actor.ActorRef import akka.event.LoggingAdapter import akka.testkit.TestProbe import com.softwaremill.quicklens.{ModifyPimp, QuicklensAt} -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, TxId, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ @@ -34,7 +34,6 @@ import fr.acinq.eclair.payment.relay.{OnTheFlyFunding, PostRestartHtlcCleaner, R import fr.acinq.eclair.payment.send.SpontaneousRecipient import fr.acinq.eclair.router.BaseRouterSpec.channelHopFromUpdate import fr.acinq.eclair.router.Router.Route -import fr.acinq.eclair.transactions.Transactions.{ClaimRemoteDelayedOutputTx, InputInfo} import fr.acinq.eclair.transactions.{DirectedHtlc, IncomingHtlc, OutgoingHtlc} import fr.acinq.eclair.wire.internal.channel.{ChannelCodecs, ChannelCodecsSpec} import fr.acinq.eclair.wire.protocol._ @@ -426,7 +425,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit 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(closingState.htlcOutputs.size == 4) assert(closingTxs.htlcTxs.size == 4) val htlcTxs = closingTxs.htlcTxs.sortBy(_.txOut.map(_.amount).sum) htlcTxs.reverse.drop(2).zipWithIndex.foreach { @@ -512,9 +511,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit // NB: this isn't actually a revoked commit tx, but we don't check that here, if the channel says it's a revoked // commit we accept it as such, so it simplifies the test. val revokedCommitTx = normal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.copy(txOut = Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey)))) - val dummyClaimMainTx = Transaction(2, Seq(TxIn(OutPoint(revokedCommitTx, 0), Nil, 0)), Seq(revokedCommitTx.txOut.head.copy(amount = 4000 sat)), 0) - val dummyClaimMain = ClaimRemoteDelayedOutputTx(InputInfo(OutPoint(revokedCommitTx, 0), revokedCommitTx.txOut.head, ByteVector.empty), dummyClaimMainTx) - val rcp = RevokedCommitPublished(revokedCommitTx, Some(dummyClaimMain), None, Nil, Nil, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) + val rcp = RevokedCommitPublished(revokedCommitTx, Some(OutPoint(revokedCommitTx, 0)), None, Set.empty, Set.empty, Map(revokedCommitTx.txIn.head.outPoint -> revokedCommitTx)) DATA_CLOSING(normal.commitments, BlockHeight(0), Script.write(Script.pay2wpkh(randomKey().publicKey)), mutualCloseProposed = Nil, revokedCommitPublished = List(rcp)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala index eef61fb583..12958bf700 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/ChannelCodecsSpec.scala @@ -197,22 +197,22 @@ class ChannelCodecsSpec extends AnyFunSuite { val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingLocal.localCommitPublished.nonEmpty) assert(closingLocal.localCommitPublished.get.commitTx.txOut.size == 6) - assert(closingLocal.localCommitPublished.get.htlcTxs.size == 4) - assert(closingLocal.localCommitPublished.get.claimHtlcDelayedTxs.size == 4) + assert(closingLocal.localCommitPublished.get.htlcOutputs.size == 4) + assert(closingLocal.localCommitPublished.get.htlcDelayedOutputs.size == 4) assert(closingLocal.localCommitPublished.get.irrevocablySpent.isEmpty) val closingRemote = channelDataCodec.decode(dataClosingRemote.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRemote.remoteCommitPublished.nonEmpty) assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.size == 7) assert(closingRemote.remoteCommitPublished.get.commitTx.txOut.count(_.amount == AnchorOutputsCommitmentFormat.anchorAmount) == 2) - assert(closingRemote.remoteCommitPublished.get.claimHtlcTxs.size == 3) + assert(closingRemote.remoteCommitPublished.get.htlcOutputs.size == 3) assert(closingRemote.remoteCommitPublished.get.irrevocablySpent.isEmpty) val closingRevoked = channelDataCodec.decode(dataClosingRevoked.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingRevoked.revokedCommitPublished.size == 1) assert(closingRevoked.revokedCommitPublished.head.commitTx.txOut.size == 6) - assert(closingRevoked.revokedCommitPublished.head.htlcPenaltyTxs.size == 4) - assert(closingRevoked.revokedCommitPublished.head.claimHtlcDelayedPenaltyTxs.size == 2) + assert(closingRevoked.revokedCommitPublished.head.htlcOutputs.size == 4) + assert(closingRevoked.revokedCommitPublished.head.htlcDelayedOutputs.size == 2) assert(closingRevoked.revokedCommitPublished.head.irrevocablySpent.isEmpty) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala index 44e8eb5e35..619534af55 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version4/ChannelCodecs4Spec.scala @@ -18,7 +18,7 @@ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.Codecs._ import fr.acinq.eclair.wire.internal.channel.version4.ChannelCodecs4.channelDataCodec import fr.acinq.eclair.wire.protocol.{LiquidityAds, TxSignatures} -import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{BlockHeight, CltvExpiryDelta, Features, MilliSatoshi, MilliSatoshiLong, UInt64, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -230,48 +230,26 @@ class ChannelCodecs4Spec extends AnyFunSuite { assert(originCodec.decode(trampolineRelayedBin.bits).require.value == trampolineRelayed) } - test("fill transaction data for closing channel") { + test("include directed htlc_id for closing channel") { val closing1 = channelDataCodec.decode(hex"0011015847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e701010310100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009888a8f1a9231e36da2168b0c3147a097dfab1f4798cc0a31d3e1dce336471cd080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c0000000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000001808022a698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e03b0a410a12070c62d9b401915adead19b3ff2ccdca0e26eb6ef3bfcaf441ea243033429bb2a4d35268c55113ea97544f4e5c74e85d5fc686722b00927572ae1778303e62d2b2f0eb6256482a72e0b3680e5a2ce9f81b09822336c1b253df3fad71e40023b7b1a73bfc39356e17713e070cd4e9d5206cb4a6b2b0c5965bb687c39f5d7d2000000140800000000000000000000000000100802aa6982000000014a00825847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000000000004422f65fd6bb15259a70d2ec79aaa6953d2a58e7d3a4eae779276eab45fe40f30000000000000000000000000000000000040000000000000001000200fd05b15847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e7000000000000000300000000047868c0e60afd49d053a0660837e692ca97e6a3a83536bd40a7cce944d0e2c1d2abe7dd00061b10000332c48fe95695da4f2099a3943abf99be9705cbec2f1af961298d701bec53decce487d6910cead042d5a45c37b7009fa196fa2dbc55b44490000a6a60f7a18d7b2fd832253c16a70295a8099a685c42da3a0334248bcfd91417dde694c3a0afd9afdd74f710b6df5a0e1674432670728a221c4eb52b4a4971cfdc8a8205493ce15d3ac666ac8cd6fd579fd62a17b946319c9b67fe2e401c0a5685430a08cf7da7309f54a23cc0d2e5b72d643cdb9a63e8a8f3add3130cca9eac0efc23db0c97da94d1ea8fb512d6b0a2fa47c21b43b034bfb0f2918585c62d6f4af2a95c4ffa27632cd3724033a53fe1d78fe7ae98b81e6e724949182fb1e214765796de6ea9622af13d6e90181c66287ffd2811b4fd8b6b71d50ff45d8ec297906b8d917262eb9572fe92ca181480037d9e0a0c81d6df5894f70e989579a0a363bd5a690d69752d854ae28c79025082e59c01345c2b054f43655d94fc6c088e12be04266f2c1aa6574ede4f20892acbd531f82321a752c65a8760f264e9de46f4462b2bba67192cdeb4e281a371a121595e8389cdd90885cc51b05f3712ec2dfb934ee90709affde926c6b6eb542709cb2d8bced2ac903bd8c15ecf9fc2189929ea031133ff1b4897b26ae5da97e9ed49d25bbf0143a22c0827b0b24b6a020fe2ce5f769939d71c1ddc94b7ebbb84547b1b1f49e90728d19bd55f3730044e583089cea132462184c6ce6d7f7b07b4b2bd6fdf89f1340b65031813a9abe9ce51377885235bffc005346af8218e9d32410c7c0584ae13a4b0e75075b76fddc9ce36ab9ec3dae511cef81760d98653431a464dc81bbef029797896032f9601d9a6b0c45ad790b0f16914c382d698dc46111a6d4aed0f7515e338d1e3668ae27bf06c0b50f14f9ec8e14abc88ac68b0ad3c6c4cc307ce6b6c084ec3399c1b1a7748d61911ee1c425973c737b55d4504f4509e25d5efb5793fc382cd7e0f260bbba79e666f09dad12aded5d402bc99e65098f4ad68f5f88179930cace8b9b23720f3261efa542cb2b0c488fd6d0ddc3566b6896580349b90584ba61bea6c20a1541be43f71f74db167282ab6e19501f477835a4d444717de9b1f70361cbceed8b71b51e91fd939084c53199fe3171a22cf53bbcb8b245a523cf5af1a40bb9ba6e86f8592d9292bfa3efdce8a34583eb32beebb0c8847e051802f977e6e63765c3235fc7b8b3964f3b82ec5686e77f5901cf3887e5653ec5f2b02842454274db8bc9fa1bd73d6fa1ef786bca55fd6813e3016abc4ba87db319ac6ab7ddf8b9ee11cd92f3a506b3af86e825cea498a5ce72dc6a6c763e6392a21edd3722d6a049011530ebfc6f0784430ebdbd5e4fe3c35bed8cfc51d915a101ffb87cf2b269274aaaf8b37ead9451e371ee8b5958ea8300ee568c707c913344f4bb5344e62b095384212b1d8b00873af5feb6e756818e0806e42771875911aa67ab6e0c4077f67a6a8841ca0fc69d57c818152a52d8468f4cd623feb1a30241834a3d02348c103ccefde6997709bc8e7569efabf4c5f712826fb03ba1c01e8f57edb113734e03a4933223149b4ecd844e933210ab4ca366d630b02005a8d0189737a2bff410f0a97aa3ae5811877e27de9b451e35f1c901c379f864e02c823ad6cf3e92db32437f53c108960a012c4d28d3b66f39b66b1b57af170b00fb976fc63b6123cb22ee216cdc48b0a9e8e11bef5fc36f6e5bf3d40eb28f7c0d7c498d7c740c0008a9cade8d147fd99a5b323b92e93fc64ec2599d7d19a997fcf8653f059e082fda5bed8c79ee29c356597a56dc08a5965aa69f44b7d2377d7bf75819a74721eddc5218cf1f16a1ff5f50e37fe14a51d4206d576ce63c2b9cf472e871fa253a36e4c469abfaf17e8e910f966b6ce0075a6088a45cdcf4ef095df06e37ed994b5e0fe0001a1470107fffd05b15847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000000000000000000004c4b40095cc3acd7792306bc5792d504325926816fe42457c6ad29b00741901fbe46d6c00061b10000281f8622444b2130756cff4fa963aebe1a6591ba779ae258b2c4a4d29bd1f1c5227f7db4ff5d623465d52dae4445a3e4079fb3d10e286e1f51c38e5f336e543ffd11936c2802f5f952689b7f6aab330304fab68e3eb52eaf384291241330c1e397275f0b5c75033f68ac4231db6e9909bba53fe6f5c811f1d08043d47c2c29830cc1765297c6f175dcd0fc66aba6939a5d8572c1e6211f38081b6072ce0baa79f51d5a24b4ad545a57fc40d55b5a6e2f403789635478df97f821b5ac59bade80d3ff7a61619818924fd22397f8eab9ba8b627cc1574f217ed31ab8beb2f14568d2959b859baa7ecbdc7f83d58f24c406c9600081d087a039c01c42e8e998558265318a63c17c8768a4f5aa08c611f24378f8f05b376e51b9cb2d303fc587f25112e2b02fee7783ee6956c1cfc170a3e5e4efe3daed57dbd60d82b3ddb2c3621150a29b043a99aa25ec4e1e6dd8cd3e530fb423b2826a97f7947e73fda2d0334f22282f65306a54d0544fac2b2e1910f3627cfa9cc933fc70f78c70f8d9e7a48988981393355c0170d7ae1b5e3bf0cbac64796ef9780b6bee51394d9dab37f7706318c29432cc28ce7133c6f9ab109e414b1c721bd61acecc903c3cf7018eeac6cd76414905abb2139f32225238d546f70b18164d48f46521b367053fcb3ddf58256c46296f0a38f049e789248bb3b91744206e445444280487eb53e35af868c9a5c097be32413523e25ec589fff8809053c68ac22c312e113c8266f1f4e6648f48ba435d03a5dcd22eae0065e5dd2a4e8054952fefb71630350ea229b6a22e3799713fce8b97e89dfa792a140f3105a32fc3e61b9e536ecf7e0617c4e285eab7a6aa3ea53842ac994e302c1699f0efc2fb9d8ac7ece2179821cb94d8c52079c74fc4178bbce4031137f1c78d2275eae8e7dd43b627329261fe8215421b4aa5e3ad3034686801863132707eb2b36f0510c90ba0cb901c05a9b32d3535669b2ed2a43d594ded9bb4c6ff228b352737fe84bddae6b2f6320d46853e30b14c842aadd9ad2f19d0ef26ee99f1bfad18983b3fd523fd55af06c2a95a8bf9eee832028d23a48667dbb4132cdabad7939dde02e77e3f0848a433591a4e75115623fb29bf03a88a2a744dbbcd6997e079150f1818ef6d31880665ba03ba9e9def8fd468f9fd2ef67f240dbb6c74c2f2ba303ff544db022ad5d034080e6eebaee744500f5d970837658661bd09c6ce4fed4fb828743ea26624b256318e6741f005533692c8a4e67296551f393a922a3576a03a2b1734acfbd4574663dcaeb594d5572b6006b8f593d25b91ba19991c5df5e9861a85206a677beff4c89426b374f150514331864a436f5477f2d1533584ef56dc0b9f2d4c8743e0dcae471a1eafde3b9c353bbbef9fe41305ec332b7e1043282fc7c057d4ebeb15e6c3f2cd774d73842bc20724f4a965c902d13088329ed66783467b03c186a6ed2f5ab19d62a0ac03a01eccdbbd6d1f09fc9d6113b33dabe3140b10bccefeccaa66ea04850818293d44a33829dd6cc80b2be8e9ebbf3061fde598a603d4fe8f4135dee713f4f88459c2ac791d89ad7cb59f0dcbfd96d2ddc954ea735a9aeaee8f47127b7dd4cfd7b08019602e8acc6cd37a7d640e24b589abf2bcb9d1eb4abd19bbe1150938ff90f49b9c5203f1c17c8b85c8a26105995f053fa54f56db9bfdd10f37d9aba6899a2ed375f49197cc3e36cc82839edc36f37d5b661ced3a7528f8f8e438aeb6e8f5c566fa9249fca6966a2dc26f01ae949dabbbdfd03a849b17351dbfa181b219abe93bc97d793f5f72a1d19adaa92043e0ec1200b592c52b3270fd77560f5f2b4340bf936366d79dab9d6c1c7c867019019324b7a74c8d82990e21d2b7bb55ec836f2dbbb2543b77a6f565475ab4fea1822c99a4c1e5885b280fe0001a147010700010000000000000000000000000392de23ee259d18e48e80b3dc7dad83f8cd87a64b4ac0da7dd94871e5ea876ec60a5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200203ac31ef83f5b594ea58c988be0f62bfef04500ba6d678c11aaeeda1fe662f0c700000000061a8000002a000000000200000000000000070002000000000000000003ff0000000000000000000009c40000000007735940000000002aea5400245847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e7000000002b40420f00000000002200203ac31ef83f5b594ea58c988be0f62bfef04500ba6d678c11aaeeda1fe662f0c7475221032991639ef70a233d4d0df569b4d065ed8df5b68ccb507208b8f10984c5a297b6210392de23ee259d18e48e80b3dc7dad83f8cd87a64b4ac0da7dd94871e5ea876ec652aefd013502000000015847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000004c2d0d80064a010000000000002200200f4391a26bcc5afde68e6465183d9b38c91f7b096fb0c3afd8f0561c3a406ea54a01000000000000220020a4bfb70b647bd0ad161401e362f88c00d41151a2b3d4d058a7c23dda3fc755aef82401000000000022002084f49a8cfdec3da99b5faa1459f97c3838223cd8a0ed102ce9365ae154c6d9b5803801000000000022002099f285919d127466e41c9cf5044afbbe12ecb2f6408be9d7cbab3625d6e85d415ed701000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e480fc0a00000000002200200ef18b6f3cdd44b9028e77fd9041c643ba5c5e27de4571405d169fb34ddd7b4dc886ad207b5bf5eb953734dcc0d6a94161f2bf5cdc6fac417dd69b52220c6308768af21906ca7423d76ca3542a62a581e7ea505e5606583a48cd1f29c4f5462c6a38b1f3000212241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9020000002bf82401000000000022002084f49a8cfdec3da99b5faa1459f97c3838223cd8a0ed102ce9365ae154c6d9b58876a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c820120876475527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae67a914a397093635d220f06843b4dca27686b9591dc62a88ac6851b275685e02000000011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd902000000000100000001771e01000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e4101b0600000000000000000300061b10067a0d3a45dfb874cc69c374aa4379885f02326e13e2eb65bf694a3ebcd902a869ef62d89258677fced5f4371a9dc2242d31ce84e2f1d65af2ad4ae3a3a1f12111241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9030000002b803801000000000022002099f285919d127466e41c9cf5044afbbe12ecb2f6408be9d7cbab3625d6e85d418e76a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c8201208763a914ff7c99067754ff00b64b0c55f7bf392afda34a5788527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae677503101b06b175ac6851b275685e02000000011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9030000000001000000019b3101000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e40000000095cc3acd7792306bc5792d504325926816fe42457c6ad29b00741901fbe46d6c000000000000000000061b10f4a0c14c421977d0fbfb4da089096f1b41559df84365b7270ad67e8b92fb28d34c514614affa5ea47eb2b2647457f9d2ea23fb47d449d8ccb9c4b202bac08a2100000000000000080002ff0000000000000003000000000000000000000009c4000000002aea54000000000007735940441c3be8f90738b9d65acd2896726cc8b9796b4c5aca307720997082f876d2ee0327e1109720bd3fc2432f24ca56709ce978688310e88141288440730a357f4d9b000000ff02d87e2e97bfb17b65793e748712a44445bfcc14db5337bcbf323e9faaa44e06da0001003d0000fffffffffff80100349e5c6e9d10f61e0ab643d0d82cee98332c306cbb2d825b6d9c4bcb92c1861c0003ffffffffffe000010000000000000003000369b82e7d4d2f49598062820ba3cad8970000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e300000000fffd0212020000000001015847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000004c2d0d80064a010000000000002200200f4391a26bcc5afde68e6465183d9b38c91f7b096fb0c3afd8f0561c3a406ea54a01000000000000220020a4bfb70b647bd0ad161401e362f88c00d41151a2b3d4d058a7c23dda3fc755aef82401000000000022002084f49a8cfdec3da99b5faa1459f97c3838223cd8a0ed102ce9365ae154c6d9b5803801000000000022002099f285919d127466e41c9cf5044afbbe12ecb2f6408be9d7cbab3625d6e85d415ed701000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e480fc0a00000000002200200ef18b6f3cdd44b9028e77fd9041c643ba5c5e27de4571405d169fb34ddd7b4d0400483045022100881537b143aff4129e0a1918d91ea02c0318d6aa2ee47e9265b6403f5cbe61bc022006f4a18a4bcd58160444daf5bc29f2ae1d460d18bb23076adfa92df656e6e62b0147304402207b5bf5eb953734dcc0d6a94161f2bf5cdc6fac417dd69b52220c6308768af219022006ca7423d76ca3542a62a581e7ea505e5606583a48cd1f29c4f5462c6a38b1f301475221032991639ef70a233d4d0df569b4d065ed8df5b68ccb507208b8f10984c5a297b6210392de23ee259d18e48e80b3dc7dad83f8cd87a64b4ac0da7dd94871e5ea876ec652aec886ad20ff241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9040000002b5ed701000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e44d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068acec020000000001011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd90400000000900000000180c4010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3034730440220710e9165f60bd915b94714b59c8c2062b02adad671d1464385d03204aa885d3402203fab7f3a8987bf55c6ce6cbb95740e7f5c78c818eb1fc05b25ce36c5b2e2136601004d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068ac000000000002241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd902000000ff12241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9020000002bf82401000000000022002084f49a8cfdec3da99b5faa1459f97c3838223cd8a0ed102ce9365ae154c6d9b58876a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c820120876475527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae67a914a397093635d220f06843b4dca27686b9591dc62a88ac6851b27568fd017d020000000001011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd902000000000100000001771e01000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e405004730440220067a0d3a45dfb874cc69c374aa4379885f02326e13e2eb65bf694a3ebcd902a8022069ef62d89258677fced5f4371a9dc2242d31ce84e2f1d65af2ad4ae3a3a1f12183483045022100b9ea29e68afbc2ddc5e3cc2e15bb104e8914daa47154a55fa1540925ac45cea1022021e3440f5fbc93b11b78805d30bb02125b57e45f0acf2fe925ea42e3644d44eb01008876a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c820120876475527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae67a914a397093635d220f06843b4dca27686b9591dc62a88ac6851b27568101b0600000000000000000300061b10241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd903000000ff11241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9030000002b803801000000000022002099f285919d127466e41c9cf5044afbbe12ecb2f6408be9d7cbab3625d6e85d418e76a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c8201208763a914ff7c99067754ff00b64b0c55f7bf392afda34a5788527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae677503101b06b175ac6851b27568fd01a4020000000001011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9030000000001000000019b3101000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e40500483045022100f4a0c14c421977d0fbfb4da089096f1b41559df84365b7270ad67e8b92fb28d302204c514614affa5ea47eb2b2647457f9d2ea23fb47d449d8ccb9c4b202bac08a21834830450221009f00e2782a17f91a5a7bbda12e8fd24aec9d967341d83c95f5f1f5cd7a39e97a02206591dbc6815ef01826186348db314e51d4a8c2247dc7b514d8147a02502cb2fd01204422f65fd6bb15259a70d2ec79aaa6953d2a58e7d3a4eae779276eab45fe40f38e76a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c8201208763a914ff7c99067754ff00b64b0c55f7bf392afda34a5788527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae677503101b06b175ac6851b275680000000095cc3acd7792306bc5792d504325926816fe42457c6ad29b00741901fbe46d6c000000000000000000061b10000224e456c1304d4f950521995e6df1a82ee9bd7b37fb096985ad5c50818180f0d4be000000002b771e01000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e44d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068aced02000000000101e456c1304d4f950521995e6df1a82ee9bd7b37fb096985ad5c50818180f0d4be00000000009000000001990b010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e303483045022100e5bc8ad2039c412edfc977397feb1d274d3404995db5f1243a664148db316f6c02203a1b80894a4f5b610810ac75dc9e5ccdb31301e5e6dce4966251f6527ecb27a101004d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068ac0000000024b2911362612c021a5f9e5f3693483960b105ac335ccd7fa4085d9596cdc623a2000000002b9b3101000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e44d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068acec02000000000101b2911362612c021a5f9e5f3693483960b105ac335ccd7fa4085d9596cdc623a200000000009000000001bd1e010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30347304402200b8ae397cf7c0a30f25c5aa96540ce3600b5157dc52c5767a895a3982bcaf28e02206fc02f2481f5fc145ceba7c2c4cdf59753edd98a64c0a369fbfbc60473bcb51b01004d63210321fe12ac0817cd8b7b32c43e1c37d341edc6b6c0ef2e7a4a193247569ad8ae4467029000b275210262ad7b97170d7443c86d24fc3f4e3fd0710f4eda2d0726f1d953fd49db573e2068ac00000000000112241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9010000002b4a01000000000000220020a4bfb70b647bd0ad161401e362f88c00d41151a2b3d4d058a7c23dda3fc755ae2821032991639ef70a233d4d0df569b4d065ed8df5b68ccb507208b8f10984c5a297b6ac736460b2683302000000011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd901000000000000000000000000000000061b100003245847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000fd0212020000000001015847757d05a5e2639d5777a9f46a8dff49fcd5dfba142d93c4de6af54e4620e700000000004c2d0d80064a010000000000002200200f4391a26bcc5afde68e6465183d9b38c91f7b096fb0c3afd8f0561c3a406ea54a01000000000000220020a4bfb70b647bd0ad161401e362f88c00d41151a2b3d4d058a7c23dda3fc755aef82401000000000022002084f49a8cfdec3da99b5faa1459f97c3838223cd8a0ed102ce9365ae154c6d9b5803801000000000022002099f285919d127466e41c9cf5044afbbe12ecb2f6408be9d7cbab3625d6e85d415ed701000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e480fc0a00000000002200200ef18b6f3cdd44b9028e77fd9041c643ba5c5e27de4571405d169fb34ddd7b4d0400483045022100881537b143aff4129e0a1918d91ea02c0318d6aa2ee47e9265b6403f5cbe61bc022006f4a18a4bcd58160444daf5bc29f2ae1d460d18bb23076adfa92df656e6e62b0147304402207b5bf5eb953734dcc0d6a94161f2bf5cdc6fac417dd69b52220c6308768af219022006ca7423d76ca3542a62a581e7ea505e5606583a48cd1f29c4f5462c6a38b1f301475221032991639ef70a233d4d0df569b4d065ed8df5b68ccb507208b8f10984c5a297b6210392de23ee259d18e48e80b3dc7dad83f8cd87a64b4ac0da7dd94871e5ea876ec652aec886ad20241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd902000000fd017d020000000001011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd902000000000100000001771e01000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e405004730440220067a0d3a45dfb874cc69c374aa4379885f02326e13e2eb65bf694a3ebcd902a8022069ef62d89258677fced5f4371a9dc2242d31ce84e2f1d65af2ad4ae3a3a1f12183483045022100b9ea29e68afbc2ddc5e3cc2e15bb104e8914daa47154a55fa1540925ac45cea1022021e3440f5fbc93b11b78805d30bb02125b57e45f0acf2fe925ea42e3644d44eb01008876a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c820120876475527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae67a914a397093635d220f06843b4dca27686b9591dc62a88ac6851b27568101b0600241dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd903000000fd01a4020000000001011dd545f83c476102271f04dc690ef02c75d7febda458e9fdb4fb73811705cfd9030000000001000000019b3101000000000022002064a48a683d489888a75d7cf57d5427e6bc332b20acf6abca8085d52da28da7e40500483045022100f4a0c14c421977d0fbfb4da089096f1b41559df84365b7270ad67e8b92fb28d302204c514614affa5ea47eb2b2647457f9d2ea23fb47d449d8ccb9c4b202bac08a21834830450221009f00e2782a17f91a5a7bbda12e8fd24aec9d967341d83c95f5f1f5cd7a39e97a02206591dbc6815ef01826186348db314e51d4a8c2247dc7b514d8147a02502cb2fd01204422f65fd6bb15259a70d2ec79aaa6953d2a58e7d3a4eae779276eab45fe40f38e76a9146e2711de9703f69190ce9c77675c385fa44368838763ac6721027fafdc0dc58427092109a17a7a16a600730db3482352a04edafc99eccf1f802f7c8201208763a914ff7c99067754ff00b64b0c55f7bf392afda34a5788527c2102d6a274c66362e55b5b2ac0a8f63cf7fa61b4374843e8cbbc28017dd7cf3e789c52ae677503101b06b175ac6851b27568000000000000000000".bits).require.value.asInstanceOf[DATA_CLOSING] assert(closing1.localCommitPublished.nonEmpty) val lcp = closing1.localCommitPublished.get - assert(lcp.claimMainDelayedOutputTx.map(_.toLocalDelay).contains(CltvExpiryDelta(144))) - val htlcSuccessTxs = lcp.htlcTxs.values.collect { case Some(tx: HtlcSuccessTx) => tx }.toSeq - assert(htlcSuccessTxs.size == 1) - assert(htlcSuccessTxs.head.htlcId == 0) - assert(htlcSuccessTxs.head.paymentHash == ByteVector32(hex"95cc3acd7792306bc5792d504325926816fe42457c6ad29b00741901fbe46d6c")) - assert(htlcSuccessTxs.head.htlcExpiry == CltvExpiry(400144)) - val htlcTimeoutTxs = lcp.htlcTxs.values.collect { case Some(tx: HtlcTimeoutTx) => tx }.toSeq - assert(htlcTimeoutTxs.size == 1) - assert(htlcTimeoutTxs.head.htlcId == 3) - assert(htlcTimeoutTxs.head.paymentHash == ByteVector32(hex"e60afd49d053a0660837e692ca97e6a3a83536bd40a7cce944d0e2c1d2abe7dd")) - assert(htlcTimeoutTxs.head.htlcExpiry == CltvExpiry(400144)) - assert(lcp.claimHtlcDelayedTxs.size == 2) - assert(lcp.claimHtlcDelayedTxs.forall(_.toLocalDelay == CltvExpiryDelta(144))) + assert(lcp.localOutput_opt.nonEmpty) + assert(lcp.incomingHtlcs.values.toSeq == Seq(0)) + assert(lcp.outgoingHtlcs.values.toSeq == Seq(3)) + assert(lcp.htlcDelayedOutputs.size == 2) val closing2 = channelDataCodec.decode(hex"001101ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a010102100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa00094510ff19974f78c704a51a7ed74ed76104201c9b8e2a84378a95c72fdea7bef380000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c000028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000001808020a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e03b240d3152962db6c8df5f492961cee063405a6d1d39a15ef4f6b040170be99e7028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120319bdffabbf3fd88d9635a556fadd2ce3332bd0a66b5ee51b09501dc8531f7e4e028a2744aed6398199fd4d3dce7b21bd994ce0c58027b17d93f10611ee40550ce10000001408000000000000000000000000001008028a5982000000014a0082ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a00000000000000003624f17e10a0ebcc71b4ce5eb541f3e68c73ffddda7294f38f411cd19f24aaed0001fd05b30080ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a00000000000000030000000005a995c05900ea5104b145117c6a48520742fb05d48a16a75ba1cc9f67a5eb90284746d400061b100003a52e309818a26a2737bed449f9310171509591cfd7daeb62216f633b1c9cc14faf2adceebf7dbfcf1fd7138442320ff99c54a4f316fc98b4cd7710c1a279bad88058e6795cb09eb288e07630e9829c6f93b851194363e479a1431ca2cabc4f4c092034cce4ac860533e0cd90b8b5a393941c2f7308ec4068b74c2d245cd6c5cce45a3ee16fc36dc2957134feec5403711489ef56382bbd69e98478323e5b1377564859b67effcec1f164fd82d452863ff2cfbe9e0bfd17b9dcdee5064216fe99df14f3676225321816898b971aa3710e4f954876212e7d446479eb0b5105eb57b90c0f0301d45010f627dc11a67d3289dd6463bb389264b52603e5cee4416f3a1f3d7a2a2da60a0620fc25bb8a01613920049e1a287d0e639462838cbcc1938ab4f441c319324eac70fdea294960c0918ea3361e4325e8da5728d025a3538686e0574e9f72c8745978af00a70f9c7b6610a87e47a720fded32f9af507772e0cb08c64b768d77378c18e3743a27d765aaad9a35cf7bac9e9af2ca5d2a1816d1bd8bba944539e9d85be8b254c82e8ce86333cbf82c72bd5a401eb8fd2e71d84c87f80c65798833f8d4faf5cbf3623f520c6e7c9badd474f6d582f5467e40de58204eb719663ef9f694325df4d7f7a5ef797d675eed2480c7c4f3158f4caeb11b9d0f05b3766c00570274c63475fa6c0f2420b2a19cf289a174961184a4a8dd02ba18d26b845d6960e22689bd790aee45f20dde4ec5156c84ac02c2553d9a5a6bf25a93da90b599420a98b30326509d4661bfb46d9e2f43a1fc97388313f7f3ab61615a728f27ffdaf43c7c88542672983902d79a20fcce03240d3795b58a66c720e1b4a0a9cfc04f368edef33d62eea9e1123ca4a5cb99792d655ad8c44604c42cdb3953335db24fe154b20559774b24ace3d35c2bf518640a5d8b4cd30aa0148e868296d6c9ea050e712f57f940f2693099671038042bfcb145712939129004148c59f79af7a9b7b3c8bf6be4d3a1719629195b7398841df73aee12bb332f132d96298003cee791a60b6c10d0708b3ae46944dd990437ccbe9a37827688150e8aeab68218cca38e8d03ce9ed65ae4831797f45d49ff6e56d72159601e9c1b6766e4feda60d957542eb5df5ab1ef7167924f4ef1d7b7661e9a4c9ee7d7df58d7a2cbc431addfc0a41dfc6e2425b7ab98632e366f5ecaf6ffdc1d73ed47269d1f6fc7c335b7c1f3206ece603016357018ccdf267267548adce056fa2a05c877660019742e10872ea2e24c0b9b853a099f38ae8221b314730bf070cd37859418cf26425d499207d75d5022297d634e05bb4ad6462b44e11a8b9dd6516468d7b4686027b50319fe5676abc4cd25477aabc47f7cf0c1b64c26001f8feac705fbc009d1164cd4811179fda86ae2b93999725b484338d15fbc1c93aca46853a69c6089879e1be8ee2716e0bd3f530de7347ef09443e0a2502ec396da67b13171d0ffddd8b21dc1dafe157c933e596cc52fe1ebe663274a0e058b6b7465280bc27e2c0fb1ccecf7289ad584efef96b0804a1af10b182f788f1b324eb769333af99c7a8f28f6f446032cb387a480354f06bca5960df22b59061946740168d1a383cedb7d7cbccbc3fcff91836ed87d4f3366a6ae7a030b49d37d43a5cee8a85d7443720df1bd57c954d57d5ffdfc9bfadae3754d7ef68830bbd2354069364e99c95d0f72b84fe2d880427c7e41435d3aaa47b2ed28eabe332af0221997386aac37021bcbbd78b8cb12b9f439d8154bf17b5f0252c1fd3c884eb0c723abde233539bf94bef906bc260bc3496cd62c29e82bdd831ea9222af8f8fa28e295409bc4bc7532946e02e6b046857ab8a7d97714fcaf64b1e45ddd1127b678499005bedc4a00a4134b11b2c934e7ecbb164afd9a5964a2def87ffa03bdfe0001a14701070000000000000000000000000000000400000000000000010002fffd05b1ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a000000000000000000000000068e778014d153e87b1c076e0ec079c69f724ad514e470cc9497403c0e7581d20de53df400061ab00002fcf3a4b860fa3e927bf185dc7ddac5b2b5af8bf44dc60da6a8edd8609304afb974f4436aad796a089ec4d7ec2fdfc28070fe0e4b1e5bfb4ce4cf44b6b32a76edb353eacbc7d1ccf503fbd11d3ee7110bc436432bef8e16d91c118bb8b1e84d2f5cd259fdd4dd9668e71cbfdee1cb27c6e528e66e70b69b9df9dd2d2aa05ce8f7c744aeb5bddd0ea456d1d893f7935174f3d70a19f363cadac78bc686b14e22b59bccc90be9c17464c5faab973a806e61aba13c54466c3ea3df26795b88df13846c6043a8e0cbdf2a9ff3484c951d6ccd7fc04f0826d60ebbd2a096b7ba55c114eb5d038d4a501376265d1d6fd056ea70c6d7951369c4aa81edea8aeba8afb9b9fe14320cb0c3f5cec8a2cc508b9480ebe630adc8dd3af0b21eba4d0c39f7c303b983f2af0c8a0ce17a54709544923c557c889712e0913f7fd8c1ce1912d98907e4b14b27019e167f25fda91f0f96916e15524cab8dfaddd58254f02993c1ae71a463297772d3887053cd228826a23f0faa27d51fb71e296f06107071bd54f9c4ee208dc39fd824f059015186fd83cec58c988a5d920fbae51840db4dd643b0e11461832c20a5d1a86813c84ad89ca72d76ebccd739244312493daab09bcbbefa5eacb0cbce18ad5d21f2562c82c59e5de947dfeff0afcb8e5ada37eff448195e911af1b8ccb3bbc0f0d184108d72cbeecbae2f01e5151eb3a0e19022abafd13a8de290f2c59afaa991a8bc05afe046d19e2802fc73646eb7a1e359d49288dba9cde637829e2c5b2022fdaec0882fc0117467ef4089a708ae2797f69e989b1d00e5dd83473b8657bf2ba18411fff74c6a81b7b14771c2baeeb34608526fb194ce4526fc207d99e93b25d0f6c777b631f402f31369f0aa7ecdda31020c0b74dd5e5d67f01d44af2bde4e7ffdf32f48cc731adf08fe3d5a8eb6dd9ed2115b917474bdfdbce2db780a6f62115dc54948fc32ddc3e7552614eb8d7d7b1106c3fd239054b03beaf32e3426d2ef7e2815e7527b4ba158c02dffe84a543bf15f6be831d01b7e37907eb13d7e69dd7cb032f1e78ec4d55328ef188a8db606bd608adfa1ea93a7fc690a3ea435d9c7eb59047fd2dc541bfb4ab7969cb994e03a8a283ca1a4a1331489a5c0793aaa7346912ea09d23baca1f8a381f5040e16af01c901ac2f2973e711c8e3eb889b4a007bc5a2e70dd61e70e48034a3713e99a759a6b04233c80a280bfb794b83f57bbea26d4b00dac61a10f2b6674e494004823dacd2872a2fe349e015c9f8bdd042f686fc73590870c0cf0ce6172101baeb852a213c07c5ffb6a70f833a4c65e47a65e9ff61cadf660628f49970347cbad79d2b5b4f8923330287630d00e095d730d2540421170f4f8560355115aa7c8301a6495e22c9a2c38de611ac0854db03f2bf8e7ccc3a973c6513862ee2b98fdcf7acbc14ce2bcb8442588cca624aeed1e4297d6b272450735e6b6a87b273877a38e17b4ee635d0f6254136db74130ccf5dc287e0b24eeef94deafb81fd5803ff1de1db3c52f65b242e26ce733efe010e58ac2864542c1ffcc5b49959f8b207f237b8a0b86e95393eadf9c778182a1be4680a9eaaae7760fa269ea3afa04595f4c970fe854beacc6e5b4d95de3b9915b24de58271f455a7792bd3a1b794c6eed63b9bad7d2ec1ffdb413fc8aafaf8c11914310e94d5ba3b9673ed77301cf37e65463293cc612fcacd98c15174f06e45e57f135e7573b2ab7a1feee7821f2f0aee3aa35338653c7f17b85f5acf321f48b0e576109b4158efaa1d9fe610fe569e7a639a0ebe0b38facd8e252549d09140b99e175ffc8aba082ddb66c759698d66367c1dc7cc45f03384a3a55eb5e9cc8e681c7824a1de443cd0069ae890a53979b08b4bc07235e7f2aec993f6e02d4e5c645e7e646c2cb1b8322f490a0fe0001a147010700fd05b1ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a00000000000000030000000005a995c05900ea5104b145117c6a48520742fb05d48a16a75ba1cc9f67a5eb90284746d400061b100003a52e309818a26a2737bed449f9310171509591cfd7daeb62216f633b1c9cc14faf2adceebf7dbfcf1fd7138442320ff99c54a4f316fc98b4cd7710c1a279bad88058e6795cb09eb288e07630e9829c6f93b851194363e479a1431ca2cabc4f4c092034cce4ac860533e0cd90b8b5a393941c2f7308ec4068b74c2d245cd6c5cce45a3ee16fc36dc2957134feec5403711489ef56382bbd69e98478323e5b1377564859b67effcec1f164fd82d452863ff2cfbe9e0bfd17b9dcdee5064216fe99df14f3676225321816898b971aa3710e4f954876212e7d446479eb0b5105eb57b90c0f0301d45010f627dc11a67d3289dd6463bb389264b52603e5cee4416f3a1f3d7a2a2da60a0620fc25bb8a01613920049e1a287d0e639462838cbcc1938ab4f441c319324eac70fdea294960c0918ea3361e4325e8da5728d025a3538686e0574e9f72c8745978af00a70f9c7b6610a87e47a720fded32f9af507772e0cb08c64b768d77378c18e3743a27d765aaad9a35cf7bac9e9af2ca5d2a1816d1bd8bba944539e9d85be8b254c82e8ce86333cbf82c72bd5a401eb8fd2e71d84c87f80c65798833f8d4faf5cbf3623f520c6e7c9badd474f6d582f5467e40de58204eb719663ef9f694325df4d7f7a5ef797d675eed2480c7c4f3158f4caeb11b9d0f05b3766c00570274c63475fa6c0f2420b2a19cf289a174961184a4a8dd02ba18d26b845d6960e22689bd790aee45f20dde4ec5156c84ac02c2553d9a5a6bf25a93da90b599420a98b30326509d4661bfb46d9e2f43a1fc97388313f7f3ab61615a728f27ffdaf43c7c88542672983902d79a20fcce03240d3795b58a66c720e1b4a0a9cfc04f368edef33d62eea9e1123ca4a5cb99792d655ad8c44604c42cdb3953335db24fe154b20559774b24ace3d35c2bf518640a5d8b4cd30aa0148e868296d6c9ea050e712f57f940f2693099671038042bfcb145712939129004148c59f79af7a9b7b3c8bf6be4d3a1719629195b7398841df73aee12bb332f132d96298003cee791a60b6c10d0708b3ae46944dd990437ccbe9a37827688150e8aeab68218cca38e8d03ce9ed65ae4831797f45d49ff6e56d72159601e9c1b6766e4feda60d957542eb5df5ab1ef7167924f4ef1d7b7661e9a4c9ee7d7df58d7a2cbc431addfc0a41dfc6e2425b7ab98632e366f5ecaf6ffdc1d73ed47269d1f6fc7c335b7c1f3206ece603016357018ccdf267267548adce056fa2a05c877660019742e10872ea2e24c0b9b853a099f38ae8221b314730bf070cd37859418cf26425d499207d75d5022297d634e05bb4ad6462b44e11a8b9dd6516468d7b4686027b50319fe5676abc4cd25477aabc47f7cf0c1b64c26001f8feac705fbc009d1164cd4811179fda86ae2b93999725b484338d15fbc1c93aca46853a69c6089879e1be8ee2716e0bd3f530de7347ef09443e0a2502ec396da67b13171d0ffddd8b21dc1dafe157c933e596cc52fe1ebe663274a0e058b6b7465280bc27e2c0fb1ccecf7289ad584efef96b0804a1af10b182f788f1b324eb769333af99c7a8f28f6f446032cb387a480354f06bca5960df22b59061946740168d1a383cedb7d7cbccbc3fcff91836ed87d4f3366a6ae7a030b49d37d43a5cee8a85d7443720df1bd57c954d57d5ffdfc9bfadae3754d7ef68830bbd2354069364e99c95d0f72b84fe2d880427c7e41435d3aaa47b2ed28eabe332af0221997386aac37021bcbbd78b8cb12b9f439d8154bf17b5f0252c1fd3c884eb0c723abde233539bf94bef906bc260bc3496cd62c29e82bdd831ea9222af8f8fa28e295409bc4bc7532946e02e6b046857ab8a7d97714fcaf64b1e45ddd1127b678499005bedc4a00a4134b11b2c934e7ecbb164afd9a5964a2def87ffa03bdfe0001a1470107000100000000000000000000000002d87e81abde4e3ec8820378dfed9b23de246ea99520120d1f44fb5f08c6e25cc20a5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f00000000002200204187b4de4877993f8eaefba883cd1b5ced129cbf65b32369ce94278ba373bea000000000061a8000002a000000000200000000000000070001ff000000000000000000002710000000000bebc200000000002920908024ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a000000002b40420f00000000002200204187b4de4877993f8eaefba883cd1b5ced129cbf65b32369ce94278ba373bea047522102d87e81abde4e3ec8820378dfed9b23de246ea99520120d1f44fb5f08c6e25cc2210321b0573b4983b21fae946f9d5813b49ba551ae937e04453d853ed2b17ebc0e1f52aea80200000001ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a000000000036a2ab8003b0ad0100000000002200201991d1de5f3d395a979d0fa4ff88d3e7226f66e6961c6641d75e6be850a74f9840ea020000000000220020c9abd3d347f1d3f9b06cf38adb0e466d30ac14531d66ee4a58468da201b1744250870a0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ab6789203cc33db437cdc8a155697a5623fc8b95dcdbbe39ae6b8208a87d3d9401b9cd583fd069d733aae4c3176064e906688968d6db67235901f66e057370398bbf03f600011124b25163410a7498125e4b9b7f8b7d9de5058aa17c33b434d20f02bcafb73351ee000000002bb0ad0100000000002200201991d1de5f3d395a979d0fa4ff88d3e7226f66e6961c6641d75e6be850a74f988b76a9149ffa573e4ee636e634f219fadf8c6b12d80486568763ac672103aee5bce7fc5e18ba065474648005ea3e9f38cf970384dc5f8d2e4c03cb516f957c8201208763a9145d87298de5ed29a001ff24c4abb3d3d091682eed88527c210227923219b11f31e65eda4b48e5e1332c64b32855908f3174b3e1988046e40a8152ae677503b01a06b175ac68685e0200000001b25163410a7498125e4b9b7f8b7d9de5058aa17c33b434d20f02bcafb73351ee000000000000000000013a92010000000000220020c9abd3d347f1d3f9b06cf38adb0e466d30ac14531d66ee4a58468da201b174420000000014d153e87b1c076e0ec079c69f724ad514e470cc9497403c0e7581d20de53df4000000000000000000061ab0bb4dd41c527120c21c93f2fb47d1c2ff8cd3a1c02d7ba84ee75c722ecdab68736c29f05f36d2a459b88465138f324a4f7e5d6d1b68cdb0424b0d5a1211c5ccfd00000000000000070001000000000000000000000027100000000029209080000000000bebc2005d845aa65434b27605eb87c7f6140341ea62830069e5f920fadee8fbd04cd9a3039e9c9873bbd72749e8e7c6e20113eb165f1311c54aafcfbcc4d17406782919e0ffe2ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a464775ee59a1cf732abfcfdf379802dcd35ca156552bd4bcec993fd4f973c8923036941dad06a84c462934b49129149adc5ec9966cbc15e9b1d45dcca22dc285000282a40d98f56378d7b5317259cebc9ee7bf988978cc559c364abe11b02d5bc23279e0ec9e9d150f45277c8eb15f7c06e2cb78b61c693a6930ac833fcac743d2f57484d9079265129578deab29ec880838c33533a6b6ccf8e2809589ec102cbfb054f911ce68034c4a85af246805d2a486bb5651046658b9f4d8b374202fb3d12b00000000000000080002000000000000000000ff00000000000000030000271000000000292090800000000006422c409e3a670043626160e4168680f6c9ed54f3e4a36a6431ae8b033e952733d8df4203f6df89806aac70525348c9992304f87c6696af5b79a873babebc98647b5c8dc400000000000000000000070003003e0000fffffffffffc008303fe69a5668f4ab7ab30ba3351c2cee3ea694ff845fd1597d2021bea85133e5400fc0003ffffffffffe80107be162987941764fd52d41bc68e8bc2608490344f5262a6ba73a1cef4cf96635802000007ffffffffffc80101a7d494b974c327cd2c2a51582cf428c0085ea2c28e84256db34e90e1903b8f5c0003ffffffffffe40001000000000000000300038aa4d004f59a49509c75b39e5724f9fe0000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e30000000000ffa80200000001ecd251e9bcb26a5a0414297b97adc533034f14ce81c0ff05c2fb4b1b3360976a000000000036a2ab8003b0ad010000000000220020a913e317167390473b4df76d339637dd116746e0f8a3014a15e028b18b5edf8d40ea020000000000160014761879f7b274ce995f87150a02e75cc0c037e8e350870a000000000022002062c4769ddc7b9e379dade5ad80b8100d540b0df91868e454fe1a5d4195351a1dab67892000000124a3d94cd0fbe8defa20f9e569008362ea410314f6c787eb0576b23454a65a845d00000000ff2324a3d94cd0fbe8defa20f9e569008362ea410314f6c787eb0576b23454a65a845d000000002bb0ad010000000000220020a913e317167390473b4df76d339637dd116746e0f8a3014a15e028b18b5edf8d8576a914acc2a5ecf25b533fee601c4e694c06c53d32e0268763ac6721030afb6f8fba31f5a4394cd3a1145268354dc9464f3253d311333cfc7591c4cb697c820120876475527c21032dd2755000aba32a5442d6d6bf0488d997a1988c534092ac6bf244c7c864ffbf52ae67a9145d87298de5ed29a001ff24c4abb3d3d091682eed88ac6868fd014402000000000101a3d94cd0fbe8defa20f9e569008362ea410314f6c787eb0576b23454a65a845d000000000000000000016297010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3034730440220337e642e9d285ba15a93d070e8a7d5e598de5f90799cf64ec1b4aa5906ccfb9002204756e51351e1afadb73af8072b5b9ae7c9f3a777132d1faeea8240b5fdd6810101203624f17e10a0ebcc71b4ce5eb541f3e68c73ffddda7294f38f411cd19f24aaed8576a914acc2a5ecf25b533fee601c4e694c06c53d32e0268763ac6721030afb6f8fba31f5a4394cd3a1145268354dc9464f3253d311333cfc7591c4cb697c820120876475527c21032dd2755000aba32a5442d6d6bf0488d997a1988c534092ac6bf244c7c864ffbf52ae67a9145d87298de5ed29a001ff24c4abb3d3d091682eed88ac68680000000014d153e87b1c076e0ec079c69f724ad514e470cc9497403c0e7581d20de53df4000000000000000000061ab00000000000000000".bits).require.value.asInstanceOf[DATA_CLOSING] assert(closing2.remoteCommitPublished.nonEmpty) val rcp = closing2.remoteCommitPublished.get - assert(rcp.claimHtlcTxs.size == 1) - val claimHtlcSuccessTxs1 = rcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcSuccessTx) => tx }.toSeq - assert(claimHtlcSuccessTxs1.size == 1) - assert(claimHtlcSuccessTxs1.head.htlcId == 0) - assert(claimHtlcSuccessTxs1.head.paymentHash == ByteVector32(hex"14d153e87b1c076e0ec079c69f724ad514e470cc9497403c0e7581d20de53df4")) - assert(claimHtlcSuccessTxs1.head.htlcExpiry == CltvExpiry(400048)) + assert(rcp.outgoingHtlcs.isEmpty) + assert(rcp.incomingHtlcs.values.toSeq == Seq(0)) val closing3 = channelDataCodec.decode(hex"001101b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b010102100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000947634c4314cc9859632bb0db04f4aa6194fa4c7b5a24f94ed30197557e232fcb80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c000028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000001808020a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e0351cda699fd1471468b07360b09aa63d9f0b7066030c053ffc17c708ebc5fa5a7028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b120379bf217b5c2ea4602f186fb3abff38198ce69a2c48215a0f53bac42da7b8eb3e02595899924a395a4af0987f056456ee1a4133c0183227e72af934d1a2f424a18b0000001408000000000000000000000000001008028a5982000000014a0082b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b0000000000000000e559091e6b69e409919a0956d1fe2e786262446548a920b203fa3a7ce51146ae0001fd05b30080b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b00000000000000030000000005a995c0028fd04843d9cea2a6053932960308a3400dd79873bbdd9b7bf5325ad5eaa2e100061aa00003883bc264072d01155f2ef9b27242fe84cfd26949e8832d38839d5a3924647c045d901a42ff93b586fbb75dc27ea987d2419f08db8fa03bbee2f76ecab9e06dd09374be37e8be3884bbcbe1eb61b2f9087bce50586ebe8401a825f6547e0255b89200b9ec3944798ab66600bd964e21ecb9e7294b10365fe9bff532244e6dc2ef42a90b84bf2a4a69c6160c5ae3b540a49e32b85c059f9d5441f99789fa381d61ce56e276e920ba7e84cff9c15555378d2ee0d4cc1b473f666bfa5ca27289628f04a2470b26934c4ed3b637abff89afcf669b90872bb4dac908d9662f9cf4c6ae0860cdb3d3c98aa6aeb1a6bb078307a8c5f51162165f12ae47540732d04f4c7736bec7644b6f1d542dcd66db6c51a9a3e3b046dfccff15db283df3f7784f06f49eb79bb2b9bd27e87b62ece40f2522e2119799cad60d3491dc470fa8389c4a0ba203cb3cf703f1fc8520d7d24f2964d531e506c98b436303b98615f6fd111cd3eb37276935eb900cb0104a741f4b44de3371eb578ac59314790362426c76838358d352940302d03d9aecc50918dd81748558cff571f5db3514a955a28bb570daa047eb2868d787b84b5707979aba1083e0d22d06890a6fa79f6b0b56f86da80ae79cb9c45641aaca16d57cedca81841d780b8d684f0f9347edccee172a8710248920981195fec9378111293193d6fccb94f438a59ec272130d644c9de7f1d5c1e59bf9a41782ac5a2947f7eba0f6fdf9013a7254c6bbc0c325d50cc69fefbcd83dcd009f7bc26d28aaa622b1f441a9fe95782e5d546fd7746949fe21fa431b4a49e93a9c6e501725977f94f1d8e06e127f15a1ee1beafd6e4a691d1f384315e1060d7c06411f207571128d46dc571d3ffc9afb0c79e2502e5ed02e13c59cd099ad1eeb255af86a64da908d91702ba62739c0dd2df4abe7b0dff71ef24be7c09bf17b280a727a9787e2c8ea4cdb85797cdc2b538ce54791349d61f0b40d134ec667cb3c53d13697af5165ffcc05c7aa847a51986d1beeda7b5d1b644f60217939e35f989fe10ce015253895845ff881472b3f477a78f90998e93e38f2b3c42dfabb08b4d2c1a3f8cd0a969334715ee1b9021a545d9fbddded126b1b93edd068ef25eb7ed73d29249fffeafbcb66badd559422c04120a90001608f712fd55b993bc12f5a540b87dbe1960d388cb8c1e691a7a6990160669532548fe1ed9baa406864d30796db7704030edda83031994a21bcaa2615cf81d5cfdc0dce02bb8771c0d161c17ddf8279407872a7db2374a3a5b159a082a582faa27681215ec63902807a38e48c5fb4973e9fa553b9fb3019971be20e1ea590575d4b595279f791dbdef338a28872e0e06a43ca2d7b74d9474429b2c8069849e626141b87fbbad950f1f86a30f6f24b92fe73f76b4a065ef881ebc168085c5950650b82fb6c92dbda1ba8002b995d35a916e2451e2008385defe1433481dda3d60ab43fbe847233522918906dad9169a45eb8799cf9fb5e9718684a01b30c65b3285c9ff5e5408346822d9a2898e2f717cd3e2f4056af575e4657dbe78b9549b28242034adcec1e3fa1b321997d735ffaf20b1cc9332b7af6f664225c16ae6debeb1d07a17549e5cdabba902f1caf74bb4f2ea6085f219f9edc18eaf2a0193009712d1deaa3ace9a8c05eeeb45aaae5853361d105aa20807647e4404b8e7516971fb0f45cb7fb98b5fc62e39fe13d9243d94cf7fe0abd8be94fed886edc4796d00c37c45a68efc08c19fd172914b5ccce42260d21f99b9ae5f20afc5805b0c65ef52c1678e479ed139076aec1bdca9df21a3e6730a16129b70ebb372a236e1ddac607770bc4033180dedc15bce4afc211644c9c3b5ceca5627b5b3122987eb5397067783eb45624da4d5a189cd340c0d33c7ade63a64a74ee2eec01a5b1fe0001a14701070000000000000000000000000000000400000000000000010002fffd05b1b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b000000000000000000000000068e77806fdf03f953dc6040f0ba2f511c65f70e6d6a32d050db40f8400419fe70ffa0e500061ac0000323fa7a843bd1b7b5e80a2a0401eba107865debb4a99a43680fbfb043046dda23faf07641c4f8ac14552d6e43089d8bd46069aed363c51b0d680e3794a950fbcecc9605d139e9790281319bd53785597ac05e9b545542b11a0f70623e088c3e1565632fdb2a7f51194de9c7094595943ba196d30d5ee1b862b9a306bde44e727297f14289826439541a605ed07c98db42a07db9092ca8c0e127e5b19d6c013386a6e4ec84456e543e9bc4be008e81b6b50c8fb7a4264847f65c0dcc121aea93378ad1d7ceefb9ff87ee1152d11f2b5c17222087268230b2e649290bfda463a41230ffc841d0d06ba7ceed825030fbed28b2519bbf72bf082c83e0abbf2748c4ab786dd4c18f83aeec7718d1c13bb7568d3f3b530acca249258dd64d2fb142e31c0b3ffde72233d6a86b1e10c5357289c72b1bd2ff0565c44aa143b877f2e7144a6c8032b512af112533947b66925effde588f21072534dd1d2efdc40efbcb67eaec37e2f26e590b269c1c8e93337b1d36b3516f43f993a0e5aea2dd6c8421458c41cc262ecf7007b5cf8047404f816b0ed63f6a4c737b78ce0bb74fae381edf3ffbf6fbb3de91cf4922d99d48418f2c5e22f9a187e74c81f953b923d9d16af865f380d62ebd31916ce265393fb6881372e34ee04bc566811320f50ca488fdf480b453f4240447704a617cff060bd32940dfd130eb77513839d2b8de0d48a0d58a29088202bca3e3d7ff38ca820a20f157624169abd68a36f692040032b68f67e500c4d84039a53365e857a0cde0218f8caf331483e904848a3379569e61a01d470547e0b376a6ce824b7c84e9d2ae57fc2e96eb794cdb7317c7f5d978c0f3c1c3e74afd89d234b7cecdd571c5a54c756c14e8d37e5bc2a4f6430ecd80288502b6250be70d52b40074dd0e6284184ad0af4e8162f7d356d76afd74120edf0843ba86f03a0b43228d17ba2ac79b7953f178a1af17f3949fc6020bebcc48503f51e81d2e8b0a2c15dfa89316d9a116d4808f3f45dc75c7d145d2f10d6a5490483b7c92084d6747aca140a4ddca23d25c64cbb74e4d77476fbadfb038ac7d228ab711baadde3b2291ec5b20bafb2f2c2679cf482a2d2725edfaa70acb3f91d68cb532d09bb13fac514079cf60c19f4564be9ad350fa5e34d9f35a40ac32e9838886727065da0b8613000efe8db4ebc5744b9d9deef00747340f40dc2d977a47bbd4ce40dacf9aaa6f798f430a1aa9920f0c2488a48717e6276c6d140577d2ad050d163402ecc04d0008cebe1630ee70d389980edc3ef68c251fa5b075ddc613b96875a77c5984e34163b1e15a86068ec0b3f7fd2f482a57ff6b465837661ec3148d860918515b25f19535db6c0770c05738651ab8c453cbb066c45e4abab96381e8fd0609d8564d0345490c1533d58e58e54e27518d10df186ef6f85fbb3ed0fcde0e55ce19ad11b9b87c837e1e7a5f0834a637c02a20f434b6e52aa6cb8ab5d7f495489ffad8848e7041e6464e4b4bbbce34603ab447e0ee9b1e0a30f5fa858559e4a792b546ea29777bf393231c3e40aa207c13297fad343a2c73959417837f2268bb5d849f158e53ac00261742d91e429e5b330ff284f9b61f3952595d45ed187a7a4946f7664a14b44e5a5c21f9fe5f54e283cb4511c9e26fbf30f40f84c2c67a791fe19a02f6abd4253bf087d93b9a6f73e6a330c8f5848b644907d50fac0eded6e389f05038f6db634e9ac155b38621416254588fc15579163fef54779d139c192615618f12c72dbee711fde597bf6af98e5846241fbe9d572b1cf8eb9afb0e3c4b690da5c20d7e4362c1a69979ce9ef9b4ee7630a711879c03e9ed9d7fe35a02910dc5cadba8d4cb60ff1b4aaf51904ca4ec8e2aeef0e14a42ebdf8f24d73b2ce445f57710a15ee2bd2b7494a775ab9c03dbe9fe0001a147010700fd05b1b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b00000000000000030000000005a995c0028fd04843d9cea2a6053932960308a3400dd79873bbdd9b7bf5325ad5eaa2e100061aa00003883bc264072d01155f2ef9b27242fe84cfd26949e8832d38839d5a3924647c045d901a42ff93b586fbb75dc27ea987d2419f08db8fa03bbee2f76ecab9e06dd09374be37e8be3884bbcbe1eb61b2f9087bce50586ebe8401a825f6547e0255b89200b9ec3944798ab66600bd964e21ecb9e7294b10365fe9bff532244e6dc2ef42a90b84bf2a4a69c6160c5ae3b540a49e32b85c059f9d5441f99789fa381d61ce56e276e920ba7e84cff9c15555378d2ee0d4cc1b473f666bfa5ca27289628f04a2470b26934c4ed3b637abff89afcf669b90872bb4dac908d9662f9cf4c6ae0860cdb3d3c98aa6aeb1a6bb078307a8c5f51162165f12ae47540732d04f4c7736bec7644b6f1d542dcd66db6c51a9a3e3b046dfccff15db283df3f7784f06f49eb79bb2b9bd27e87b62ece40f2522e2119799cad60d3491dc470fa8389c4a0ba203cb3cf703f1fc8520d7d24f2964d531e506c98b436303b98615f6fd111cd3eb37276935eb900cb0104a741f4b44de3371eb578ac59314790362426c76838358d352940302d03d9aecc50918dd81748558cff571f5db3514a955a28bb570daa047eb2868d787b84b5707979aba1083e0d22d06890a6fa79f6b0b56f86da80ae79cb9c45641aaca16d57cedca81841d780b8d684f0f9347edccee172a8710248920981195fec9378111293193d6fccb94f438a59ec272130d644c9de7f1d5c1e59bf9a41782ac5a2947f7eba0f6fdf9013a7254c6bbc0c325d50cc69fefbcd83dcd009f7bc26d28aaa622b1f441a9fe95782e5d546fd7746949fe21fa431b4a49e93a9c6e501725977f94f1d8e06e127f15a1ee1beafd6e4a691d1f384315e1060d7c06411f207571128d46dc571d3ffc9afb0c79e2502e5ed02e13c59cd099ad1eeb255af86a64da908d91702ba62739c0dd2df4abe7b0dff71ef24be7c09bf17b280a727a9787e2c8ea4cdb85797cdc2b538ce54791349d61f0b40d134ec667cb3c53d13697af5165ffcc05c7aa847a51986d1beeda7b5d1b644f60217939e35f989fe10ce015253895845ff881472b3f477a78f90998e93e38f2b3c42dfabb08b4d2c1a3f8cd0a969334715ee1b9021a545d9fbddded126b1b93edd068ef25eb7ed73d29249fffeafbcb66badd559422c04120a90001608f712fd55b993bc12f5a540b87dbe1960d388cb8c1e691a7a6990160669532548fe1ed9baa406864d30796db7704030edda83031994a21bcaa2615cf81d5cfdc0dce02bb8771c0d161c17ddf8279407872a7db2374a3a5b159a082a582faa27681215ec63902807a38e48c5fb4973e9fa553b9fb3019971be20e1ea590575d4b595279f791dbdef338a28872e0e06a43ca2d7b74d9474429b2c8069849e626141b87fbbad950f1f86a30f6f24b92fe73f76b4a065ef881ebc168085c5950650b82fb6c92dbda1ba8002b995d35a916e2451e2008385defe1433481dda3d60ab43fbe847233522918906dad9169a45eb8799cf9fb5e9718684a01b30c65b3285c9ff5e5408346822d9a2898e2f717cd3e2f4056af575e4657dbe78b9549b28242034adcec1e3fa1b321997d735ffaf20b1cc9332b7af6f664225c16ae6debeb1d07a17549e5cdabba902f1caf74bb4f2ea6085f219f9edc18eaf2a0193009712d1deaa3ace9a8c05eeeb45aaae5853361d105aa20807647e4404b8e7516971fb0f45cb7fb98b5fc62e39fe13d9243d94cf7fe0abd8be94fed886edc4796d00c37c45a68efc08c19fd172914b5ccce42260d21f99b9ae5f20afc5805b0c65ef52c1678e479ed139076aec1bdca9df21a3e6730a16129b70ebb372a236e1ddac607770bc4033180dedc15bce4afc211644c9c3b5ceca5627b5b3122987eb5397067783eb45624da4d5a189cd340c0d33c7ade63a64a74ee2eec01a5b1fe0001a1470107000100000000000000000000000002ff4c609c1b9a4b70b8eaafedf018a1cb24a83918bdd1b87fde8c1905c84f450f0a5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020224b8c58b3fa0020f800829615232bf183dc41b13603a67cce9b94dd28f8bb2800000000061a8000002a000000000200000000000000070001ff000000000000000000002710000000000bebc200000000002920908024b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b000000002b40420f0000000000220020224b8c58b3fa0020f800829615232bf183dc41b13603a67cce9b94dd28f8bb2847522102ff4c609c1b9a4b70b8eaafedf018a1cb24a83918bdd1b87fde8c1905c84f450f2103d68951fa91802e65a951917f58bf547a615875b6553251fe91c940f98b62030552aea80200000001b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b000000000036a2ab8003b0ad0100000000002200202faa5d052a90a5e8ef750efc0f43205cb84bfbe8d0a97c8ebbb0fd7d30be8dab40ea020000000000220020822d615f74a3f1da36d31bad7fbc39920436895f15137aaefa767f5e698af36d50870a0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ab6789200bb8316328924ab4641b821b72c9fb1cf9b51311c1e3ffafe291756ea403081e130022dc33eb8a765d0909454ba6bf3a5a74ff5467a1d017e63758eabf91b4ad000111243e453164b4e4944314473df2cd44617c007a69fe00819de3a71a3f39605af67d000000002bb0ad0100000000002200202faa5d052a90a5e8ef750efc0f43205cb84bfbe8d0a97c8ebbb0fd7d30be8dab8b76a9144b261689dafc1227a18f487a7929419c6591d9c58763ac672103c50b9d3b40403c354d73948670978f008a3a671c7f6376f6fc7fc3d0e3e1cdaf7c8201208763a91423a2a6f93c2a944f662595191a7829411630ea9788527c21026b7bfaa3fff831c853e94ecce416fb41c23e8fa74b7f408cfddde01b7cdb798852ae677503c01a06b175ac68685e02000000013e453164b4e4944314473df2cd44617c007a69fe00819de3a71a3f39605af67d000000000000000000013a92010000000000220020822d615f74a3f1da36d31bad7fbc39920436895f15137aaefa767f5e698af36d000000006fdf03f953dc6040f0ba2f511c65f70e6d6a32d050db40f8400419fe70ffa0e5000000000000000000061ac071e938061e81f5ecab7725d70a54e3d6036e2fa22e8b2b0c771d32dfe01707b14c942ce6aa6291f0ab8fce4aee5296cf7b9fdcfdca7ec2b7262c219b020004c400000000000000070001000000000000000000000027100000000029209080000000000bebc200ab17ef69024716713fd8a2433cee1a80230b53d11dbe3802782578017df3a85f03e094d392a23dfd51e499b1d88405ca1234d80dca3e1fe4d6a257956c0ab29185ffe2b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589bdbf802f84854fd0aa4c57db7076190a29ec4a62283568e4d2364216906b6b804457b13b58b8a1106a2dab6496985e64a5829c019d2421d84477e903eab73482800023328bbd3c85d4f30b98fb9ef799f62aaeafca3318fa091fc294fb5ae39ea3a1e1b67960565fa4239d121713074804c5912122ea114e2974a0494592ab33e02c7ad9f3311006e314dc264ce0a8ba93f78d2633bdab60b8cf23208ecaa0aa84f2176291b501e3c0dd50afbc895c01b2a89959773b240ee2962fd8a3694771b369400000000000000080002000000000000000000ff00000000000000030000271000000000292090800000000006422c40ef2c4ef07d34051e1941965e9a420f75735208791058df88162f1e8db90680d0032de5e77d76d525909df1773cbfa29851a9b499bbbc697149060cb390a854fabb00000000000000000000070003003e0000fffffffffffc0081edb904be4493b4bf33014f8c55d7dfc0a0eb6ba1b5c74503731bdccff2fe825c00fc0003ffffffffffe801007810b0c8a83d397e50a0272e9f2457c003f8eb1d2c84b517e49de9df668df10802000007ffffffffffc80107b28bf2a478a86ac898e1edfefa85c5bc5b57100a38a5bc714fab6ddaa57b68f40003ffffffffffe40001000000000000000300031e47851bb56246b4b54c0ca9b7825dce0000061a80160014761879f7b274ce995f87150a02e75cc0c037e8e3000000000000ffd30200000001b848165af91647fd18dc3da2e49972e4c6a76d485b1eafdf4e1ddd393107589b000000000036a2ab80047070010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e31873010000000000220020ebb090e27885e133dcc4c2c50048a5ecad13ef3332340fb4d09933278f0cacfcb0ad0100000000002200202fa94c3bd8b31fb7dd6205f9db6b81066e81c46b8918ef7a02bed3438ab901f650870a0000000000220020bbe62879855ea85e05c2513fd39a76516d448f71269fe74dce02fd0988b3c5ada467892000000224d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef02000000ff2324d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef020000002bb0ad0100000000002200202fa94c3bd8b31fb7dd6205f9db6b81066e81c46b8918ef7a02bed3438ab901f68576a9140bb693d6b90f314c152b1d2c2ffc088f9542b3ba8763ac672102df93807a4ea0764a5fd9fab2bc573f5a8cee592293615a069c33ba1cc6f329467c820120876475527c2102dbd2b371b31656230bedfcafbea4df02b1f9f2d121c596bd984062b589aa27a452ae67a91423a2a6f93c2a944f662595191a7829411630ea9788ac6868fd014402000000000101d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef020000000000000000016297010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30347304402201b4125bc11ce9f463f659e0058421e9f41e9d4d03e0c3776bc9d5521656382090220276fd4de9215b7f6a329c9af0f254b66181fd9cee38dd1750ae3f9dc29c338e10120e559091e6b69e409919a0956d1fe2e786262446548a920b203fa3a7ce51146ae8576a9140bb693d6b90f314c152b1d2c2ffc088f9542b3ba8763ac672102df93807a4ea0764a5fd9fab2bc573f5a8cee592293615a069c33ba1cc6f329467c820120876475527c2102dbd2b371b31656230bedfcafbea4df02b1f9f2d121c596bd984062b589aa27a452ae67a91423a2a6f93c2a944f662595191a7829411630ea9788ac6868000000006fdf03f953dc6040f0ba2f511c65f70e6d6a32d050db40f8400419fe70ffa0e5000000000000000000061ac024d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef01000000ff2224d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef010000002b1873010000000000220020ebb090e27885e133dcc4c2c50048a5ecad13ef3332340fb4d09933278f0cacfc8b76a9140bb693d6b90f314c152b1d2c2ffc088f9542b3ba8763ac672102df93807a4ea0764a5fd9fab2bc573f5a8cee592293615a069c33ba1cc6f329467c8201208763a91430fc0d20cea6c07069115f75bd755115dd04808388527c2102dbd2b371b31656230bedfcafbea4df02b1f9f2d121c596bd984062b589aa27a452ae677503a01a06b175ac6868fd012a02000000000101d08006b98d1e2f1688df581079085273750f429a5e9641191e05347df04e2cef01000000000000000001ce5d010000000000160014761879f7b274ce995f87150a02e75cc0c037e8e30347304402200109d51a3db516ba023039618a80c68e2791c0cee74f4af0f52bc0e695fa05200220792a459e57eee58efcf4732323e934332a269d4336a8fa182e488ee54d01ed2401008b76a9140bb693d6b90f314c152b1d2c2ffc088f9542b3ba8763ac672102df93807a4ea0764a5fd9fab2bc573f5a8cee592293615a069c33ba1cc6f329467c8201208763a91430fc0d20cea6c07069115f75bd755115dd04808388527c2102dbd2b371b31656230bedfcafbea4df02b1f9f2d121c596bd984062b589aa27a452ae677503a01a06b175ac6868a01a0600000000000000000300061aa000000000000000".bits).require.value.asInstanceOf[DATA_CLOSING] assert(closing3.nextRemoteCommitPublished.nonEmpty) val nrcp = closing3.nextRemoteCommitPublished.get - assert(nrcp.claimHtlcTxs.size == 2) - val claimHtlcSuccessTxs2 = nrcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcSuccessTx) => tx }.toSeq - assert(claimHtlcSuccessTxs2.size == 1) - assert(claimHtlcSuccessTxs2.head.htlcId == 0) - assert(claimHtlcSuccessTxs2.head.paymentHash == ByteVector32(hex"6fdf03f953dc6040f0ba2f511c65f70e6d6a32d050db40f8400419fe70ffa0e5")) - assert(claimHtlcSuccessTxs2.head.htlcExpiry == CltvExpiry(400064)) - val claimHtlcTimeoutTxs2 = nrcp.claimHtlcTxs.values.collect { case Some(tx: ClaimHtlcTimeoutTx) => tx }.toSeq - assert(claimHtlcTimeoutTxs2.size == 1) - assert(claimHtlcTimeoutTxs2.head.htlcId == 3) - assert(claimHtlcTimeoutTxs2.head.paymentHash == ByteVector32(hex"028fd04843d9cea2a6053932960308a3400dd79873bbdd9b7bf5325ad5eaa2e1")) - assert(claimHtlcTimeoutTxs2.head.htlcExpiry == CltvExpiry(400032)) + assert(nrcp.incomingHtlcs.values.toSeq == Seq(0)) + assert(nrcp.outgoingHtlcs.values.toSeq == Seq(3)) } }