From 64b81e1455522accabd78596c64539c65c9858a2 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 3 Jun 2025 11:51:14 +0200 Subject: [PATCH 1/5] Remove non-final transactions from `XxxCommitPublished` When a channel was force-closed, we previously stored partially created closing transactions for each output of the commitment transaction. In some cases those transactions didn't include any on-chain fee (HTLC txs with 0-fee anchors) and weren't signed. The fee is set in the publisher actors which then sign those transactions. This was an issue because we used those partial transactions as if they were the final transaction that would eventually confirm: this made it look like RBF wasn't an option to callers, and was thus easy to misuse. We now change our data model to only store the outputs of the commit tx that we may spend, without any actual spending transaction. We only store spending transactions once they are confirmed, at which point they can safely be used as closing transactions by callers such as our balance checks. This lets us get rid of some codec migration code related to closing transactions, and is more future-proof because we now don't need to encode closing transactions and can thus change their format easily. This also reduces the size of our channel state during force-close. --- .../acinq/eclair/balance/CheckBalance.scala | 32 ++- .../fr/acinq/eclair/channel/ChannelData.scala | 135 +++++----- .../fr/acinq/eclair/channel/Helpers.scala | 162 +++++++----- .../fr/acinq/eclair/channel/fsm/Channel.scala | 99 +++---- .../eclair/channel/fsm/ErrorHandlers.scala | 65 +++-- .../eclair/transactions/Transactions.scala | 40 +-- .../channel/version0/ChannelTypes0.scala | 39 ++- .../channel/version2/ChannelCodecs2.scala | 6 +- .../channel/version2/ChannelTypes2.scala | 71 +++++ .../channel/version3/ChannelCodecs3.scala | 13 +- .../channel/version3/ChannelTypes3.scala | 90 ------- .../channel/version4/ChannelCodecs4.scala | 81 ++++-- .../eclair/balance/CheckBalanceSpec.scala | 9 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 158 ++++++------ .../ChannelStateTestsHelperMethods.scala | 32 +-- .../channel/states/e/NormalStateSpec.scala | 43 ++-- .../channel/states/f/ShutdownStateSpec.scala | 8 +- .../channel/states/h/ClosingStateSpec.scala | 242 +++++++++++++----- .../integration/ChannelIntegrationSpec.scala | 8 +- .../payment/PostRestartHtlcCleanerSpec.scala | 9 +- .../internal/channel/ChannelCodecsSpec.scala | 10 +- .../channel/version4/ChannelCodecs4Spec.scala | 43 +--- 22 files changed, 761 insertions(+), 634 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelTypes2.scala 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..e0ed61d74c 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 @@ -69,29 +69,28 @@ 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 additionalHtlcs = lcp.htlcs.map { + case (outpoint, directedHtlcId) => 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) + 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. - 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 + directedHtlcId match { + case _: IncomingHtlcId => htlcAmount + case _: OutgoingHtlcId => 0 sat } } }.sum @@ -102,16 +101,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.htlcs.map { + case (outpoint, OutgoingHtlcId(_)) 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 +120,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 +137,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 +192,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/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 196f5c3497..ca62815b96 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,33 @@ final case class RES_GET_CHANNEL_INFO(nodeId: PublicKey, channelId: ByteVector32 case class ClosingTxProposed(unsignedTx: ClosingTx, localClosingSigned: ClosingSigned) +/** When channels are closing, we map each HTLC output to the corresponding HTLC. */ +sealed trait DirectedHtlcId { def htlcId: Long } +case class IncomingHtlcId(htlcId: Long) extends DirectedHtlcId +case class OutgoingHtlcId(htlcId: Long) extends DirectedHtlcId + +/** + * 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 +343,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], htlcs: Map[OutPoint, DirectedHtlcId], htlcDelayedOutputs: Set[OutPoint], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = htlcs.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], htlcs: Map[OutPoint, DirectedHtlcId], irrevocablySpent: Map[OutPoint, Transaction]) extends CommitPublished { + override val htlcOutputs: Set[OutPoint] = htlcs.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..89b0e1e12a 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,23 @@ 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 htlcs = claimHtlcOutputs(commitmentKeys, commitment) + val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitmentKeys, commitment.params.commitmentFormat).toOption + val spendAnchor = htlcs.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), + htlcs = htlcs.map { case (outpoint, (htlcId, _)) => (outpoint, htlcId) }, + 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, htlcs.values.flatMap(_._2).toSeq) (lcp, txs) } @@ -919,7 +920,7 @@ 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. */ - private def claimHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, Option[HtlcTx]] = { + private def claimHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, Option[HtlcTx])] = { // 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 @@ -937,10 +938,11 @@ object Helpers { 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 -> (IncomingHtlcId(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 +954,36 @@ 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 -> (IncomingHtlcId(txInfo.htlcId), None)) } 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)) - }) + } + Some(txInfo.input.outPoint -> (OutgoingHtlcId(txInfo.htlcId), htlcTx_opt)) }.flatten.toMap } /** 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 +1000,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(htlcs = localCommitPublished.htlcs -- outpoints) } /** @@ -1011,7 +1010,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 +1018,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 { @@ -1039,21 +1047,22 @@ object Helpers { val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) 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 htlcs = claimHtlcOutputs(channelKeys, commitKeys, commitment, remoteCommit, finalScriptPubKey) + val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitKeys, commitment.params.commitmentFormat).toOption + val spendAnchor = htlcs.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), + htlcs = htlcs.map { case (outpoint, (htlcId, _)) => (outpoint, htlcId) }, irrevocablySpent = Map.empty ) - val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcTxs.values.flatten.toSeq) + val txs = SecondStageTransactions(mainTx_opt, anchorTx_opt, htlcs.values.flatMap(_._2).toSeq) (rcp, txs) } @@ -1086,7 +1095,7 @@ 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. */ - private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, Option[ClaimHtlcTx]] = { + private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, 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") @@ -1114,7 +1123,7 @@ object Helpers { 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)) + }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> (OutgoingHtlcId(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,7 +1135,7 @@ 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(remoteCommitTx.tx, outputs, add).map(input => input.outPoint -> (OutgoingHtlcId(add.id), None)) } case IncomingHtlc(add: UpdateAddHtlc) => // We track all outputs that belong to outgoing htlcs. Our peer may or may not have the preimage: if they @@ -1134,17 +1143,17 @@ object Helpers { // 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)) + }.map(claimHtlcTx => claimHtlcTx.input.outPoint -> (IncomingHtlcId(add.id), Some(claimHtlcTx))) }.flatten.toMap } /** 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 +1163,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 +1171,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.htlcs.collect { + case (outpoint, OutgoingHtlcId(id)) if id == htlcId && !htlcsWithPreimage.contains(id) => outpoint + }.toSet + remoteCommitPublished.copy(htlcs = remoteCommitPublished.htlcs -- outpoints) } } @@ -1257,10 +1263,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 +1291,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 +1307,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) + } + } /** @@ -1483,7 +1503,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 @@ -1534,7 +1554,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..34f0a3c345 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() 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..a1cb76ad88 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,21 @@ 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, + htlcs = 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 +439,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..2298c2ecb5 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 { @@ -56,16 +56,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 htlcSuccessOutputs = htlcSuccessTxs.map(tx => tx.txIn.head.outPoint -> IncomingHtlcId(0)).toMap + val htlcTimeoutOutputs = htlcTimeoutTxs.map(tx => tx.txIn.head.outPoint -> OutgoingHtlcId(0)).toMap + val htlcDelayedOutputs = claimHtlcDelayedTxs.map(_.txIn.head.outPoint).toSet + channel.LocalCommitPublished(commitTx, localOutput_opt, anchorOutput_opt = None, htlcSuccessOutputs ++ htlcTimeoutOutputs, htlcDelayedOutputs, irrevocablySpentNew) } } @@ -76,11 +71,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 htlcSuccessOutputs = claimHtlcSuccessTxs.map(tx => tx.txIn.head.outPoint -> OutgoingHtlcId(0)).toMap + val htlcTimeoutOutputs = claimHtlcTimeoutTxs.map(tx => tx.txIn.head.outPoint -> IncomingHtlcId(0)).toMap + channel.RemoteCommitPublished(commitTx, localOutput_opt, anchorOutput_opt = None, htlcSuccessOutputs ++ htlcTimeoutOutputs, irrevocablySpentNew) } } @@ -90,14 +84,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..474788164d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelTypes2.scala @@ -0,0 +1,71 @@ +/* + * 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.channel.{IncomingHtlcId, OutgoingHtlcId} +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), + htlcs = htlcTxs.map { + case (outpoint, Some(htlcTx: HtlcSuccessTx)) => outpoint -> IncomingHtlcId(htlcTx.htlcId) + case (outpoint, Some(htlcTx: HtlcTimeoutTx)) => outpoint -> OutgoingHtlcId(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 -> IncomingHtlcId(0x00ffffffffffffffL) + }, + 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), + htlcs = claimHtlcTxs.map { + case (outpoint, Some(htlcTx: ClaimHtlcSuccessTx)) => outpoint -> OutgoingHtlcId(htlcTx.htlcId) + case (outpoint, Some(htlcTx: ClaimHtlcTimeoutTx)) => outpoint -> IncomingHtlcId(htlcTx.htlcId) + // Similarly to LocalCommitPublished above, it is fine to ignore this case. + case (outpoint, None) => outpoint -> OutgoingHtlcId(0x00ffffffffffffffL) + }, + 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..5a7776fcf5 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,71 @@ private[channel] object ChannelCodecs4 { ("unsignedTx" | closingTxCodec) :: ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] - private val legacyLocalCommitPublishedCodec: Codec[LocalCommitPublished] = ( + private val directedHtlcIdCodec: Codec[DirectedHtlcId] = discriminated[DirectedHtlcId].by(uint8) + .typecase(0x01, uint64overflow.as[IncomingHtlcId]) + .typecase(0x02, uint64overflow.as[OutgoingHtlcId]) + + 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)) :: + ("htlcOutputs" | mapCodec(outPointCodec, directedHtlcIdCodec)) :: + ("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 - private val legacyRevokedCommitPublishedCodec: Codec[RevokedCommitPublished] = ( + val remoteCommitPublishedCodec: Codec[RemoteCommitPublished] = ( + ("commitTx" | txCodec) :: + ("localOutput_opt" | optional(bool8, outPointCodec)) :: + ("anchorOutput_opt" | optional(bool8, outPointCodec)) :: + ("htlcOutputs" | mapCodec(outPointCodec, directedHtlcIdCodec)) :: + ("irrevocablySpent" | spentMapCodec)).as[RemoteCommitPublished] + + 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 +884,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 +896,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 +937,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/balance/CheckBalanceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala index 207e2f69bf..62015c3c63 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 @@ -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,14 @@ 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.htlcs + // Note that this is Alice's commit tx, so incoming and outgoing are inverted. + .collectFirst { case (outpoint, IncomingHtlcId(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/channel/HelpersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/HelpersSpec.scala index 23e6baabd6..0c989cf3a4 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.htlcs.collect { case (_, OutgoingHtlcId(htlcId)) => htlcId }.toSet == htlcTimeoutTxs.map(_.htlcId).toSet) val htlcSuccessTxs = htlcTxs.map(_.txInfo).collect { case tx: HtlcSuccessTx => tx } assert(htlcSuccessTxs.length == 1) + assert(lcp.htlcs.collect { case (_, IncomingHtlcId(htlcId)) => htlcId }.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.htlcs.collect { case (_, IncomingHtlcId(htlcId)) => htlcId }.toSet == claimHtlcTimeoutTxs.map(_.htlcId).toSet) val claimHtlcSuccessTxs = claimHtlcTxs.map(_.tx.txInfo).collect { case tx: ClaimHtlcSuccessTx => tx } assert(claimHtlcSuccessTxs.length == 1) + assert(rcp.htlcs.collect { case (_, OutgoingHtlcId(htlcId)) => htlcId }.toSet.contains(claimHtlcSuccessTxs.head.htlcId)) Fixture(alice, Set(htlca1a, htlca1b, htlca2), htlcSuccessTxs, htlcTimeoutTxs, bob, Set(htlcb1a, htlcb1b, htlcb2), claimHtlcSuccessTxs, claimHtlcTimeoutTxs, probe) } @@ -241,10 +245,10 @@ 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, + htlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -263,10 +267,10 @@ 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, + htlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map(tx2.input.outPoint -> tx2.tx) )), remoteCommitPublished = None, @@ -285,17 +289,17 @@ 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, + htlcs = 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, + htlcs = Map.empty, irrevocablySpent = Map.empty )), nextRemoteCommitPublished = None, @@ -313,17 +317,17 @@ 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, + htlcs = 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, + htlcs = Map.empty, irrevocablySpent = Map(tx3.input.outPoint -> tx3.tx) )), nextRemoteCommitPublished = None, @@ -343,24 +347,24 @@ 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, + htlcs = 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, + htlcs = 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, + htlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), futureRemoteCommitPublished = None, @@ -380,9 +384,9 @@ 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, + htlcs = Map.empty, irrevocablySpent = Map.empty )), revokedCommitPublished = Nil) @@ -401,9 +405,9 @@ 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, + htlcs = Map.empty, irrevocablySpent = Map(tx4.input.outPoint -> tx4.tx) )), revokedCommitPublished = Nil) @@ -419,10 +423,10 @@ 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, + htlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -431,26 +435,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 +470,10 @@ 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, + htlcs = Map.empty, + htlcDelayedOutputs = Set.empty, irrevocablySpent = Map.empty )), remoteCommitPublished = None, @@ -478,26 +482,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..554d088584 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.htlcs.values.collect { case i: IncomingHtlcId => i }.size >= htlcSuccessCount) + assert(localCommitPublished.htlcs.values.collect { case i: OutgoingHtlcId => i }.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.htlcs.values.collect { case i: OutgoingHtlcId => i }.size >= htlcSuccessCount) + assert(remoteCommitPublished.htlcs.values.collect { case i: IncomingHtlcId => i }.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..057d24b361 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 @@ -3111,7 +3111,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 +3119,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 +3206,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 +3279,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 +3409,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 +3424,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 +3436,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 +3474,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 +3506,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..559b65582d 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 @@ -726,7 +726,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 +771,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 +920,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 +939,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..7dda541c9a 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 @@ -392,7 +392,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 +407,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 +448,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 +503,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 +583,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 +687,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 +720,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 +752,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 +765,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 +783,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 +808,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 +836,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 +865,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 +922,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 +946,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. @@ -1035,7 +1035,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 +1104,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 +1154,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 +1270,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 +1335,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 +1363,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 +1375,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 +1398,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 +1419,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 +1437,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 +1467,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 +1517,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 +1558,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 +1567,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 +1616,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 +1703,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 +1955,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 +2106,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 +2149,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 +2157,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 +2165,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 +2199,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 +2244,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..7e1dce9ee1 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,27 @@ 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.htlcs.values.collect { case IncomingHtlcId(htlcId) => htlcId }.toSeq == Seq(0)) + assert(lcp.htlcs.values.collect { case OutgoingHtlcId(htlcId) => htlcId }.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.htlcs.size == 1) + assert(rcp.htlcs.values.collect { case OutgoingHtlcId(htlcId) => htlcId }.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.htlcs.size == 2) + assert(nrcp.htlcs.values.collect { case OutgoingHtlcId(htlcId) => htlcId }.toSeq == Seq(0)) + assert(nrcp.htlcs.values.collect { case IncomingHtlcId(htlcId) => htlcId }.toSeq == Seq(3)) } } From f957d16d7fb8d5178992788bb7ee2e98580c06f9 Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 4 Jun 2025 17:28:13 +0200 Subject: [PATCH 2/5] Stop storing commit tx and HTLC txs in channel data We previously stored the commitment transaction and HTLC transactions in our channel data along with the remote signatures, without our local signatures. It is unnecessary to store those transactions, as we have all the data we need to recompute them on-the-fly if we need to force close. Storing those transactions can easily use a few hundreds of kB per channel, which adds up to the post-restart latency when channels need to be read from the DB. By not storing those transactions in our commitments, we also remove the need to support encoding them, which gives us greater flexibility to change their format in the future. It also made it confusing in tests, where we sometimes use unsigned transactions as if they were signed. --- .../fr/acinq/eclair/channel/Commitments.scala | 63 +++++--- .../fr/acinq/eclair/channel/Helpers.scala | 85 ++++++----- .../fr/acinq/eclair/channel/fsm/Channel.scala | 20 +-- .../channel/fsm/ChannelOpenSingleFunded.scala | 4 +- .../channel/fsm/CommonFundingHandlers.scala | 2 +- .../eclair/channel/fsm/ErrorHandlers.scala | 15 +- .../channel/fund/InteractiveTxBuilder.scala | 8 +- .../channel/publish/ReplaceableTxFunder.scala | 6 +- .../acinq/eclair/crypto/WeakEntropyPool.scala | 11 +- .../acinq/eclair/json/JsonSerializers.scala | 12 -- .../relay/PostRestartHtlcCleaner.scala | 2 +- .../channel/version0/ChannelTypes0.scala | 23 +-- .../channel/version3/ChannelCodecs3.scala | 13 +- .../channel/version3/ChannelTypes3.scala | 23 ++- .../channel/version4/ChannelCodecs4.scala | 110 ++++++++++--- .../eclair/wire/protocol/CommonCodecs.scala | 7 + .../000003-DATA_NORMAL/fundee/data.json | 17 ++- .../000003-DATA_NORMAL/funder/data.json | 17 ++- .../020002-DATA_NORMAL/funder/data.json | 17 ++- .../funder/data.json | 17 ++- .../funder/data.json | 17 ++- .../funder/data.json | 17 ++- .../fundee/data.json | 17 ++- .../funder/data.json | 17 ++- .../fundee/data.json | 17 ++- .../funder/data.json | 17 ++- .../04000e-DATA_NORMAL/announced/data.json | 17 ++- .../splicing-private/data.json | 51 ++++--- .../eclair/balance/CheckBalanceSpec.scala | 6 +- .../eclair/channel/CommitmentsSpec.scala | 6 +- .../fr/acinq/eclair/channel/HelpersSpec.scala | 16 +- .../publish/ReplaceableTxPublisherSpec.scala | 26 ++-- .../ChannelStateTestsHelperMethods.scala | 22 ++- .../c/WaitForChannelReadyStateSpec.scala | 9 +- ...WaitForDualFundingConfirmedStateSpec.scala | 64 ++++---- .../c/WaitForDualFundingReadyStateSpec.scala | 10 +- .../c/WaitForFundingConfirmedStateSpec.scala | 9 +- .../states/e/NormalQuiescentStateSpec.scala | 20 +-- .../states/e/NormalSplicesStateSpec.scala | 40 ++--- .../channel/states/e/NormalStateSpec.scala | 144 +++++++++--------- .../channel/states/e/OfflineStateSpec.scala | 30 ++-- .../channel/states/f/ShutdownStateSpec.scala | 45 +++--- .../states/g/NegotiatingStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 143 ++++++++--------- .../integration/ChannelIntegrationSpec.scala | 59 +++---- .../fr/acinq/eclair/io/SwitchboardSpec.scala | 10 +- .../eclair/json/JsonSerializersSpec.scala | 15 +- .../eclair/payment/PaymentPacketSpec.scala | 5 +- .../payment/PostRestartHtlcCleanerSpec.scala | 4 +- .../internal/channel/ChannelCodecsSpec.scala | 60 +++----- .../channel/version2/ChannelCodecs2Spec.scala | 15 +- .../channel/version4/ChannelCodecs4Spec.scala | 46 +++++- 52 files changed, 775 insertions(+), 675 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 66bc94f722..4be79bd720 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Script, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} @@ -166,8 +166,6 @@ object CommitmentChanges { } } -case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64) - /** * The channel funding output requires signatures from both channel participants to be spent. * Depending on the segwit version used, those signatures have a different format. @@ -184,10 +182,11 @@ object ChannelSpendSignature { case class PartialSignatureWithNonce(partialSig: ByteVector32, nonce: IndividualNonce) extends ChannelSpendSignature } -case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ChannelSpendSignature) - -/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */ -case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig]) +/** + * The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. + * The [[htlcRemoteSigs]] are stored in the order in which HTLC outputs appear in the commitment transaction. + */ +case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64]) object LocalCommit { def fromCommitSig(params: ChannelParams, commitKeys: LocalCommitmentKeys, fundingTxId: TxId, @@ -201,27 +200,27 @@ object LocalCommit { if (!remoteCommitSigOk) { return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, localCommitIndex, localCommitTx.tx)) } - val commitxTxAndRemoteSig = params.commitmentFormat match { - case _: SegwitV0CommitmentFormat => CommitTxAndRemoteSig(localCommitTx, ChannelSpendSignature.IndividualSignature(commit.signature)) + val commitTxRemoteSig = params.commitmentFormat match { + case _: SegwitV0CommitmentFormat => ChannelSpendSignature.IndividualSignature(commit.signature) case _: SimpleTaprootChannelCommitmentFormat => ??? } val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) if (commit.htlcSignatures.size != sortedHtlcTxs.size) { return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) } - val htlcTxsAndRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map { + val htlcRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map { case (htlcTx: HtlcTx, remoteSig) => if (!htlcTx.checkRemoteSig(commitKeys, remoteSig, params.commitmentFormat)) { return Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) } - HtlcTxAndRemoteSig(htlcTx, remoteSig) + remoteSig } - Right(LocalCommit(localCommitIndex, spec, commitxTxAndRemoteSig, htlcTxsAndRemoteSigs)) + Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, localCommitTx.input, commitTxRemoteSig, htlcRemoteSigs)) } } /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ -case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { +case class RemoteCommit(index: Long, spec: CommitmentSpec, txId: TxId, remotePerCommitmentPoint: PublicKey) { def sign(params: ChannelParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint) @@ -269,9 +268,9 @@ case class Commitment(fundingTxIndex: Long, remoteFundingPubKey: PublicKey, localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { - val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input + val commitInput: InputInfo = localCommit.input val fundingTxId: TxId = commitInput.outPoint.txid - val commitTxIds: CommitTxIds = CommitTxIds(localCommit.commitTxAndRemoteSig.commitTx.tx.txid, remoteCommit.txid, nextRemoteCommit_opt.map(_.commit.txid)) + val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) val capacity: Satoshi = commitInput.txOut.amount /** Once the funding transaction is confirmed, short_channel_id matching this transaction. */ val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match { @@ -683,16 +682,17 @@ case class Commitment(fundingTxIndex: Long, val fundingKey = channelKeys.fundingKey(fundingTxIndex) val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) LocalCommit.fromCommitSig(params, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec).map { localCommit1 => - log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.commitTxAndRemoteSig.commitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) + log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.txId} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) copy(localCommit = localCommit1) } } /** Return a fully signed commit tx, that can be published as-is. */ - def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Transaction = { - val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx + def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = { val fundingKey = channelKeys.fundingKey(fundingTxIndex) - localCommit.commitTxAndRemoteSig.remoteSig match { + val commitKeys = localKeys(params, channelKeys) + val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec) + localCommit.remoteSig match { case remoteSig: ChannelSpendSignature.IndividualSignature => val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey) unsignedCommitTx.aggregateSigs(fundingKey.publicKey, remoteFundingPubKey, localSig, remoteSig) @@ -700,6 +700,19 @@ case class Commitment(fundingTxIndex: Long, } } + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + def htlcTxs(params: ChannelParams, channelKeys: ChannelKeys): Seq[(HtlcTx, ByteVector64)] = { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val commitKeys = localKeys(params, channelKeys) + htlcTxs(params, fundingKey, commitKeys) + } + + /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ + def htlcTxs(params: ChannelParams, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = { + val (_, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec) + htlcTxs.sortBy(_.input.outPoint.index).zip(localCommit.htlcRemoteSigs) + } + } object Commitment { @@ -751,9 +764,9 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, } val localParams: LocalParams = params.localParams val remoteParams: RemoteParams = params.remoteParams - val commitInput: InputInfo = localCommit.commitTxAndRemoteSig.commitTx.input + val commitInput: InputInfo = localCommit.input val fundingTxId: TxId = commitInput.outPoint.txid - val commitTxIds: CommitTxIds = CommitTxIds(localCommit.commitTxAndRemoteSig.commitTx.tx.txid, remoteCommit.txid, nextRemoteCommit_opt.map(_.commit.txid)) + val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) val capacity: Satoshi = commitInput.txOut.amount val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, nextRemoteCommit_opt) @@ -765,7 +778,11 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, def remoteChannelReserve: Satoshi = commitment.remoteChannelReserve(params) - def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Transaction = commitment.fullySignedLocalCommitTx(channelKeys) + def fullySignedLocalCommitTx(channelKeys: ChannelKeys): Transaction = commitment.fullySignedLocalCommitTx(params, channelKeys) + + def htlcTxs(channelKeys: ChannelKeys): Seq[(HtlcTx, ByteVector64)] = commitment.htlcTxs(params, channelKeys) + + def htlcTxs(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = commitment.htlcTxs(params, fundingKey, commitKeys) def specs2String: String = { s"""specs: @@ -1170,7 +1187,7 @@ case class Commitments(params: ChannelParams, /** This function should be used to ignore a commit_sig that we've already received. */ def ignoreRetransmittedCommitSig(commitSig: CommitSig): Boolean = { - val isLatestSig = latest.localCommit.commitTxAndRemoteSig.remoteSig match { + val isLatestSig = latest.localCommit.remoteSig match { case ChannelSpendSignature.IndividualSignature(latestRemoteSig) => latestRemoteSig == commitSig.signature case ChannelSpendSignature.PartialSignatureWithNonce(_, _) => ??? } 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 89b0e1e12a..33df041cf1 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 @@ -883,14 +883,14 @@ object Helpers { /** Claim all the outputs that belong to us in our local commitment transaction. */ def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (LocalCommitPublished, SecondStageTransactions) = { - require(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid == commitTx.txid, "txid mismatch, provided tx is not the current local commit tx") + require(commitment.localCommit.txId == commitTx.txid, "txid mismatch, provided tx is not the current local commit tx") val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitmentKeys = commitment.localKeys(channelKeys) val feerateDelayed = onChainFeeConf.getClosingFeerate(feerates) val mainDelayedTx_opt = withTxGenerationLog("local-main-delayed") { ClaimLocalDelayedOutputTx.createSignedTx(commitmentKeys, commitTx, commitment.localParams.dustLimit, commitment.remoteParams.toSelfDelay, finalScriptPubKey, feerateDelayed, commitment.params.commitmentFormat) } - val htlcs = claimHtlcOutputs(commitmentKeys, commitment) + val htlcs = claimHtlcOutputs(fundingKey, commitmentKeys, commitment) val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitmentKeys, commitment.params.commitmentFormat).toOption val spendAnchor = htlcs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs val anchorTx_opt = if (spendAnchor) { @@ -916,11 +916,17 @@ object Helpers { } } + /** Create outputs of the local commitment transaction, allowing us for example to identify HTLC outputs. */ + def makeLocalCommitTxOutputs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Seq[CommitmentOutput] = { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + makeCommitTxOutputs(fundingKey.publicKey, commitment.remoteFundingPubKey, commitKeys.publicKeys, commitment.localParams.paysCommitTxFees, commitment.localParams.dustLimit, commitment.remoteParams.toSelfDelay, commitment.localCommit.spec, commitment.params.commitmentFormat) + } + /** * 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. */ - private def claimHtlcOutputs(commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, Option[HtlcTx])] = { + private def claimHtlcOutputs(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, Option[HtlcTx])] = { // 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 @@ -933,8 +939,8 @@ 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 { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, remoteSig) => + commitment.htlcTxs(fundingKey, commitKeys).collect { + case (txInfo: HtlcSuccessTx, remoteSig) => if (preimages.contains(txInfo.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. val preimage = preimages(txInfo.paymentHash) @@ -956,7 +962,7 @@ object Helpers { // by us if we receive the preimage, or by our peer after the timeout. Some(txInfo.input.outPoint -> (IncomingHtlcId(txInfo.htlcId), None)) } - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, remoteSig) => + case (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. @@ -969,9 +975,11 @@ object Helpers { } /** 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): Seq[TxPublisher.PublishTx] = { - commitment.localCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, remoteSig) if txInfo.paymentHash == Crypto.sha256(preimage) => + def claimHtlcsWithPreimage(channelKeys: ChannelKeys, localCommitPublished: LocalCommitPublished, commitment: FullCommitment, preimage: ByteVector32)(implicit log: LoggingAdapter): Seq[TxPublisher.PublishTx] = { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val commitKeys = commitment.localKeys(channelKeys) + commitment.htlcTxs(fundingKey, commitKeys).collect { + case (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)) @@ -997,8 +1005,11 @@ object Helpers { val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case u: UpdateFulfillHtlc => Crypto.sha256(u.paymentPreimage) -> u.paymentPreimage }.toMap - val outpoints = commitment.localCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == htlcId && !preimages.contains(txInfo.paymentHash) => txInfo.input.outPoint + val htlcsWithPreimage = commitment.localCommit.spec.htlcs.collect { + case IncomingHtlc(add: UpdateAddHtlc) if preimages.contains(add.paymentHash) => add.id + } + val outpoints = localCommitPublished.htlcs.collect { + case (outpoint, IncomingHtlcId(id)) if id == htlcId && !htlcsWithPreimage.contains(id) => outpoint }.toSet localCommitPublished.copy(htlcs = localCommitPublished.htlcs -- outpoints) } @@ -1043,7 +1054,7 @@ object Helpers { /** Claim all the outputs that belong to us in the remote commitment transaction (which can be either their current or next commitment). */ def claimCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): (RemoteCommitPublished, SecondStageTransactions) = { - require(remoteCommit.txid == commitTx.txid, "txid mismatch, provided tx is not the current remote commit tx") + 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 mainTx_opt = claimMainOutput(commitment.params, commitKeys, commitTx, feerates, onChainFeeConf, finalScriptPubKey) @@ -1098,10 +1109,9 @@ object Helpers { private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, 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") + require(remoteCommitTx.tx.txid == remoteCommit.txId, "txid mismatch, cannot recompute the current remote commit tx") // 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 @@ -1114,7 +1124,6 @@ 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 { case OutgoingHtlc(add: UpdateAddHtlc) => @@ -1377,27 +1386,25 @@ object Helpers { * * @return a set of htlcs that need to be failed upstream */ - def trimmedOrTimedOutHtlcs(commitmentFormat: CommitmentFormat, localCommit: LocalCommit, localDustLimit: Satoshi, confirmedTx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - val untrimmedHtlcs = Transactions.trimOfferedHtlcs(localDustLimit, localCommit.spec, commitmentFormat).map(_.add) - if (confirmedTx.txid == localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { + def trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, localCommit: LocalCommit, confirmedTx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { + if (confirmedTx.txid == localCommit.txId) { // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). + val untrimmedHtlcs = Transactions.trimOfferedHtlcs(commitment.localParams.dustLimit, localCommit.spec, commitment.params.commitmentFormat).map(_.add) localCommit.spec.htlcs.collect(outgoing) -- untrimmedHtlcs - } else { - // Maybe this is a timeout tx: in that case we can resolve and fail the corresponding htlc. - confirmedTx.txIn.flatMap(txIn => localCommit.htlcTxsAndRemoteSigs.map(_.htlcTx).find(_.input.outPoint == txIn.outPoint) match { + } else if (confirmedTx.txIn.exists(_.outPoint.txid == localCommit.txId)) { + // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the + // corresponding htlc. + val outputs = LocalClose.makeLocalCommitTxOutputs(channelKeys, commitment.localKeys(channelKeys), commitment) + confirmedTx.txIn.filter(_.outPoint.txid == localCommit.txId).flatMap(txIn => outputs(txIn.outPoint.index.toInt) match { // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already // extracted the preimage with [[extractPreimages]] and relayed it upstream. - case Some(htlcTimeoutTx: HtlcTimeoutTx) if Scripts.extractPreimagesFromClaimHtlcSuccess(confirmedTx).isEmpty => - untrimmedHtlcs.find(_.id == htlcTimeoutTx.htlcId) match { - case Some(htlc) => - log.info("htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlcTimeoutTx.htlcId, htlc.paymentHash, confirmedTx.lockTime, confirmedTx) - Some(htlc) - case None => - log.error("could not find htlc #{} for htlc-timeout tx={}", htlcTimeoutTx.htlcId, confirmedTx) - None - } + case CommitmentOutput.OutHtlc(htlc, _, _) if Scripts.extractPreimagesFromClaimHtlcSuccess(confirmedTx).isEmpty => + log.info("htlc-timeout tx for htlc #{} paymentHash={} expiry={} has been confirmed (tx={})", htlc.add.id, htlc.add.paymentHash, htlc.add.cltvExpiry, confirmedTx) + Some(htlc.add) case _ => None }).toSet + } else { + Set.empty } } @@ -1409,16 +1416,16 @@ object Helpers { * @return a set of htlcs that need to be failed upstream */ def trimmedOrTimedOutHtlcs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, confirmedTx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] = { - if (confirmedTx.txid == remoteCommit.txid) { + if (confirmedTx.txid == remoteCommit.txId) { // The commitment tx is confirmed: we can immediately fail all dust htlcs (they don't have an output in the tx). val untrimmedHtlcs = Transactions.trimReceivedHtlcs(commitment.remoteParams.dustLimit, remoteCommit.spec, commitment.params.commitmentFormat).map(_.add) remoteCommit.spec.htlcs.collect(incoming) -- untrimmedHtlcs - } else if (confirmedTx.txIn.exists(_.outPoint.txid == remoteCommit.txid)) { + } else if (confirmedTx.txIn.exists(_.outPoint.txid == remoteCommit.txId)) { // The transaction spends the commitment tx: maybe it is a timeout tx, in which case we can resolve and fail the // corresponding htlc. val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) val outputs = RemoteClose.makeRemoteCommitTxOutputs(channelKeys, commitKeys, commitment, remoteCommit) - confirmedTx.txIn.filter(_.outPoint.txid == remoteCommit.txid).flatMap(txIn => outputs(txIn.outPoint.index.toInt) match { + confirmedTx.txIn.filter(_.outPoint.txid == remoteCommit.txId).flatMap(txIn => outputs(txIn.outPoint.index.toInt) match { // This may also be our peer claiming the HTLC by revealing the preimage: in that case we have already // extracted the preimage with [[extractPreimages]] and relayed it upstream. // Note: we're looking at the remote commitment, so it's an incoming HTLC for them (outgoing for us). @@ -1439,11 +1446,11 @@ object Helpers { * @param tx a transaction that is sufficiently buried in the blockchain */ def onChainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = { - if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (localCommit.txId == tx.txid) { localCommit.spec.htlcs.collect(outgoing) - } else if (remoteCommit.txid == tx.txid) { + } else if (remoteCommit.txId == tx.txid) { remoteCommit.spec.htlcs.collect(incoming) - } else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) { + } else if (nextRemoteCommit_opt.map(_.txId).contains(tx.txid)) { nextRemoteCommit_opt.get.spec.htlcs.collect(incoming) } else { Set.empty @@ -1461,7 +1468,7 @@ object Helpers { val nextRemoteCommit_opt = d.commitments.latest.nextRemoteCommit_opt.map(_.commit) // NB: from the p.o.v of remote, their incoming htlcs are our outgoing htlcs. val outgoingHtlcs = localCommit.spec.htlcs.collect(outgoing) ++ (remoteCommit.spec.htlcs ++ nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty)).collect(incoming) - if (localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (localCommit.txId == tx.txid) { // Our commit got confirmed: any htlc that is *not* in our commit will never reach the chain. outgoingHtlcs -- localCommit.spec.htlcs.collect(outgoing) } else if (d.revokedCommitPublished.map(_.commitTx.txid).contains(tx.txid)) { @@ -1472,10 +1479,10 @@ object Helpers { // upstream. In the best case scenario, we already fulfilled upstream, then the fail will be a no-op and we // will pocket the htlc amount. outgoingHtlcs - } else if (remoteCommit.txid == tx.txid) { + } else if (remoteCommit.txId == tx.txid) { // Their current commit got confirmed: any htlc that is *not* in their current commit will never reach the chain. outgoingHtlcs -- remoteCommit.spec.htlcs.collect(incoming) - } else if (nextRemoteCommit_opt.map(_.txid).contains(tx.txid)) { + } else if (nextRemoteCommit_opt.map(_.txId).contains(tx.txid)) { // Their next commit got confirmed: any htlc that is *not* in their next commit will never reach the chain. outgoingHtlcs -- nextRemoteCommit_opt.map(_.spec.htlcs).getOrElse(Set.empty).collect(incoming) } else { 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 34f0a3c345..7694baba61 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 @@ -1894,7 +1894,7 @@ 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 localTxs = d.localCommitPublished.map(lcp => Closing.LocalClose.claimHtlcsWithPreimage(commitment.localKeys(channelKeys), lcp, commitment, c.r)).getOrElse(Nil) + val localTxs = d.localCommitPublished.map(lcp => Closing.LocalClose.claimHtlcsWithPreimage(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) @@ -1973,10 +1973,10 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } else if (d.futureRemoteCommitPublished.exists(_.commitTx.txid == tx.txid)) { // this is because WatchSpent watches never expire and we are notified multiple times stay() - } else if (tx.txid == d.commitments.latest.remoteCommit.txid) { + } else if (tx.txid == d.commitments.latest.remoteCommit.txId) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentCurrent(tx, d) - } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid)) { + } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txId == tx.txid)) { // counterparty may attempt to spend its last commit tx at any time handleRemoteSpentNext(tx, d) } else if (tx.txIn.map(_.outPoint.txid).contains(d.commitments.latest.fundingTxId)) { @@ -2023,12 +2023,12 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // We reset the state to match the commitment that confirmed. val d1 = d.copy(commitments = commitments1) // This commitment may be revoked: we need to verify that its index matches our latest known index before overwriting our previous commitments. - if (commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid == tx.txid) { + if (commitment.localCommit.txId == tx.txid) { // Our local commit has been published from the outside, it's unexpected but let's deal with it anyway. spendLocalCurrent(d1) - } else if (commitment.remoteCommit.txid == tx.txid && commitment.remoteCommit.index == d.commitments.remoteCommitIndex) { + } else if (commitment.remoteCommit.txId == tx.txid && commitment.remoteCommit.index == d.commitments.remoteCommitIndex) { handleRemoteSpentCurrent(tx, d1) - } else if (commitment.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid) && commitment.remoteCommit.index == d.commitments.remoteCommitIndex && d.commitments.remoteNextCommitInfo.isLeft) { + } else if (commitment.nextRemoteCommit_opt.exists(_.commit.txId == tx.txid) && commitment.remoteCommit.index == d.commitments.remoteCommitIndex && d.commitments.remoteNextCommitInfo.isLeft) { handleRemoteSpentNext(tx, d1) } else { // Our counterparty is trying to broadcast a revoked commit tx (cheating attempt). @@ -2132,7 +2132,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall } // we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold val timedOutHtlcs = Closing.isClosingTypeAlreadyKnown(d1) match { - case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(d.commitments.params.commitmentFormat, c.localCommit, d.commitments.params.localParams.dustLimit, tx) + case Some(c: Closing.LocalClose) => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.localCommit, tx) case Some(c: Closing.RemoteClose) => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.remoteCommit, tx) case Some(_: Closing.RevokedClose) => Set.empty[UpdateAddHtlc] // revoked commitments are handled using [[overriddenOutgoingHtlcs]] below case Some(_: Closing.RecoveryClose) => Set.empty[UpdateAddHtlc] // we lose htlc outputs in dataloss protection scenarios (future remote commit) @@ -2894,11 +2894,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall if (d.commitments.all.map(_.fundingTxId).contains(tx.txid)) { // if the spending tx is itself a funding tx, this is a splice and there is nothing to do stay() - } else if (tx.txid == d.commitments.latest.remoteCommit.txid) { + } else if (tx.txid == d.commitments.latest.remoteCommit.txId) { handleRemoteSpentCurrent(tx, d) - } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txid == tx.txid)) { + } else if (d.commitments.latest.nextRemoteCommit_opt.exists(_.commit.txId == tx.txid)) { handleRemoteSpentNext(tx, d) - } else if (tx.txid == d.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txid) { + } else if (tx.txid == d.commitments.latest.localCommit.txId) { log.warning(s"processing local commit spent from the outside") spendLocalCurrent(d) } else if (tx.txIn.map(_.outPoint.txid).contains(d.commitments.latest.fundingTxId)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index ef3895dca0..09ef1cbee6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -287,7 +287,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { remoteFundingPubKey = remoteFundingPubKey, localFundingStatus = SingleFundedUnconfirmedFundingTx(None), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, ChannelSpendSignature.IndividualSignature(remoteSig)), htlcTxsAndRemoteSigs = Nil), + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), nextRemoteCommit_opt = None) val commitments = Commitments( @@ -332,7 +332,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { remoteFundingPubKey = remoteFundingPubKey, localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(fundingTx)), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, CommitTxAndRemoteSig(localCommitTx, ChannelSpendSignature.IndividualSignature(remoteSig)), htlcTxsAndRemoteSigs = Nil), + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), remoteCommit = remoteCommit, nextRemoteCommit_opt = None ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index 539361019e..ed96642c0c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -44,7 +44,7 @@ trait CommonFundingHandlers extends CommonHandlers { * @param delay_opt optional delay to reduce herd effect at startup. */ def watchFundingSpent(commitment: Commitment, additionalKnownSpendingTxs: Set[TxId], delay_opt: Option[FiniteDuration]): Unit = { - val knownSpendingTxs = Set(commitment.localCommit.commitTxAndRemoteSig.commitTx.tx.txid, commitment.remoteCommit.txid) ++ commitment.nextRemoteCommit_opt.map(_.commit.txid).toSet ++ additionalKnownSpendingTxs + val knownSpendingTxs = commitment.commitTxIds.txIds ++ additionalKnownSpendingTxs val watch = WatchFundingSpent(self, commitment.commitInput.outPoint.txid, commitment.commitInput.outPoint.index.toInt, knownSpendingTxs) delay_opt match { case Some(delay) => context.system.scheduler.scheduleOnce(delay, blockchain.toClassic, watch) 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 a1cb76ad88..5ab67bed94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ErrorHandlers.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.actor.{ActorRef, FSM} +import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, OutPoint, SatoshiLong, Transaction} import fr.acinq.eclair.NotificationsLogger import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator @@ -239,7 +240,7 @@ trait ErrorHandlers extends CommonHandlers { case _ => None } val publishMainDelayedTx_opt = txs.mainDelayedTx_opt.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishHtlcTxs = redeemableHtlcTxs(lcp.commitTx, commitKeys, commitment) + val publishHtlcTxs = redeemableHtlcTxs(lcp.commitTx, fundingKey, commitKeys, commitment) val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs publishIfNeeded(publishQueue, lcp.irrevocablySpent) @@ -264,12 +265,12 @@ trait ErrorHandlers extends CommonHandlers { txs.htlcDelayedTxs.foreach(tx => watchSpentIfNeeded(tx.input, lcp.irrevocablySpent)) } - private def redeemableHtlcTxs(commitTx: Transaction, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Iterable[PublishTx] = { + private def redeemableHtlcTxs(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Iterable[PublishTx] = { val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case fulfill: UpdateFulfillHtlc => Crypto.sha256(fulfill.paymentPreimage) -> fulfill.paymentPreimage }.toMap - commitment.localCommit.htlcTxsAndRemoteSigs.flatMap { - case HtlcTxAndRemoteSig(htlcTx, remoteSig) => + commitment.htlcTxs(fundingKey, commitKeys).flatMap { + case (htlcTx, remoteSig) => val preimage_opt = preimages.get(htlcTx.paymentHash) commitment.params.commitmentFormat match { case Transactions.DefaultCommitmentFormat => @@ -295,7 +296,7 @@ trait ErrorHandlers extends CommonHandlers { def handleRemoteSpentCurrent(commitTx: Transaction, d: ChannelDataWithCommitments) = { val commitments = d.commitments.latest log.warning(s"they published their current commit in txid=${commitTx.txid}") - require(commitTx.txid == commitments.remoteCommit.txid, "txid mismatch") + require(commitTx.txid == commitments.remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "remote-commit")) val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) @@ -313,7 +314,7 @@ trait ErrorHandlers extends CommonHandlers { log.warning(s"they published their next commit in txid=${commitTx.txid}") require(commitment.nextRemoteCommit_opt.nonEmpty, "next remote commit must be defined") val remoteCommit = commitment.nextRemoteCommit_opt.get.commit - require(commitTx.txid == remoteCommit.txid, "txid mismatch") + require(commitTx.txid == remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "next-remote-commit")) @@ -332,7 +333,7 @@ trait ErrorHandlers extends CommonHandlers { 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 + case Some(c) if rcp.commitTx.txid == c.commit.txId => c.commit case _ => commitment.remoteCommit } val commitKeys = commitment.remoteKeys(channelKeys, remoteCommit.remotePerCommitmentPoint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 8b18e06b6a..dd6aec7db6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -34,7 +34,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Output.Local import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} -import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.{InputInfo, SegwitV0CommitmentFormat, SimpleTaprootChannelCommitmentFormat} import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, ToMilliSatoshiConversion, UInt64} @@ -856,7 +856,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val localSigOfRemoteTx = remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig val htlcSignatures = sortedHtlcTxs.map(_.sign(remoteCommitmentKeys, channelParams.commitmentFormat)).toList val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) - val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) + val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid, localCommitTx.input) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) case _: SimpleTaprootChannelCommitmentFormat => ??? @@ -1020,7 +1020,7 @@ object InteractiveTxSigningSession { // +-------+ +-------+ /** A local commitment for which we haven't received our peer's signatures. */ - case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx]) + case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo) private def shouldSignFirst(isInitiator: Boolean, channelParams: ChannelParams, tx: SharedTransaction): Boolean = { val sharedAmountIn = tx.sharedInput_opt.map(_.txOut.amount).getOrElse(0 sat) @@ -1095,7 +1095,7 @@ object InteractiveTxSigningSession { localCommit: Either[UnsignedLocalCommit, LocalCommit], remoteCommit: RemoteCommit, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { - val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input) + val commitInput: InputInfo = localCommit.fold(_.input, _.input) val localCommitIndex: Long = localCommit.fold(_.index, _.index) // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. val nextLocalCommitmentNumber: Long = localCommit match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 79fca97e4e..ce78a5aced 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -94,7 +94,7 @@ object ReplaceableTxFunder { case _: ReplaceableHtlc => tx.txInfo.amountIn case _: ReplaceableClaimHtlc => tx.txInfo.amountIn case _: ReplaceableAnchor => - val htlcBalance = tx.commitment.localCommit.htlcTxsAndRemoteSigs.map(_.htlcTx.amountIn).sum + val htlcBalance = tx.commitment.localCommit.spec.htlcs.map(_.add.amountMsat).sum.truncateToSatoshi val mainBalance = tx.commitment.localCommit.spec.toLocal.truncateToSatoshi // If there are no HTLCs or a low HTLC amount, we still want to get back our main balance. // In that case, we spend at most 5% of our balance in fees, with a hard cap configured by the node operator. @@ -141,7 +141,7 @@ object ReplaceableTxFunder { // @formatter:off sealed trait AdjustPreviousTxOutputResult - object AdjustPreviousTxOutputResult { + private object AdjustPreviousTxOutputResult { case class Skip(reason: String) extends AdjustPreviousTxOutputResult case class AddWalletInputs(previousTx: ReplaceableTxWithWalletInputs) extends AdjustPreviousTxOutputResult case class TxOutputAdjusted(updatedTx: ReplaceableTx) extends AdjustPreviousTxOutputResult @@ -152,7 +152,7 @@ object ReplaceableTxFunder { * Adjust the outputs of a transaction that was previously published at a lower feerate. * If the current set of inputs doesn't let us reach the target feerate, we will request new wallet inputs from bitcoind. */ - def adjustPreviousTxOutput(previousTx: FundedTx, targetFeerate: FeeratePerKw): AdjustPreviousTxOutputResult = { + private def adjustPreviousTxOutput(previousTx: FundedTx, targetFeerate: FeeratePerKw): AdjustPreviousTxOutputResult = { val targetFee = previousTx.tx match { case anchorTx: ReplaceableAnchor => val totalWeight = previousTx.signedTx.weight() + anchorTx.commitTx.weight() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala index 7f588cf692..cd34acc589 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{BlockId, ByteVector32, ByteVector64, Crypto} import fr.acinq.eclair.TimestampMilli import fr.acinq.eclair.blockchain.NewBlock -import fr.acinq.eclair.channel.ChannelSignatureReceived +import fr.acinq.eclair.channel.{ChannelSignatureReceived, ChannelSpendSignature} import fr.acinq.eclair.io.PeerConnected import fr.acinq.eclair.payment.ChannelPaymentRelayed import fr.acinq.eclair.router.NodeUpdated @@ -50,7 +50,7 @@ object WeakEntropyPool { private case class WrappedNewBlock(blockId: BlockId) extends Command private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: TimestampMilli) extends Command private case class WrappedPeerConnected(nodeId: PublicKey) extends Command - private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command + private case class WrappedChannelSignature(sig: ChannelSpendSignature) extends Command private case class WrappedNodeUpdated(sig: ByteVector64) extends Command // @formatter:on @@ -60,7 +60,7 @@ object WeakEntropyPool { context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId))) context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature))) - context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.wtxid))) + context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.latest.localCommit.remoteSig))) Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds) collecting(collector, None) @@ -87,7 +87,10 @@ object WeakEntropyPool { case WrappedNodeUpdated(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis()))) - case WrappedChannelSignature(wtxid) => collecting(collector, collect(entropy_opt, wtxid ++ ByteVector.fromLong(System.currentTimeMillis()))) + case WrappedChannelSignature(sig) => sig match { + case ChannelSpendSignature.IndividualSignature(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis()))) + case ChannelSpendSignature.PartialSignatureWithNonce(partialSig, _) => collecting(collector, collect(entropy_opt, partialSig ++ ByteVector.fromLong(System.currentTimeMillis()))) + } } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 2abb7c6453..ddb27aab18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -316,17 +316,6 @@ object ColorSerializer extends MinimalSerializer({ case c: Color => JString(c.toString) }) -// @formatter:off -private case class CommitTxAndRemoteSigJson(commitTx: CommitTx, remoteSig: ByteVector64) -private case class CommitTxAndRemotePartialSigJson(commitTx: CommitTx, remoteSig: ChannelSpendSignature.PartialSignatureWithNonce) -object CommitTxAndRemoteSigSerializer extends ConvertClassSerializer[CommitTxAndRemoteSig]( - i => i.remoteSig match { - case f: ChannelSpendSignature.IndividualSignature => CommitTxAndRemoteSigJson(i.commitTx, f.sig) - case p: ChannelSpendSignature.PartialSignatureWithNonce => CommitTxAndRemotePartialSigJson(i.commitTx, p) - } -) -// @formatter:on - // @formatter:off private sealed trait HopJson private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: HopRelayParams) extends HopJson @@ -725,7 +714,6 @@ object JsonSerializers { OpenChannelResponseSerializer + CommandResponseSerializer + InputInfoSerializer + - CommitTxAndRemoteSigSerializer + ColorSerializer + ThrowableSerializer + FailureMessageSerializer + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 52db0a055a..1b517d4c1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -430,7 +430,7 @@ object PostRestartHtlcCleaner { val params = d.commitments.params val channelKeys = nodeParams.channelKeyManager.channelKeys(params.channelConfig, params.localParams.fundingKeyPath) val timedOutHtlcs: Set[Long] = (closingType_opt match { - case Some(c: Closing.LocalClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(params.commitmentFormat, c.localCommit, params.localParams.dustLimit, tx)) + case Some(c: Closing.LocalClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.localCommit, tx)) case Some(c: Closing.RemoteClose) => confirmedTxs.flatMap(tx => Closing.trimmedOrTimedOutHtlcs(channelKeys, d.commitments.latest, c.remoteCommit, tx)) case Some(_: Closing.RevokedClose) => Set.empty // revoked commitments are handled using [[overriddenOutgoingHtlcs]] above case Some(_: Closing.RecoveryClose) => Set.empty // we lose htlc outputs in dataloss protection scenarios (future remote commit) 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 2298c2ecb5..5fe64865bd 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 @@ -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 @@ -110,22 +102,13 @@ private[channel] object ChannelTypes0 { case class PublishableTxs(commitTx: CommitTx, htlcTxsAndSigs: List[HtlcTxAndSigs]) // Before version3, we stored fully signed local transactions (commit tx and htlc txs). It meant that someone gaining - // access to the database could publish revoked commit txs, so we changed that to only store unsigned txs and remote - // signatures. + // access to the database could publish revoked commit txs, so we changed that to only store remote signatures. case class LocalCommit(index: Long, spec: CommitmentSpec, publishableTxs: PublishableTxs) { def migrate(remoteFundingPubKey: PublicKey): channel.LocalCommit = { val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey) val unsignedCommitTx = publishableTxs.commitTx.copy(tx = removeWitnesses(publishableTxs.commitTx.tx)) - val commitTxAndRemoteSig = CommitTxAndRemoteSig(unsignedCommitTx, remoteSig) - val htlcTxsAndRemoteSigs = publishableTxs.htlcTxsAndSigs map { - case HtlcTxAndSigs(htlcTx: HtlcSuccessTx, _, remoteSig) => - val unsignedHtlcTx = htlcTx.copy(tx = removeWitnesses(htlcTx.tx)) - HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig) - case HtlcTxAndSigs(htlcTx: HtlcTimeoutTx, _, remoteSig) => - val unsignedHtlcTx = htlcTx.copy(tx = removeWitnesses(htlcTx.tx)) - HtlcTxAndRemoteSig(unsignedHtlcTx, remoteSig) - } - channel.LocalCommit(index, spec, commitTxAndRemoteSig, htlcTxsAndRemoteSigs) + val htlcRemoteSigs = publishableTxs.htlcTxsAndSigs.map(_.remoteSig) + channel.LocalCommit(index, spec, unsignedCommitTx.tx.txid, unsignedCommitTx.input, remoteSig, htlcRemoteSigs) } private def extractRemoteSig(commitTx: CommitTx, remoteFundingPubKey: PublicKey): ChannelSpendSignature.IndividualSignature = { 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 e40c519e46..6c1a15dfa5 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 @@ -18,7 +18,6 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Transaction, TxOut} -import fr.acinq.eclair.blockchain.fee.ConfirmationTarget import fr.acinq.eclair.channel.LocalFundingStatus._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ @@ -206,19 +205,19 @@ private[channel] object ChannelCodecs3 { .typecase(0x02, claimHtlcTimeoutTxNoConfirmCodec) .typecase(0x03, claimHtlcSuccessTxNoConfirmCodec) - val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( + val htlcTxsAndRemoteSigsCodec: Codec[ChannelTypes3.HtlcTxAndRemoteSig] = ( ("txinfo" | htlcTxCodec) :: - ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] + ("remoteSig" | bytes64)).as[ChannelTypes3.HtlcTxAndRemoteSig] - val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( + val commitTxAndRemoteSigCodec: Codec[ChannelTypes3.CommitTxAndRemoteSig] = ( ("commitTx" | commitTxCodec) :: - ("remoteSig" | bytes64.as[ChannelSpendSignature.IndividualSignature].upcast[ChannelSpendSignature])).as[CommitTxAndRemoteSig] + ("remoteSig" | bytes64.as[ChannelSpendSignature.IndividualSignature])).as[ChannelTypes3.CommitTxAndRemoteSig] - val localCommitCodec: Codec[LocalCommit] = ( + val localCommitCodec: Codec[ChannelTypes3.LocalCommit] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: - ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit] + ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[ChannelTypes3.LocalCommit] val remoteCommitCodec: Codec[RemoteCommit] = ( ("index" | uint64overflow) :: 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 ce14345e8e..53367cfeaf 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 @@ -16,11 +16,14 @@ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} import fr.acinq.eclair.channel import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession import fr.acinq.eclair.crypto.ShaChain +import fr.acinq.eclair.transactions.CommitmentSpec +import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx} import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 import fr.acinq.eclair.wire.protocol.CommitSig @@ -28,13 +31,27 @@ private[channel] object ChannelTypes3 { case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long) + case class HtlcTxAndRemoteSig(htlcTx: HtlcTx, remoteSig: ByteVector64) + + case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ChannelSpendSignature.IndividualSignature) + + // Before version 4, we stored the unsigned commit tx and htlc txs in our local commit. + // We changed that to only store the remote signatures and re-compute transactions on-the-fly when force-closing. + case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig]) { + def migrate(): channel.LocalCommit = channel.LocalCommit(index, spec, commitTxAndRemoteSig.commitTx.tx.txid, commitTxAndRemoteSig.commitTx.input, commitTxAndRemoteSig.remoteSig, htlcTxsAndRemoteSigs.map(_.remoteSig)) + } + + case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx]) { + def migrate(): InteractiveTxSigningSession.UnsignedLocalCommit = InteractiveTxSigningSession.UnsignedLocalCommit(index, spec, commitTx.tx.txid, commitTx.input) + } + // Before version4, we didn't support multiple active commitments, which were later introduced by dual funding and splicing. case class Commitments(channelId: ByteVector32, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, localParams: LocalParams, remoteParams: ChannelTypes0.RemoteParams, channelFlags: ChannelFlags, - localCommit: LocalCommit, remoteCommit: RemoteCommit, + localCommit: ChannelTypes3.LocalCommit, remoteCommit: RemoteCommit, localChanges: LocalChanges, remoteChanges: RemoteChanges, localNextHtlcId: Long, remoteNextHtlcId: Long, originChannels: Map[Long, Origin], @@ -45,7 +62,7 @@ private[channel] object ChannelTypes3 { def migrate(): channel.Commitments = channel.Commitments( ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams.migrate(), channelFlags), CommitmentChanges(localChanges, remoteChanges, localNextHtlcId, remoteNextHtlcId), - Seq(Commitment(fundingTxIndex = 0, firstRemoteCommitIndex = 0, remoteFundingPubKey = remoteParams.fundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), + Seq(Commitment(fundingTxIndex = 0, firstRemoteCommitIndex = 0, remoteFundingPubKey = remoteParams.fundingPubKey, localFundingStatus, remoteFundingStatus, localCommit.migrate(), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), inactive = Nil, remoteNextCommitInfo.fold(w => Left(WaitForRev(w.sentAfterLocalCommitIndex)), remotePerCommitmentPoint => Right(remotePerCommitmentPoint)), remotePerCommitmentSecrets, 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 5a7776fcf5..4e3fed1ac5 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 @@ -17,7 +17,7 @@ import fr.acinq.eclair.wire.internal.channel.version3.ChannelTypes3 import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature, RealShortChannelId, channel} +import fr.acinq.eclair.{Alias, CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshiLong, PermanentChannelFeature, RealShortChannelId, channel} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -199,13 +199,17 @@ private[channel] object ChannelCodecs4 { .typecase(0x02, claimHtlcTimeoutTxNoConfirmCodec) .typecase(0x03, claimHtlcSuccessTxNoConfirmCodec) - val htlcTxsAndRemoteSigsCodec: Codec[HtlcTxAndRemoteSig] = ( + val htlcTxsAndRemoteSigsCodec: Codec[ChannelTypes3.HtlcTxAndRemoteSig] = ( ("txinfo" | htlcTxCodec) :: - ("remoteSig" | bytes64)).as[HtlcTxAndRemoteSig] - - val commitTxAndRemoteSigCodec: Codec[CommitTxAndRemoteSig] = ( + ("remoteSig" | bytes64)).as[ChannelTypes3.HtlcTxAndRemoteSig] + + val commitTxAndRemoteSigCodec: Codec[ChannelTypes3.CommitTxAndRemoteSig] = ( ("commitTx" | commitTxCodec) :: - ("remoteSig" | bytes64.as[ChannelSpendSignature.IndividualSignature].upcast[ChannelSpendSignature])).as[CommitTxAndRemoteSig] + ("remoteSig" | bytes64.as[ChannelSpendSignature.IndividualSignature])).as[ChannelTypes3.CommitTxAndRemoteSig] + + val channelSpendSignatureCodec: Codec[ChannelSpendSignature] = discriminated[ChannelSpendSignature].by(uint8) + .typecase(0x01, bytes64.as[ChannelSpendSignature.IndividualSignature]) + .typecase(0x02, (("partialSig" | bytes32) :: ("nonce" | publicNonce)).as[ChannelSpendSignature.PartialSignatureWithNonce]) val updateMessageCodec: Codec[UpdateMessage] = lengthDelimited(lightningMessageCodec.narrow[UpdateMessage](f => Attempt.successful(f.asInstanceOf[UpdateMessage]), g => g)) @@ -475,11 +479,19 @@ private[channel] object ChannelCodecs4 { ("localNextHtlcId" | uint64overflow) :: ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] - private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( + private def localCommitWithTxsCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: - ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[LocalCommit] + ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[ChannelTypes3.LocalCommit].decodeOnly.map[LocalCommit](_.migrate()).decodeOnly + + private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txId" | txId) :: + ("input" | inputInfoCodec) :: + ("remoteSig" | channelSpendSignatureCodec) :: + ("htlcRemoteSigs" | listOfN(uint16, bytes64))).as[LocalCommit] private def remoteCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[RemoteCommit] = ( ("index" | uint64overflow) :: @@ -497,7 +509,17 @@ private[channel] object ChannelCodecs4 { ("fundingPubKey" | publicKey) :: ("fundingTxStatus" | fundingTxStatusCodec) :: ("remoteFundingStatus" | remoteFundingStatusCodec) :: - ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: + ("localCommit" | localCommitWithTxsCodec(minimalCommitmentSpecCodec(htlcs))) :: + ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + + private def commitmentCodecWithLocalTxs(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + ("fundingTxIndex" | uint32) :: + ("firstRemoteCommitIndex" | uint64overflow) :: + ("fundingPubKey" | publicKey) :: + ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingStatus" | remoteFundingStatusCodec) :: + ("localCommit" | localCommitWithTxsCodec(minimalCommitmentSpecCodec(htlcs))) :: ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] @@ -579,6 +601,21 @@ private[channel] object ChannelCodecs4 { commitments => EncodedCommitments(commitments) ) + val commitmentsCodecWithLocalTxs: Codec[Commitments] = ( + ("params" | paramsCodec) :: + ("changes" | changesCodec) :: + (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => + ("active" | listOfN(uint16, commitmentCodecWithLocalTxs(htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodecWithLocalTxs(htlcs))) :: + ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: + ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: + ("originChannels" | originsMapCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) + })).as[EncodedCommitments].xmap( + encoded => encoded.toCommitments, + commitments => EncodedCommitments(commitments) + ) + val commitmentsCodec: Codec[Commitments] = ( ("params" | paramsCodec) :: ("changes" | changesCodec) :: @@ -595,7 +632,8 @@ private[channel] object ChannelCodecs4 { ) val versionedCommitmentsCodec: Codec[Commitments] = discriminated[Commitments].by(uint8) - .typecase(0x01, commitmentsCodec) + .typecase(0x02, commitmentsCodec) + .typecase(0x01, commitmentsCodecWithLocalTxs) val closingFeeratesCodec: Codec[ClosingFeerates] = ( ("preferred" | feeratePerKw) :: @@ -603,8 +641,8 @@ private[channel] object ChannelCodecs4 { ("max" | feeratePerKw)).as[ClosingFeerates] val closeStatusCodec: Codec[CloseStatus] = discriminated[CloseStatus].by(uint8) - .typecase(0x01, optional(bool8, closingFeeratesCodec).as[CloseStatus.Initiator]) - .typecase(0x02, optional(bool8, closingFeeratesCodec).as[CloseStatus.NonInitiator]) + .typecase(0x01, optional(bool8, closingFeeratesCodec).as[CloseStatus.Initiator]) + .typecase(0x02, optional(bool8, closingFeeratesCodec).as[CloseStatus.NonInitiator]) val closingTxProposedCodec: Codec[ClosingTxProposed] = ( ("unsignedTx" | closingTxCodec) :: @@ -678,21 +716,35 @@ private[channel] object ChannelCodecs4 { // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel // cannot be used for payments. - private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { - val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( + private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsWithTxsCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { + val unsignedLocalCommitWithTxsCodec: Codec[UnsignedLocalCommit] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTx" | commitTxCodec) :: - ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[UnsignedLocalCommit] + ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[ChannelTypes3.UnsignedLocalCommit].decodeOnly.map[UnsignedLocalCommit](_.migrate()).decodeOnly + + val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( + ("index" | uint64overflow) :: + ("spec" | commitmentSpecCodec) :: + ("txId" | txId) :: + ("input" | inputInfoCodec)).as[UnsignedLocalCommit] val waitingForSigsWithoutLiquidityPurchaseCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: ("fundingTxIndex" | uint32) :: ("fundingTx" | partiallySignedSharedTransactionCodec) :: - ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: + ("localCommit" | either(bool8, unsignedLocalCommitWithTxsCodec, localCommitWithTxsCodec(commitmentSpecCodec))) :: ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[InteractiveTxSigningSession.WaitingForSigs] + val waitingForSigsWithTxsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( + ("fundingParams" | fundingParamsCodec) :: + ("fundingTxIndex" | uint32) :: + ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("localCommit" | either(bool8, unsignedLocalCommitWithTxsCodec, localCommitWithTxsCodec(commitmentSpecCodec))) :: + ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] + val waitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: ("fundingTxIndex" | uint32) :: @@ -701,17 +753,19 @@ private[channel] object ChannelCodecs4 { ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] - (waitingForSigsWithoutLiquidityPurchaseCodec, waitingForSigsCodec) + (waitingForSigsWithoutLiquidityPurchaseCodec, waitingForSigsWithTxsCodec, waitingForSigsCodec) } val dualFundingStatusCodec: Codec[DualFundingStatus] = discriminated[DualFundingStatus].by(uint8) .\(0x01) { case status: DualFundingStatus if !status.isInstanceOf[DualFundingStatus.RbfWaitingForSigs] => DualFundingStatus.WaitingForConfirmations }(provide(DualFundingStatus.WaitingForConfirmations)) - .\(0x03) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[DualFundingStatus.RbfWaitingForSigs]) + .\(0x04) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[DualFundingStatus.RbfWaitingForSigs]) + .\(0x03) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsWithTxsCodec.as[DualFundingStatus.RbfWaitingForSigs]) .\(0x02) { case status: DualFundingStatus.RbfWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[DualFundingStatus.RbfWaitingForSigs]) val spliceStatusCodec: Codec[SpliceStatus] = discriminated[SpliceStatus].by(uint8) .\(0x01) { case status: SpliceStatus if !status.isInstanceOf[SpliceStatus.SpliceWaitingForSigs] => SpliceStatus.NoSplice }(provide(SpliceStatus.NoSplice)) - .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x04) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) + .\(0x03) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsWithTxsCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) .\(0x02) { case status: SpliceStatus.SpliceWaitingForSigs => status }(interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec.as[channel.SpliceStatus.SpliceWaitingForSigs]) private val shortids: Codec[ChannelTypes4.ShortIds] = ( @@ -753,6 +807,14 @@ private[channel] object ChannelCodecs4 { ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_13_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( + ("channelParams" | paramsCodec) :: + ("secondRemotePerCommitmentPoint" | publicKey) :: + ("localPushAmount" | millisatoshi) :: + ("remotePushAmount" | millisatoshi) :: + ("status" | interactiveTxWaitingForSigsWithTxsCodec) :: + ("remoteChannelData_opt" | optional(bool8, varsizebinarydata))).as[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + + val DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_1c_Codec: Codec[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] = ( ("channelParams" | paramsCodec) :: ("secondRemotePerCommitmentPoint" | publicKey) :: ("localPushAmount" | millisatoshi) :: @@ -871,6 +933,14 @@ private[channel] object ChannelCodecs4 { ("remoteOnly_opt" | optional(bool8, closingTxCodec))).as[ClosingTxs] val DATA_NEGOTIATING_SIMPLE_17_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( + ("commitments" | commitmentsCodecWithLocalTxs) :: + ("lastClosingFeerate" | feeratePerKw) :: + ("localScriptPubKey" | varsizebinarydata) :: + ("remoteScriptPubKey" | varsizebinarydata) :: + ("proposedClosingTxs" | listOfN(uint16, closingTxsCodec)) :: + ("publishedClosingTxs" | listOfN(uint16, closingTxCodec))).as[DATA_NEGOTIATING_SIMPLE] + + val DATA_NEGOTIATING_SIMPLE_1d_Codec: Codec[DATA_NEGOTIATING_SIMPLE] = ( ("commitments" | commitmentsCodec) :: ("lastClosingFeerate" | feeratePerKw) :: ("localScriptPubKey" | varsizebinarydata) :: @@ -937,6 +1007,8 @@ private[channel] object ChannelCodecs4 { // Order matters! val channelDataCodec: Codec[PersistentChannelData] = discriminated[PersistentChannelData].by(uint16) + .typecase(0x1d, Codecs.DATA_NEGOTIATING_SIMPLE_1d_Codec) + .typecase(0x1c, Codecs.DATA_WAIT_FOR_DUAL_FUNDING_SIGNED_1c_Codec) .typecase(0x1b, Codecs.DATA_CLOSING_1b_Codec) .typecase(0x1a, Codecs.DATA_CLOSING_1a_Codec) .typecase(0x19, Codecs.DATA_SHUTDOWN_19_Codec) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 3018c02032..ea9d823b16 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -16,12 +16,14 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.{BlockHash, ByteVector32, ByteVector64, Satoshi, Transaction, TxHash, TxId} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.{ChannelFlags, ShortIdAliases} import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, Features, InitFeature, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampSecond, UInt64, UnspecifiedShortChannelId} +import fr.acinq.secp256k1.Secp256k1 import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -156,6 +158,11 @@ object CommonCodecs { val publicKey: Codec[PublicKey] = catchAllCodec(bytes(33).xmap(bin => PublicKey(bin), pub => pub.value)) + val publicNonce: Codec[IndividualNonce] = Codec[IndividualNonce]( + (pub: IndividualNonce) => bytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE).encode(ByteVector.view(pub.toByteArray)), + (wire: BitVector) => bytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE).decode(wire).map(_.map(b => new IndividualNonce(b.toArray))) + ) + val rgb: Codec[Color] = bytes(3).xmap(buf => Color(buf(0), buf(1), buf(2)), t => ByteVector(t.r, t.g, t.b)) val txCodec: Codec[Transaction] = bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d)) diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json index 0021f36d05..54f68dee87 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/fundee/data.json @@ -85,14 +85,15 @@ "toLocal" : 204739729, "toRemote" : 16572475271 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4", - "tx" : "020000000107738f22c16a24795bcaebc6e09016af61902dd66bbaf64e6e5db50b0c45d63d010000000032c3698002c31f0300000000002200205cc91746133145180585bfb3bb9a1c1740c9b43338aa30c90b5f5652d729ce0884dffc0000000000160014cfb373f55b722ca1c028d63ee85cb82c00ce11127af8a620" - }, - "remoteSig" : "4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d" + "txId" : "e25a866b79212015e01e155e530fb547abc8276869f8740a9948e52ca231f1e4", + "input" : { + "outPoint" : "3dd6450c0bb55d6e4ef6ba6bd62d9061af1690e0c6ebca5b79246ac1228f7307:1", + "amountSatoshis" : 16777215 + }, + "remoteSig" : { + "sig" : "4d4d24b8cb3a00dfd685ac73e3c85ba26449dc935469ce36c259f2db6cd519a865845eca78a998bc8213044e84eca0c884cdb01bda8b6e70f5c1ff821ca5388d" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 7779, @@ -102,7 +103,7 @@ "toLocal" : 16572475271, "toRemote" : 204739729 }, - "txid" : "ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be", + "txId" : "ac994c4f64875ab22b45cba175a04cec4051bbe660932570744dad822e6bf8be", "remotePerCommitmentPoint" : "03daadaed37bcfed40d15e34979fbf2a0643e748e8960363bb8e930cefe2255c35" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json index d5fe938497..56a91bdd4a 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/000003-DATA_NORMAL/funder/data.json @@ -85,14 +85,15 @@ "toLocal" : 1343316620, "toRemote" : 13656683380 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43", - "tx" : "02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320" - }, - "remoteSig" : "bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f" + "txId" : "65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43", + "input" : { + "outPoint" : "115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0", + "amountSatoshis" : 15000000 + }, + "remoteSig" : { + "sig" : "bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 20024, @@ -102,7 +103,7 @@ "toLocal" : 13656683380, "toRemote" : 1343316620 }, - "txid" : "919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49", + "txId" : "919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49", "remotePerCommitmentPoint" : "02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json index b723107096..e552713d4a 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/020002-DATA_NORMAL/funder/data.json @@ -99,14 +99,15 @@ "toLocal" : 15000000000, "toRemote" : 0 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f", - "tx" : "02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20" - }, - "remoteSig" : "871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b" + "txId" : "fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f", + "input" : { + "outPoint" : "1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0", + "amountSatoshis" : 15000000 + }, + "remoteSig" : { + "sig" : "871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 4, @@ -116,7 +117,7 @@ "toLocal" : 0, "toRemote" : 15000000000 }, - "txid" : "b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8", + "txId" : "b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8", "remotePerCommitmentPoint" : "02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json index 677a22cfdf..aaac6823b9 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/030000-DATA_WAIT_FOR_FUNDING_CONFIRMED/funder/data.json @@ -93,14 +93,15 @@ "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "c6fe6bc0a5a9c149a03a907d2351714aa27fc98a485e981343cea08a1904ee26", - "tx" : "0200000001e917adc681383fe00f779c6144a1bd91135ba2c9862ad1bc5aa8a14d37bae3f40000000000fd11418002400d0300000000001600142276cff9d96f4696d6e504568db62088428706e0b8180c0000000000220020ad1b593fc0780225407ba65c612b974b4b66610e129f21064cb8ada0ffe4c8d2b7262120" - }, - "remoteSig" : "1ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b4" + "txId" : "c6fe6bc0a5a9c149a03a907d2351714aa27fc98a485e981343cea08a1904ee26", + "input" : { + "outPoint" : "f4e3ba374da1a85abcd12a86c9a25b1391bda144619c770fe03f3881c6ad17e9:0", + "amountSatoshis" : 1000000 + }, + "remoteSig" : { + "sig" : "1ea9cffd2af82f6c14251bd59ffa3e876178c7b263f16d12790c33fc093eaed053dd9e0d49f79558aeb3c2fe7fe84f95a91eecbea2679fb65916ad9632df07b4" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -110,7 +111,7 @@ "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "10e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290", + "txId" : "10e4205393672b61bbde4261441a2cf8d08ae50fdb813df8efaac6c6090ef290", "remotePerCommitmentPoint" : "032a992c123095216f7937a8b0baf442211eeb57942d586854a61a0dc6b01ca6ee" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json index bf125aac53..19dc177430 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/03000a-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json @@ -94,14 +94,15 @@ "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "a591186503570022767b96fc4e02448e2c2d95f9e83f06c526f810886b02dedd", - "tx" : "020000000183a6dea8adf975b60df17738937034a2f96cb8b784da01e2934e9e172244317d0000000000fc10bb8002400d03000000000016001434b58af5c6bf8472898ef7ed44f65bce2e4a6101b8180c00000000002200206bf6eeedf2e93dee452c92c0c1e2421cc064bf8f70b432c6e36459215576d24decab0320" - }, - "remoteSig" : "8ccca40bbf6c859ceb2d5288153801426e19f8b119c2d1d4ba6d82511c7816aa0eaa8e41eafb179818d1ba7b0cfdce2da5fa002921df00a1fb4e1458d0b9042c" + "txId" : "a591186503570022767b96fc4e02448e2c2d95f9e83f06c526f810886b02dedd", + "input" : { + "outPoint" : "7d314422179e4e93e201da84b7b86cf9a23470933877f10db675f9ada8dea683:0", + "amountSatoshis" : 1000000 + }, + "remoteSig" : { + "sig" : "8ccca40bbf6c859ceb2d5288153801426e19f8b119c2d1d4ba6d82511c7816aa0eaa8e41eafb179818d1ba7b0cfdce2da5fa002921df00a1fb4e1458d0b9042c" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -111,7 +112,7 @@ "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "839b92423077adb38eeaa118dd6ba0e6daf8c1e0add666ddd18f57b517a2d750", + "txId" : "839b92423077adb38eeaa118dd6ba0e6daf8c1e0add666ddd18f57b517a2d750", "remotePerCommitmentPoint" : "0324b50221ad635b97f597802fbe5b2d6414fdf41f224ac1869d3772314e9fbfa5" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json index 702dd3c5b5..78c97bb722 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json @@ -100,14 +100,15 @@ "toLocal" : 1000000000, "toRemote" : 500000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "b543f86a2a9a06f80b7339f9aaa169a779a7754d3523c28845d10dad9b9d7bc9", - "tx" : "0200000001db1c7ef42a989448f5599588b8a3213df36f9ed732a33043a45cab7773274374000000000058951280044a010000000000002200203a52179d307a973d00b2b102a0df754258ba7f0d1011a2a5d9e2844004b76d8a4a01000000000000220020a8b562e803e0523f85ff90ff8f90571a03c016bb61f387016d16db99f539c54e20a107000000000022002084bbfa5dee3d11a165c2ec7c0ce8b66e055fd7b6d90be6f0162dd4887cc25226b2340f00000000002200207fafaeecce02b12fad250d513a7493fc21bc9f4b82c7bbc115de3038cf984f933e9c0820" - }, - "remoteSig" : "5d65b42b0a8f05ca2793360a6b34bd5c1b9d960cf62c300c0e493f9cd83d2ca41f46a663d4fc11891ef268c0200b8fea8fb454d9055d35c2d6f5ba0b14c78b2a" + "txId" : "b543f86a2a9a06f80b7339f9aaa169a779a7754d3523c28845d10dad9b9d7bc9", + "input" : { + "outPoint" : "7443277377ab5ca44330a332d79e6ff33d21a3b8889559f54894982af47e1cdb:0", + "amountSatoshis" : 1500000 + }, + "remoteSig" : { + "sig" : "5d65b42b0a8f05ca2793360a6b34bd5c1b9d960cf62c300c0e493f9cd83d2ca41f46a663d4fc11891ef268c0200b8fea8fb454d9055d35c2d6f5ba0b14c78b2a" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -117,7 +118,7 @@ "toLocal" : 500000000, "toRemote" : 1000000000 }, - "txid" : "990138002f6fc36ae3ea48b88e2302a80eb925c33668e59638a82840f1e633ad", + "txId" : "990138002f6fc36ae3ea48b88e2302a80eb925c33668e59638a82840f1e633ad", "remotePerCommitmentPoint" : "037d0b91e7bf58eec2eddf033d457b17140a341533808a346c869ada9ecea0cec0" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json index 7e8e183d49..bca747bcc4 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/fundee/data.json @@ -106,14 +106,15 @@ "toLocal" : 200000000, "toRemote" : 800000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", - "tx" : "02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d0300000000002200207d4136c71e1c702d6e56ea300e1a56298b70fc8e96d67e72cfab3a27d39c9bf2b8180c0000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3ac678920" - }, - "remoteSig" : "02b8ce0a760ae6052a0d6c5770ae626ae71ea1b64853f97bb2c784839264054c64dc8717789f804342cbd9c5a364b32f4cbd9e9a4f91f48127a58f95014f6663" + "txId" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", + "input" : { + "outPoint" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d:0", + "amountSatoshis" : 1000000 + }, + "remoteSig" : { + "sig" : "02b8ce0a760ae6052a0d6c5770ae626ae71ea1b64853f97bb2c784839264054c64dc8717789f804342cbd9c5a364b32f4cbd9e9a4f91f48127a58f95014f6663" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -123,7 +124,7 @@ "toLocal" : 800000000, "toRemote" : 200000000 }, - "txid" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", + "txId" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", "remotePerCommitmentPoint" : "02e813fa8f4480fcbfa92d65037fe2d5d99cb72d6987381cf253d13f071a45f049" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json index c190f0a5be..bc335c7da4 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000b-DATA_WAIT_FOR_CHANNEL_READY/funder/data.json @@ -106,14 +106,15 @@ "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", - "tx" : "02000000017d975ecb75e1497076150f745b83dace95a189eecb6172c20a9a4fe0f91b8d1d000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002029b18fc4cf2f2b4634802a1b950d9e27e93b9be9ce638351143432fd0db09163ac678920" - }, - "remoteSig" : "8d3c57514dd1c01073b3d3704eddcfd8ca6ac3bb58f145f2c2dd921b77f238e842dce706a711b69b250e9ec3b827a9d9e144720a41fadce820bb18280667bffd" + "txId" : "ad28d946d13d331539d08da9d131311d1bec7308ddf072a59b07b2dc22779c3f", + "input" : { + "outPoint" : "1d8d1bf9e04f9a0ac27261cbee89a195ceda835b740f15767049e175cb5e977d:0", + "amountSatoshis" : 1000000 + }, + "remoteSig" : { + "sig" : "8d3c57514dd1c01073b3d3704eddcfd8ca6ac3bb58f145f2c2dd921b77f238e842dce706a711b69b250e9ec3b827a9d9e144720a41fadce820bb18280667bffd" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -123,7 +124,7 @@ "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", + "txId" : "d873d760728d9cf12d61c43360c383ab83799c9153f38c6012d9d6f6e6026df1", "remotePerCommitmentPoint" : "026b84590050c242d572dd2a45b873ce1e108303e1a54a6492e641f3ec1a70a7aa" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json index 8ef01bebf6..1c8cbee034 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/fundee/data.json @@ -108,14 +108,15 @@ "toLocal" : 500000000, "toRemote" : 1000000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", - "tx" : "0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a10700000000002200200e84b31c024b1498353109b6127546c1c1999edf47edb4c772c2108edeb524b4b2340f0000000000220020c38ddef3ceb25118b96983889833cbecd3fdeaf190083eef04ee451b70b47e04a71aae20" - }, - "remoteSig" : "c70d3fc3e720cc4fd1a2e07b97d454392ecfbe27c0ef2eed401538466924f6c24accb834eed784536fe5d30656eb8308a8b0f48bdba2ccc9812ce49a12b9dd80" + "txId" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", + "input" : { + "outPoint" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393:0", + "amountSatoshis" : 1500000 + }, + "remoteSig" : { + "sig" : "c70d3fc3e720cc4fd1a2e07b97d454392ecfbe27c0ef2eed401538466924f6c24accb834eed784536fe5d30656eb8308a8b0f48bdba2ccc9812ce49a12b9dd80" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -125,7 +126,7 @@ "toLocal" : 1000000000, "toRemote" : 500000000 }, - "txid" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", + "txId" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", "remotePerCommitmentPoint" : "02df0aedf5ee7eba9dbe640dfd3b2da6e108cf52568e4de3e492ed4366355a33c0" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json index 1d8b1d5a7e..9f22731a67 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY/funder/data.json @@ -108,14 +108,15 @@ "toLocal" : 1000000000, "toRemote" : 500000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", - "tx" : "0200000001934351ffe48993f070a665d6e22a2c023b567ccc2f73489213cfeb6c9a2c05e000000000001a741d80044a010000000000002200209698cbfb709ad73804217ae67e86ea857e8c1b317c82e7740670eb6def38ea334a01000000000000220020d468d18ef4bd63800e3e59bbb204a2921dc75cec9786e363ba2d33d467ae98e820a1070000000000220020fbc40c7329e9ed94bb1a94bed8d66eba59e96aba45dc5d59150b6ac6ab24436ab2340f00000000002200201e1e972764d9ad2836c50f22bcb80b031dd989239b89db8d3c62c83e14ad88c0a71aae20" - }, - "remoteSig" : "353c51755a0c852a4c5dd9e2d33d4a9c0934e9a07a43da89f061a3b4499f374d1857dfa513ed714287baf4ac3f4375f06bf435577962f121182ad9ce4b19f549" + "txId" : "c98fac755c41c1688a55f820a3ac9c4c8b2adde1d6687b2a9d843b364211965f", + "input" : { + "outPoint" : "e0052c9a6cebcf139248732fcc7c563b022c2ae2d665a670f09389e4ff514393:0", + "amountSatoshis" : 1500000 + }, + "remoteSig" : { + "sig" : "353c51755a0c852a4c5dd9e2d33d4a9c0934e9a07a43da89f061a3b4499f374d1857dfa513ed714287baf4ac3f4375f06bf435577962f121182ad9ce4b19f549" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -125,7 +126,7 @@ "toLocal" : 500000000, "toRemote" : 1000000000 }, - "txid" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", + "txId" : "800a5eab3f585d54f19d6e574b611d94fa7414af651e17104310bf004ccd5cd6", "remotePerCommitmentPoint" : "03a59c87106cab1b24df7f9a3843a728a45bca74680f65118ca6c5a3d45c1934e6" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json index 40bc72d116..978c58072e 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/announced/data.json @@ -106,14 +106,15 @@ "toLocal" : 800000000, "toRemote" : 200000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "41418383164935b0753fa1f9b128dff2f1b3608e6b50e8beea4525edf7c3959d", - "tx" : "0200000001c380aa11700db0a6d797dfd0be8aecfadad9397e4975f0fa8c9d10db71feac38000000000036a2ab8002400d030000000000160014761879f7b274ce995f87150a02e75cc0c037e8e3b8180c000000000022002010a0e90d648e62c6aa0d9b9883ee391449f1a1ce28926eb26c3bbdf76c25f631ac678920" - }, - "remoteSig" : "62de565629126afb02a76e67ddf18dfc883cf93bfeb7c807826dd6c8d1bd1f4d55528717f0d2303dc254b4512e32ce82dc7947d5d8e4d38647c2407fc53b874a" + "txId" : "41418383164935b0753fa1f9b128dff2f1b3608e6b50e8beea4525edf7c3959d", + "input" : { + "outPoint" : "38acfe71db109d8cfaf075497e39d9dafaec8abed0df97d7a6b00d7011aa80c3:0", + "amountSatoshis" : 1000000 + }, + "remoteSig" : { + "sig" : "62de565629126afb02a76e67ddf18dfc883cf93bfeb7c807826dd6c8d1bd1f4d55528717f0d2303dc254b4512e32ce82dc7947d5d8e4d38647c2407fc53b874a" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -123,7 +124,7 @@ "toLocal" : 200000000, "toRemote" : 800000000 }, - "txid" : "287a0627974d69a85199233f5cb8e86e7d04d8afdbc0faa4e56587833838c5af", + "txId" : "287a0627974d69a85199233f5cb8e86e7d04d8afdbc0faa4e56587833838c5af", "remotePerCommitmentPoint" : "03293b6fcd6474a6c8dff63eeed1ca704c15d5bb80177025c2c45a4f995723393d" } } ], diff --git a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json index 29fb6d0eb3..2239df8f75 100644 --- a/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json +++ b/eclair-core/src/test/resources/nonreg/codecs/04000e-DATA_NORMAL/splicing-private/data.json @@ -105,14 +105,15 @@ "toLocal" : 700000000, "toRemote" : 1800000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "ec1fdbdffd12f20851f33319fb3ab6ecb8528a34a9a2c76e53aaf91cf4a18da4", - "tx" : "020000000101310d33721f5dab9805cd15529a750a0e1d335e986f165306e9ac782b3883880000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160f85a1b0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "52ea4ba9421f4b4ad8a9a1e3041a940b13751504faf7d79338d73bbc1187fb551d9340de809849d6d7dcd745b813e01c07809cb8eacc57a211990b0c021d4c45" + "txId" : "ec1fdbdffd12f20851f33319fb3ab6ecb8528a34a9a2c76e53aaf91cf4a18da4", + "input" : { + "outPoint" : "8883382b78ace90653166f985e331d0e0a759a5215cd0598ab5d1f72330d3101:0", + "amountSatoshis" : 2500000 + }, + "remoteSig" : { + "sig" : "52ea4ba9421f4b4ad8a9a1e3041a940b13751504faf7d79338d73bbc1187fb551d9340de809849d6d7dcd745b813e01c07809cb8eacc57a211990b0c021d4c45" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -122,7 +123,7 @@ "toLocal" : 1800000000, "toRemote" : 700000000 }, - "txid" : "ac57fb7fa3b5493412ad83d7d056f269f54133e69026306d34a9ede545ef145c", + "txId" : "ac57fb7fa3b5493412ad83d7d056f269f54133e69026306d34a9ede545ef145c", "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" } }, { @@ -147,14 +148,15 @@ "toLocal" : 700000000, "toRemote" : 1300000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "27f7218158a68f5bb23c9d8a280f85bfca32d7636529e1db2ba23172241161f4", - "tx" : "020000000105303d3f65cce13af3e5908692cefadae10b3f1205ac9c23eb381088f56c55910000000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160d8b9130000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "32a246840dd67eb72b59fc170303c508c77ee8666ac6be36abced02bf79dbfbe114cdd4e56600ae6e3573e6e10d3517ddc857fbd89c00e2cbcdf5186c6592f26" + "txId" : "27f7218158a68f5bb23c9d8a280f85bfca32d7636529e1db2ba23172241161f4", + "input" : { + "outPoint" : "91556cf5881038eb239cac05123f0be1daface928690e5f33ae1cc653f3d3005:0", + "amountSatoshis" : 2000000 }, - "htlcTxsAndRemoteSigs" : [ ] + "remoteSig" : { + "sig" : "32a246840dd67eb72b59fc170303c508c77ee8666ac6be36abced02bf79dbfbe114cdd4e56600ae6e3573e6e10d3517ddc857fbd89c00e2cbcdf5186c6592f26" + }, + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -164,7 +166,7 @@ "toLocal" : 1300000000, "toRemote" : 700000000 }, - "txid" : "1f1392e4cee2d3fc5f4fed4afed2b962ea8df1a802dc9274043f337e8767838e", + "txId" : "1f1392e4cee2d3fc5f4fed4afed2b962ea8df1a802dc9274043f337e8767838e", "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" } }, { @@ -189,14 +191,15 @@ "toLocal" : 700000000, "toRemote" : 800000000 }, - "commitTxAndRemoteSig" : { - "commitTx" : { - "txid" : "381ee5a54f127b776114b197b2695c39f2d9b81cb967628a32e12766cdf8f1b5", - "tx" : "02000000015f212d2dd5363ebcf4ff99b4e490fef6436b983182d65fd3129d9a7ccf9812420200000000310038800260ae0a0000000000220020a1ca24b5ac3c96e066fa7466d34505e7e62bfe75017116d988328f97f4358160b8180c0000000000160014b8a6877e8036939b79fcebe6c478d4bb234656b4a0f85620" - }, - "remoteSig" : "47f525be7304ef4fa4d868eccfa2389fe1d8f058d08305b0f1792cebbd14ec7b6b43f98d0f3aa22e60d7ef024785f3576d0c1d74b666c7c5d2da88c7e7f7bd18" + "txId" : "381ee5a54f127b776114b197b2695c39f2d9b81cb967628a32e12766cdf8f1b5", + "input" : { + "outPoint" : "421298cf7c9a9d12d35fd68231986b43f6fe90e4b499fff4bc3e36d52d2d215f:2", + "amountSatoshis" : 1500000 + }, + "remoteSig" : { + "sig" : "47f525be7304ef4fa4d868eccfa2389fe1d8f058d08305b0f1792cebbd14ec7b6b43f98d0f3aa22e60d7ef024785f3576d0c1d74b666c7c5d2da88c7e7f7bd18" }, - "htlcTxsAndRemoteSigs" : [ ] + "htlcRemoteSigs" : [ ] }, "remoteCommit" : { "index" : 0, @@ -206,7 +209,7 @@ "toLocal" : 800000000, "toRemote" : 700000000 }, - "txid" : "ea2430e6f9d234b9ca79d5edbdd0edbe429aa31026ad79ecc207299a62151d33", + "txId" : "ea2430e6f9d234b9ca79d5edbdd0edbe429aa31026ad79ecc207299a62151d33", "remotePerCommitmentPoint" : "0362028d59fc5fd2598f1fad1df3933b64367fd6a16ba88a90d31d396b588eac2a" } } ], 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 62015c3c63..0191f68402 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} @@ -88,7 +88,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with fulfillHtlc(htlcb.id, rb, alice, bob, alice2bob, bob2alice) // Bob publishes his current commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) assert(bobCommitTx.txOut.size == 8) // two anchor outputs, two main outputs and 4 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) // In response to that, alice publishes her claim txs. @@ -136,7 +136,7 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // Bob publishes his next commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.last.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) assert(bobCommitTx.txOut.size == 7) // two anchor outputs, two main outputs and 3 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) // In response to that, alice publishes her claim txs 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..f1bf165a18 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 @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.states.ChannelStateTestsBase import fr.acinq.eclair.crypto.ShaChain -import fr.acinq.eclair.transactions.Transactions.{CommitTx, DefaultCommitmentFormat} +import fr.acinq.eclair.transactions.Transactions.DefaultCommitmentFormat import fr.acinq.eclair.transactions.{CommitmentSpec, Transactions} import fr.acinq.eclair.wire.protocol._ import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -496,7 +496,7 @@ object CommitmentsSpec { val remoteFundingPubKey = randomKey().publicKey val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingScript(localFundingPubKey, remoteFundingPubKey, DefaultCommitmentFormat).pubkeyScript)), 0) val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, ByteVector.empty) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), IndividualSignature(ByteVector64.Zeroes)), Nil) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toLocal, toRemote), randomTxId(), commitmentInput, IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, feeRatePerKw, toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(fundingTx, ann.shortChannelId, None, None) @@ -521,7 +521,7 @@ object CommitmentsSpec { val remoteFundingPubKey = randomKey().publicKey val fundingTx = Transaction(2, Nil, Seq(TxOut((toLocal + toRemote).truncateToSatoshi, Funding.makeFundingScript(localFundingPubKey, remoteFundingPubKey, DefaultCommitmentFormat).pubkeyScript)), 0) val commitmentInput = Transactions.InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, ByteVector.empty) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), IndividualSignature(ByteVector64.Zeroes)), Nil) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toLocal, toRemote), randomTxId(), commitmentInput, IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(0 sat), toRemote, toLocal), randomTxId(), randomKey().publicKey) val localFundingStatus = announcement_opt match { case Some(ann) => LocalFundingStatus.ConfirmedFundingTx(fundingTx, ann.shortChannelId, None, None) 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 0c989cf3a4..f24b0b075f 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 @@ -132,19 +132,19 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat def findTimedOutHtlcs(f: Fixture): Unit = { import f._ - val dustLimit = alice.underlyingActor.nodeParams.channelConf.dustLimit - val localCommit = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.localCommit + val localKeys = alice.underlyingActor.channelKeys + val localCommitment = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest + val localCommit = localCommitment.localCommit val remoteKeys = bob.underlyingActor.channelKeys val remoteCommitment = bob.stateData.asInstanceOf[DATA_CLOSING].commitments.latest val remoteCommit = remoteCommitment.remoteCommit - val commitmentFormat = remoteCommitment.params.commitmentFormat // Claim-HTLC txs can be modified to pay more (or less) fees by changing the output amount. val bobClaimHtlcTimeoutTxsModifiedFees = bobClaimHtlcTimeoutTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) val bobClaimHtlcSuccessTxsModifiedFees = bobClaimHtlcSuccessTxs.map(tx => tx.modify(_.tx.txOut).setTo(Seq(tx.tx.txOut.head.copy(amount = 5000 sat)))) val aliceTimedOutHtlcs = aliceHtlcTimeoutTxs.map(htlcTimeout => { - val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, htlcTimeout.tx) + val timedOutHtlcs = Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, htlcTimeout.tx) assert(timedOutHtlcs.size == 1) timedOutHtlcs.head }) @@ -164,14 +164,14 @@ class HelpersSpec extends TestKitBaseClass with AnyFunSuiteLike with ChannelStat }) assert(bobTimedOutHtlcs2.toSet == bobHtlcs) - aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, htlcSuccess.tx).isEmpty)) + aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, htlcSuccess.tx).isEmpty)) aliceHtlcSuccessTxs.foreach(htlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcSuccess.tx).isEmpty)) - bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) - bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcSuccess.tx).isEmpty)) + bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcSuccess.tx).isEmpty)) bobClaimHtlcSuccessTxs.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) bobClaimHtlcSuccessTxsModifiedFees.foreach(claimHtlcSuccess => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, claimHtlcSuccess.tx).isEmpty)) aliceHtlcTimeoutTxs.foreach(htlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(remoteKeys, remoteCommitment, remoteCommit, htlcTimeout.tx).isEmpty)) - bobClaimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(commitmentFormat, localCommit, dustLimit, claimHtlcTimeout.tx).isEmpty)) + bobClaimHtlcTimeoutTxs.foreach(claimHtlcTimeout => assert(Closing.trimmedOrTimedOutHtlcs(localKeys, localCommitment, localCommit, claimHtlcTimeout.tx).isEmpty)) } test("find timed out htlcs (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index b416b8fc78..4173045e22 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -188,8 +188,8 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w def closeChannelWithoutHtlcs(f: Fixture, overrideCommitTarget: BlockHeight): (PublishFinalTx, PublishReplaceableTx) = { import f._ - val signedCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys) - val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.copy(tx = signedCommitTx) + val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest + val commitTx = CommitTx(commitment.commitInput, commitment.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys)) probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] @@ -197,7 +197,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w val publishCommitTx = alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) // Forward the anchor tx to the publisher. val publishAnchor = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideCommitTarget)) - assert(publishAnchor.tx.commitTx == signedCommitTx) + assert(publishAnchor.tx.commitTx == commitTx.tx) assert(publishAnchor.tx.isInstanceOf[ReplaceableLocalCommitAnchor]) (publishCommitTx, publishAnchor) @@ -337,7 +337,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob2alice.expectMsgType[RevokeAndAck] bob2alice.expectMsgType[CommitSig] assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.nonEmpty) - val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txid + val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txId val nextRemoteCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) assert(nextRemoteCommit.txid == nextRemoteCommitTxId) @@ -485,7 +485,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w if (nextCommit) { assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.nonEmpty) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.map(_.commit.txid).contains(commitTx.txid)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.nextRemoteCommit_opt.map(_.commit.txId).contains(commitTx.txid)) } val targetFeerate = FeeratePerKw(3000 sat) @@ -1026,7 +1026,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w bob2alice.expectMsgType[RevokeAndAck] bob2alice.expectMsgType[CommitSig] assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.nonEmpty) - val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txid + val nextRemoteCommitTxId = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.nextRemoteCommit_opt.get.commit.txId // Force-close channel. probe.send(alice, CMD_FORCECLOSE(probe.ref)) @@ -1068,16 +1068,16 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w probe.expectMsgType[CommandSuccess[CMD_FULFILL_HTLC]] // Force-close channel and verify txs sent to watcher. - val signedCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys) - val commitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.copy(tx = signedCommitTx) - assert(signedCommitTx.txOut.size == 6) + val commitment = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest + val commitTx = CommitTx(commitment.commitInput, commitment.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys)) + assert(commitTx.tx.txOut.size == 6) probe.send(alice, CMD_FORCECLOSE(probe.ref)) probe.expectMsgType[CommandSuccess[CMD_FORCECLOSE]] // We make the commit tx confirm because htlc txs have a relative delay. alice2blockchain.expectMsg(PublishFinalTx(commitTx, commitTx.fee, None)) - wallet.publishTransaction(signedCommitTx).pipeTo(probe.ref) - probe.expectMsg(signedCommitTx.txid) + wallet.publishTransaction(commitTx.tx).pipeTo(probe.ref) + probe.expectMsg(commitTx.tx.txid) generateBlocks(1) val anchor = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] @@ -1086,11 +1086,11 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w assert(htlcSuccess.tx.isInstanceOf[ReplaceableHtlcSuccess]) val htlcTimeout = alice2blockchain.expectMsgType[PublishReplaceableTx].copy(confirmationTarget = ConfirmationTarget.Absolute(overrideHtlcTarget)) assert(htlcTimeout.tx.isInstanceOf[ReplaceableHtlcTimeout]) - alice2blockchain.expectWatchTxConfirmed(signedCommitTx.txid) + alice2blockchain.expectWatchTxConfirmed(commitTx.tx.txid) alice2blockchain.expectWatchOutputsSpent(Seq(main.input, anchor.txInfo.input.outPoint, htlcSuccess.input, htlcTimeout.input)) alice2blockchain.expectNoMessage(100 millis) - (signedCommitTx, htlcSuccess, htlcTimeout) + (commitTx.tx, htlcSuccess, htlcTimeout) } test("not enough funds to increase htlc tx feerate") { 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 554d088584..fb27dfb85a 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 @@ -559,13 +559,6 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } def localClose(s: TestFSMRef[ChannelState, ChannelData, Channel], s2blockchain: TestProbe, htlcSuccessCount: Int = 0, htlcTimeoutCount: Int = 0): (LocalCommitPublished, PublishedForceCloseTxs) = { - // an error occurs and s publishes its commit tx - val localCommit = s.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - // check that we store the local txs without sigs - localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - localCommit.htlcTxsAndRemoteSigs.foreach(_.htlcTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull))) - - val commitTx = localCommit.commitTxAndRemoteSig.commitTx.tx s ! Error(ByteVector32.Zeroes, "oops") eventually(assert(s.stateName == CLOSING)) val closingState = s.stateData.asInstanceOf[DATA_CLOSING] @@ -576,11 +569,10 @@ trait ChannelStateTestsBase extends Assertions with Eventually { assert(localCommitPublished.htlcs.values.collect { case i: IncomingHtlcId => i }.size >= htlcSuccessCount) assert(localCommitPublished.htlcs.values.collect { case i: OutgoingHtlcId => i }.size == htlcTimeoutCount) - val publishedLocalCommitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx - assert(publishedLocalCommitTx.txid == commitTx.txid) - assert(publishedLocalCommitTx.wtxid != commitTx.wtxid) + val commitTx = s2blockchain.expectFinalTxPublished("commit-tx").tx + assert(commitTx.txid == closingState.commitments.latest.localCommit.txId) val commitInput = closingState.commitments.latest.commitInput - Transaction.correctlySpends(publishedLocalCommitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + Transaction.correctlySpends(commitTx, Map(commitInput.outPoint -> commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) val publishedAnchorTx_opt = closingState.commitments.params.commitmentFormat match { case DefaultCommitmentFormat => None case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => Some(s2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor].txInfo.tx) @@ -604,7 +596,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { // all htlcs success/timeout should be published as replaceable txs val publishedHtlcTxs = (0 until htlcSuccessCount + htlcTimeoutCount).map { _ => val htlcTx = s2blockchain.expectReplaceableTxPublished[ReplaceableHtlc] - assert(htlcTx.commitTx == publishedLocalCommitTx) + assert(htlcTx.commitTx == commitTx) assert(localCommitPublished.htlcOutputs.contains(htlcTx.txInfo.input.outPoint)) htlcTx } @@ -615,7 +607,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually { } assert(publishedHtlcSuccessTxs.size == htlcSuccessCount) assert(publishedHtlcTimeoutTxs.size == htlcTimeoutCount) - (publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(htlcTx => Transaction.correctlySpends(htlcTx, publishedLocalCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) + (publishedHtlcSuccessTxs ++ publishedHtlcTimeoutTxs).foreach(htlcTx => Transaction.correctlySpends(htlcTx, commitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) // we're not claiming the outputs of htlc txs yet assert(localCommitPublished.htlcDelayedOutputs.isEmpty) @@ -705,6 +697,10 @@ object ChannelStateTestsBase { implicit class PimpTestFSM(private val channel: TestFSMRef[ChannelState, ChannelData, Channel]) { val nodeParams: NodeParams = channel.underlyingActor.nodeParams + def signCommitTx(): Transaction = channel.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.fullySignedLocalCommitTx(channel.underlyingActor.channelKeys) + + def htlcTxs(): Seq[HtlcTx] = channel.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.latest.htlcTxs(channel.underlyingActor.channelKeys).map(_._1) + def setBitcoinCoreFeerates(feerates: FeeratesPerKw): Unit = channel.underlyingActor.nodeParams.setBitcoinCoreFeerates(feerates) def setBitcoinCoreFeerate(feerate: FeeratePerKw): Unit = setBitcoinCoreFeerates(FeeratesPerKw.single(feerate)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala index 7751424204..d424d07832 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForChannelReadyStateSpec.scala @@ -24,6 +24,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.publish.TxPublisher +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements @@ -252,7 +253,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(tx) alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx] alice2blockchain.expectMsgType[TxPublisher.PublishTx] @@ -270,7 +271,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv Error") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) @@ -281,7 +282,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "funding double-spent") bobListener.expectMsgType[ChannelAborted] awaitCond(bob.stateName == CLOSING) @@ -300,7 +301,7 @@ class WaitForChannelReadyStateSpec extends TestKitBaseClass with FixtureAnyFunSu test("recv CMD_FORCECLOSE") { f => import f._ val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_CHANNEL_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala index c76bdf3251..b160a669e8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingConfirmedStateSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.channel.fsm.Channel.ProcessCurrentBlockHeight import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId import fr.acinq.eclair.channel.publish.{ReplaceableLocalCommitAnchor, ReplaceableRemoteCommitAnchor, TxPublisher} -import fr.acinq.eclair.channel.states.ChannelStateTestsBase.FakeTxPublisherFactory +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.{FakeTxPublisherFactory, PimpTestFSM} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.testutils.PimpTestProbe.convert import fr.acinq.eclair.transactions.Transactions @@ -749,13 +749,13 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture awaitCond(alice.stateData.isInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY]) assert(alice.stateName == OFFLINE) // Bob broadcasts his commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - alice ! WatchFundingSpentTriggered(bobCommitTx.tx) + val bobCommitTx = bob.signCommitTx() + alice ! WatchFundingSpentTriggered(bobCommitTx) aliceListener.expectMsgType[TransactionPublished] alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimMain = alice2blockchain.expectFinalTxPublished("remote-main-delayed") - Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice2blockchain.expectWatchTxConfirmed(bobCommitTx.tx.txid) + Transaction.correctlySpends(claimMain.tx, Seq(bobCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx.txid) alice2blockchain.expectWatchOutputSpent(claimMain.input) aliceListener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) @@ -765,10 +765,10 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx1 = bob.signCommitTx() val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx2 = bob.signCommitTx() assert(bobCommitTx1.txid != bobCommitTx2.txid) alice ! INPUT_DISCONNECTED @@ -803,12 +803,13 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val bobData = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] val fundingTx = aliceData.latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx + val aliceCommitTx = alice.signCommitTx() + val bobCommitTx = bob.signCommitTx() val (alice2, bob2) = restartNodes(f, aliceData, bobData) alice2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - val bobCommitTx = bobData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx alice2 ! WatchFundingSpentTriggered(bobCommitTx) alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimMainAlice = alice2blockchain.expectFinalTxPublished("remote-main-delayed") @@ -820,7 +821,6 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture bob2 ! WatchFundingConfirmedTriggered(BlockHeight(42000), 42, fundingTx) assert(bobListener.expectMsgType[TransactionConfirmed].tx == fundingTx) assert(bob2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx.txid) - val aliceCommitTx = aliceData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx bob2 ! WatchFundingSpentTriggered(aliceCommitTx) bob2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimMainBob = bob2blockchain.expectFinalTxPublished("remote-main-delayed") @@ -834,11 +834,11 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx1 = alice.signCommitTx() + val bobCommitTx1 = bob.signCommitTx() val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx2 = bob.signCommitTx() assert(bobCommitTx1.txid != bobCommitTx2.txid) val aliceData = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED] @@ -1099,7 +1099,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv Error", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) assert(alice2blockchain.expectMsgType[TxPublisher.PublishFinalTx].tx.txid == tx.txid) @@ -1109,7 +1109,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv Error (remote commit published)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "force-closing channel, bye-bye") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] @@ -1121,7 +1121,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture alice2blockchain.expectWatchOutputSpent(claimMainLocal.input) alice2blockchain.expectWatchOutputSpent(anchorTxLocal.txInfo.input.outPoint) // Bob broadcasts his commit tx as well. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) val anchorTxRemote = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") @@ -1136,24 +1136,24 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture import f._ val fundingTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].latestFundingTx.sharedTx.asInstanceOf[FullySignedSharedTransaction].signedTx - val aliceCommitTx1 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(aliceCommitTx1.input.outPoint.txid == fundingTx1.txid) - assert(bobCommitTx1.input.outPoint.txid == fundingTx1.txid) + val aliceCommitTx1 = alice.signCommitTx() + val bobCommitTx1 = bob.signCommitTx() + assert(aliceCommitTx1.txIn.head.outPoint.txid == fundingTx1.txid) + assert(bobCommitTx1.txIn.head.outPoint.txid == fundingTx1.txid) val fundingTx2 = testBumpFundingFees(f) assert(fundingTx1.txid != fundingTx2.signedTx.txid) - val aliceCommitTx2 = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(aliceCommitTx2.input.outPoint.txid == fundingTx2.signedTx.txid) + val aliceCommitTx2 = alice.signCommitTx() + assert(aliceCommitTx2.txIn.head.outPoint.txid == fundingTx2.signedTx.txid) // Alice receives an error and force-closes using the latest funding transaction. alice ! Error(ByteVector32.Zeroes, "dual funding d34d") awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] - alice2blockchain.expectFinalTxPublished(aliceCommitTx2.tx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx2.txid) val anchorTx2 = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] val claimMain2 = alice2blockchain.expectFinalTxPublished("local-main-delayed") - Transaction.correctlySpends(claimMain2.tx, Seq(aliceCommitTx2.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.tx.txid) + Transaction.correctlySpends(claimMain2.tx, Seq(aliceCommitTx2), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx2.txid) alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx2.txInfo.input.outPoint, claimMain2.input)) // A previous funding transaction confirms, so Alice publishes the corresponding commit tx. @@ -1161,27 +1161,27 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture assert(aliceListener.expectMsgType[TransactionConfirmed].tx == fundingTx1) assert(alice2blockchain.expectMsgType[WatchFundingSpent].txId == fundingTx1.txid) alice2blockchain.expectMsg(UnwatchTxConfirmed(fundingTx2.txId)) - alice2blockchain.expectFinalTxPublished(aliceCommitTx1.tx.txid) + alice2blockchain.expectFinalTxPublished(aliceCommitTx1.txid) val anchorTx1 = alice2blockchain.expectReplaceableTxPublished[ReplaceableLocalCommitAnchor] val claimMain1 = alice2blockchain.expectFinalTxPublished("local-main-delayed") - Transaction.correctlySpends(claimMain1.tx, Seq(aliceCommitTx1.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice2blockchain.expectWatchTxConfirmed(aliceCommitTx1.tx.txid) + Transaction.correctlySpends(claimMain1.tx, Seq(aliceCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(aliceCommitTx1.txid) alice2blockchain.expectWatchOutputsSpent(Seq(anchorTx1.txInfo.input.outPoint, claimMain1.input)) testUnusedInputsUnlocked(wallet, Seq(fundingTx2)) // Bob publishes his commit tx, Alice reacts by spending her remote main output. - alice ! WatchFundingSpentTriggered(bobCommitTx1.tx) + alice ! WatchFundingSpentTriggered(bobCommitTx1) val anchorRemote = alice2blockchain.expectReplaceableTxPublished[ReplaceableRemoteCommitAnchor] val claimMainRemote = alice2blockchain.expectFinalTxPublished("remote-main-delayed") - Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx1.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.tx.txid) + Transaction.correctlySpends(claimMainRemote.tx, Seq(bobCommitTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + alice2blockchain.expectWatchTxConfirmed(bobCommitTx1.txid) alice2blockchain.expectWatchOutputsSpent(Seq(anchorRemote.txInfo.input.outPoint, claimMainRemote.input)) assert(alice.stateName == CLOSING) } test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.DualFunding), Tag(noFundingContribution)) { f => import f._ - val commitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "please help me recover my funds") // We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly. awaitCond(bob.stateName == CLOSING) @@ -1205,7 +1205,7 @@ class WaitForDualFundingConfirmedStateSpec extends TestKitBaseClass with Fixture test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding)) { f => import f._ val sender = TestProbe() - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) aliceListener.expectMsgType[ChannelAborted] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala index f39290ed27..ea72f10e28 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForDualFundingReadyStateSpec.scala @@ -23,11 +23,11 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.FullySignedSharedTransaction -import fr.acinq.eclair.channel.publish.{ReplaceableRemoteCommitAnchor, TxPublisher} import fr.acinq.eclair.channel.publish.TxPublisher.SetChannelId +import fr.acinq.eclair.channel.publish.{ReplaceableRemoteCommitAnchor, TxPublisher} +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import fr.acinq.eclair.transactions.Transactions.ClaimAnchorOutputTx import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{BlockHeight, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.OptionValues.convertOptionToValuable @@ -268,7 +268,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // bob publishes his commitment tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) assert(alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx].tx.isInstanceOf[ReplaceableRemoteCommitAnchor]) alice2blockchain.expectMsgType[TxPublisher.PublishTx] @@ -287,7 +287,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF test("recv Error", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "dual funding failure") listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) @@ -308,7 +308,7 @@ class WaitForDualFundingReadyStateSpec extends TestKitBaseClass with FixtureAnyF test("recv CMD_FORCECLOSE", Tag(ChannelStateTestsTags.DualFunding), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ val sender = TestProbe() - val commitTx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_READY].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitTx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) listener.expectMsgType[ChannelAborted] awaitCond(alice.stateName == CLOSING) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index d35d398660..88a4c7b116 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.{BITCOIN_FUNDING_PUBLISH_FAILED, BITCOIN_FUNDING_TIMEOUT} import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.publish.TxPublisher.PublishFinalTx +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Scripts.multiSig2of2 import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReady, Error, FundingCreated, FundingSigned, Init, OpenChannel, TlvStream} @@ -233,7 +234,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv WatchFundingSpentTriggered (remote commit)", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ // bob publishes his commitment tx - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(tx) assert(listener.expectMsgType[TransactionPublished].tx == tx) alice2blockchain.expectMsgType[TxPublisher.PublishReplaceableTx] @@ -252,7 +253,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv Error") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) listener.expectMsgType[ChannelAborted] @@ -263,7 +264,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv Error (nothing at stake)", Tag(ChannelStateTestsTags.NoPushAmount)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "please help me recover my funds") // We have nothing at stake, but we publish our commitment to help our peer recover their funds more quickly. awaitCond(bob.stateName == CLOSING) @@ -286,7 +287,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv CMD_FORCECLOSE") { f => import f._ val sender = TestProbe() - val tx = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CONFIRMED].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! CMD_FORCECLOSE(sender.ref) awaitCond(alice.stateName == CLOSING) listener.expectMsgType[ChannelAborted] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala index d5bdd3e0e5..8458ed6e8d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalQuiescentStateSpec.scala @@ -26,11 +26,12 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fund.InteractiveTxBuilder +import fr.acinq.eclair.channel.states.ChannelStateTestsBase.PimpTestFSM import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment.relay.Relayer.RelayForward import fr.acinq.eclair.testutils.PimpTestProbe.convert -import fr.acinq.eclair.transactions.Transactions.HtlcSuccessTx +import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{channel, _} import org.scalatest.Outcome @@ -444,10 +445,11 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL crossSign(alice, bob, alice2bob, bob2alice) initiateQuiescence(f, sendInitialStfu = true) - val aliceCommit = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - val commitTx = aliceCommit.commitTxAndRemoteSig.commitTx.tx - assert(aliceCommit.htlcTxsAndRemoteSigs.size == 1) - val htlcTimeoutTx = aliceCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = alice.signCommitTx() + val htlcTxs = alice.htlcTxs() + assert(htlcTxs.size == 1) + val htlcTimeoutTx = htlcTxs.head + assert(htlcTimeoutTx.isInstanceOf[HtlcTimeoutTx]) // the HTLC times out, alice needs to close the channel alice ! CurrentBlockHeight(add.cltvExpiry.blockHeight) @@ -468,10 +470,10 @@ class NormalQuiescentStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteL crossSign(alice, bob, alice2bob, bob2alice) initiateQuiescence(f, sendInitialStfu = true) - val bobCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - val commitTx = bobCommit.commitTxAndRemoteSig.commitTx.tx - assert(bobCommit.htlcTxsAndRemoteSigs.size == 1) - val htlcSuccessTx = bobCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcTxs = bob.htlcTxs() + assert(htlcTxs.size == 1) + val htlcSuccessTx = htlcTxs.head assert(htlcSuccessTx.isInstanceOf[HtlcSuccessTx]) // bob does not force-close unless there is a pending preimage for the incoming htlc diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala index d6b0204481..6e80aafc10 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala @@ -1141,8 +1141,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx1) - val commitAlice1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice1 = alice.signCommitTx() + val commitBob1 = bob.signCommitTx() // Bob sees the first splice confirm, but Alice doesn't. bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -1153,8 +1153,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice creates another splice spending the first splice. val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx2) - val commitAlice2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice2 = alice.signCommitTx() + val commitBob2 = bob.signCommitTx() assert(commitAlice1.txid != commitAlice2.txid) assert(commitBob1.txid != commitBob2.txid) @@ -1237,8 +1237,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik // Alice creates another splice spending the first splice. val fundingTx2 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) checkWatchConfirmed(f, fundingTx2) - val commitAlice2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx - val commitBob2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head.localCommit.commitTxAndRemoteSig.commitTx.tx + val commitAlice2 = alice.signCommitTx() + val commitBob2 = bob.signCommitTx() // Alice sees the second splice confirm. alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx2) @@ -2782,8 +2782,10 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchFundingSpent(fundingTx1.txid) // Bob publishes his commit tx for the first splice transaction (which double-spends the second splice transaction). - val bobCommitment1 = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.active.find(_.fundingTxIndex == 1).get - val bobCommitTx1 = bobCommitment1.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) + val bobCommitments = bob.stateData.asInstanceOf[ChannelDataWithCommitments].commitments + val previousCommitment = bobCommitments.active.find(_.fundingTxIndex == 1).get + val bobCommitTx1 = previousCommitment.fullySignedLocalCommitTx(bobCommitments.params, bob.underlyingActor.channelKeys) + val bobHtlcTxs = previousCommitment.htlcTxs(bobCommitments.params, bob.underlyingActor.channelKeys).map(_._1) Transaction.correctlySpends(bobCommitTx1, Seq(fundingTx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) alice ! WatchFundingSpentTriggered(bobCommitTx1) assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) @@ -2815,7 +2817,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateName == CLOSING) // Bob's 2nd-stage transactions confirm. - bobCommitment1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSig => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSig.htlcTx.tx)) + bobHtlcTxs.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) @@ -2832,7 +2834,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik val fundingTx1 = initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat, pushAmount = 10_000_000 msat)), spliceOut_opt = Some(SpliceOut(100_000 sat, defaultSpliceOutScriptPubKey))) checkWatchConfirmed(f, fundingTx1) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobRevokedCommitTx = bob.signCommitTx() // The first splice confirms on Bob's side. bob ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) @@ -2861,7 +2863,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice ! WatchFundingConfirmedTriggered(BlockHeight(400000), 42, fundingTx1) alice2blockchain.expectWatchFundingSpent(fundingTx1.txid) // Bob publishes a revoked commitment for fundingTx1! - val bobRevokedCommitTx = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // Alice watches bob's revoked commit tx, and force-closes with her latest commitment. assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) @@ -2928,7 +2929,8 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(alice, bob, alice2bob, bob2alice) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobCommitTx1 = bob.signCommitTx() + val bobHtlcTxs = bob.htlcTxs() initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat))) val fundingTx2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get @@ -2959,7 +2961,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 1) // bob publishes his latest (inactive) commitment for fundingTx1 - val bobCommitTx1 = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobCommitTx1) // alice watches bob's commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobCommitTx1.txid) @@ -2992,7 +2993,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik claimHtlcTimeout.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) assert(alice.stateName == CLOSING) // Bob's htlc-timeout txs confirm. - bobCommit1.localCommit.htlcTxsAndRemoteSigs.foreach(txAndSigs => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, txAndSigs.htlcTx.tx)) + bobHtlcTxs.foreach(htlcTx => alice ! WatchTxConfirmedTriggered(BlockHeight(400000), 42, htlcTx.tx)) alice2blockchain.expectNoMessage(100 millis) awaitCond(alice.stateName == CLOSED) @@ -3010,7 +3011,7 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik alice2blockchain.expectWatchPublished(fundingTx1.txid) bob2blockchain.expectWatchPublished(fundingTx1.txid) // remember bob's commitment for later - val bobCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.active.head + val bobRevokedCommitTx = bob.signCommitTx() // splice 1 gets published alice ! WatchPublishedTriggered(fundingTx1) bob ! WatchPublishedTriggered(fundingTx1) @@ -3061,7 +3062,6 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.active.size == 1) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.inactive.size == 1) // bob publishes his latest commitment for fundingTx1, which is now revoked - val bobRevokedCommitTx = bobCommit1.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! WatchFundingSpentTriggered(bobRevokedCommitTx) // alice watches bob's revoked commit tx, and force-closes with her latest commitment assert(alice2blockchain.expectMsgType[WatchAlternativeCommitTxConfirmed].txId == bobRevokedCommitTx.txid) @@ -3223,12 +3223,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(bob, alice, bob2alice, alice2bob) val aliceCommitments1 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys) + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments1.params, alice.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments1.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) + val commitTx = c.fullySignedLocalCommitTx(bobCommitments1.params, bob.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } @@ -3237,12 +3237,12 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik crossSign(alice, bob, alice2bob, bob2alice) val aliceCommitments2 = alice.stateData.asInstanceOf[DATA_NORMAL].commitments aliceCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(alice.underlyingActor.channelKeys) + val commitTx = c.fullySignedLocalCommitTx(aliceCommitments2.params, alice.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } val bobCommitments2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments bobCommitments2.active.foreach { c => - val commitTx = c.fullySignedLocalCommitTx(bob.underlyingActor.channelKeys) + val commitTx = c.fullySignedLocalCommitTx(bobCommitments2.params, bob.underlyingActor.channelKeys) Transaction.correctlySpends(commitTx, Map(c.commitInput.outPoint -> c.commitInput.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } 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 057d24b361..0fafc20900 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 @@ -633,7 +633,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (unexpected id)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) @@ -650,7 +650,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (value too small)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150 msat, randomBytes32(), cltvExpiry = CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] @@ -665,7 +665,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(Long.MaxValue), randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] @@ -680,7 +680,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 100000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) @@ -694,7 +694,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) @@ -711,7 +711,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) @@ -727,7 +727,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (over max inflight htlc value)", Tag(ChannelStateTestsTags.AliceLowMaxHtlcValueInFlight)) { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) == HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000 msat, actual = 151000000 msat).getMessage) @@ -741,7 +741,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateAddHtlc (over max accepted htlcs)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None)) @@ -852,7 +852,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 10000000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 10_000_000 msat, randomBytes32(), CltvExpiryDelta(144).toCltvExpiry(currentBlockHeight), TestConstants.emptyOnionPacket, None, 1.0, None, localOrigin(sender.ref)) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { @@ -866,9 +866,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val commitSig = alice2bob.expectMsgType[CommitSig] assert(commitSig.htlcSignatures.toSet.size == htlcCount) alice2bob.forward(bob) - awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == htlcCount) - val htlcTxs = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs - val amounts = htlcTxs.map(_.htlcTx.tx.txOut.head.amount.toLong) + awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == htlcCount) + val htlcTxs = bob.htlcTxs() + val amounts = htlcTxs.map(_.tx.txOut.head.amount.toLong) assert(amounts == amounts.sorted) } @@ -1040,7 +1040,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc.id)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.acked.size == 0) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.signed.size == 1) @@ -1063,7 +1063,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(outgoing).exists(_.id == htlc.id)) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) } @@ -1091,7 +1091,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 5) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 5) } test("recv CommitSig (multiple htlcs in both directions) (anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => @@ -1118,7 +1118,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 3) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 3) } test("recv CommitSig (multiple htlcs in both directions) (anchor outputs zero fee htlc txs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => @@ -1145,7 +1145,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice, bobCommitSig) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.index == 1) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 5) + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 5) } test("recv CommitSig (multiple htlcs in both directions) (without fundingTxId tlv)") { f => @@ -1205,14 +1205,14 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.htlcs.collect(incoming).exists(_.id == htlc1.id)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 2) + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 2) assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == initialState.commitments.latest.localCommit.spec.toLocal) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.count(_.amount == 50000.sat) == 2) + assert(bob.signCommitTx().txOut.count(_.amount == 50000.sat) == 2) } ignore("recv CommitSig (no changes)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // signature is invalid but it doesn't matter bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) val error = bob2alice.expectMsgType[Error] @@ -1228,7 +1228,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CommitSig (invalid signature)") { f => import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // actual test begins bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) @@ -1244,7 +1244,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] @@ -1263,7 +1263,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() alice ! CMD_SIGN() val commitSig = alice2bob.expectMsgType[CommitSig] @@ -1370,7 +1370,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (invalid preimage)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() @@ -1517,7 +1517,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv RevokeAndAck (unexpectedly)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo.isRight) alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] @@ -1589,7 +1589,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with def aliceToRemoteScript(): ByteVector = { val toRemoteAmount = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote - val Some(toRemoteOut) = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.amount == toRemoteAmount.truncateToSatoshi) + val Some(toRemoteOut) = alice.signCommitTx().txOut.find(_.amount == toRemoteAmount.truncateToSatoshi) toRemoteOut.publicKeyScript } @@ -1767,7 +1767,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, r) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -1780,7 +1780,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFulfillHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -1796,7 +1796,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob2relayer.expectMsgType[RelayForward] - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() // actual test begins alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, htlc.id, ByteVector32.Zeroes) @@ -2003,7 +2003,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.hash(htlc.onionRoutingPacket), 42) alice ! fail val error = alice2bob.expectMsgType[Error] @@ -2022,7 +2022,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // actual test begins - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, htlc.id, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -2035,7 +2035,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFailHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -2226,7 +2226,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (when sender is not funder)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -2239,7 +2239,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 tx = bob.signCommitTx() val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) // we first update the feerates so that we don't trigger a 'fee too different' error bob.setBitcoinCoreFeerate(fee.feeratePerKw) @@ -2256,7 +2256,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (sender can't afford it, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.HighFeerateMismatchTolerance)) { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // This feerate is just above the threshold: (800000 (alice balance) - 20000 (reserve) - 660 (anchors)) / 1124 (commit tx weight) = 693363 bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(693364 sat)) val error = bob2alice.expectMsgType[Error] @@ -2270,9 +2270,8 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val commitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - assert(initialState.commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.feeratePerKw) + val commitTx = bob.signCommitTx() + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate == TestConstants.feeratePerKw) alice2bob.send(bob, UpdateFee(ByteVector32.Zeroes, TestConstants.feeratePerKw / 2)) bob2alice.expectNoMessage(250 millis) // we don't close because the commitment doesn't contain any HTLC @@ -2318,7 +2317,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv UpdateFee (remote feerate is too small)") { f => import f._ val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments - val tx = bobCommitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() val expectedFeeratePerKw = bob.underlyingActor.nodeParams.onChainFeeConf.getCommitmentFeerate(bob.underlyingActor.nodeParams.currentBitcoinCoreFeerates, bob.underlyingActor.remoteNodeId, bobCommitments.params.commitmentFormat, bobCommitments.latest.capacity) assert(bobCommitments.latest.localCommit.spec.commitTxFeerate == expectedFeeratePerKw) bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) @@ -2343,7 +2342,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val bobCommitments = bob.stateData.asInstanceOf[DATA_NORMAL].commitments assert(DustExposure.computeExposure(bobCommitments.latest.localCommit.spec, bobCommitments.params.localParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) assert(DustExposure.computeExposure(bobCommitments.latest.remoteCommit.spec, bobCommitments.params.remoteParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-closes: bob.setBitcoinCoreFeerate(FeeratePerKw(20000 sat)) @@ -2374,7 +2373,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(DustExposure.computeExposure(bobCommitments.latest.remoteCommit.spec, bobCommitments.params.remoteParams.dustLimit, bobCommitments.params.commitmentFormat) == 0.msat) // A large feerate increase would make these HTLCs overflow Bob's dust exposure, so he force-close: - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob.setBitcoinCoreFeerate(FeeratePerKw(20000 sat)) bob ! UpdateFee(channelId(bob), FeeratePerKw(20000 sat)) val error = bob2alice.expectMsgType[Error] @@ -2420,7 +2419,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with (1 to 2).foreach(_ => addHtlc(7400.sat.toMilliSatoshi, bob, alice, bob2alice, alice2bob)) // A feerate increase makes these HTLCs become dust in one of the commitments but not the other. - val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob.setBitcoinCoreFeerate(updatedFeerate) bob ! UpdateFee(channelId(bob), updatedFeerate) val error = bob2alice.expectMsgType[Error] @@ -2860,8 +2859,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // actual test begins - val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! CurrentBlockHeight(BlockHeight(400145)) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) alice2blockchain.expectMsgType[PublishTx] // main delayed @@ -2883,9 +2881,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice does not react to the fulfill (drops the message for some reason) // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[HtlcSuccessTx]) bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] @@ -2896,10 +2894,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.tx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] alice2blockchain.expectNoMessage(500 millis) } @@ -2917,9 +2915,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice does not react to the fulfill (drops the message for some reason) // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[HtlcSuccessTx]) bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = false) bob2alice.expectMsgType[UpdateFulfillHtlc] @@ -2930,10 +2928,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.tx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] alice2blockchain.expectNoMessage(500 millis) } @@ -2951,9 +2949,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // * Alice acks but doesn't commit // * When the HTLC timeout on Alice side is near, Bob needs to close the channel to avoid an on-chain race // condition between his HTLC-success and Alice's HTLC-timeout - val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val HtlcSuccessTx(_, htlcSuccessTx, _, _, _) = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val commitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head + assert(htlcSuccessTx.isInstanceOf[HtlcSuccessTx]) bob ! CMD_FULFILL_HTLC(htlc.id, r, commit = true) bob2alice.expectMsgType[UpdateFulfillHtlc] @@ -2968,10 +2966,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(isFatal) assert(err.isInstanceOf[HtlcsWillTimeoutUpstream]) - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txid == commitTx.txid) bob2blockchain.expectMsgType[PublishTx] // main delayed - assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.txOut) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == initialCommitTx.txid) + assert(bob2blockchain.expectMsgType[PublishFinalTx].tx.txOut == htlcSuccessTx.tx.txOut) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId == commitTx.txid) channelUpdateListener.expectMsgType[LocalChannelDown] alice2blockchain.expectNoMessage(500 millis) } @@ -3105,7 +3103,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 8) // two anchor outputs, two main outputs and 4 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) awaitCond(alice.stateName == CLOSING) @@ -3156,7 +3154,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc1) @@ -3193,7 +3191,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 7) // two anchor outputs, two main outputs and 3 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) awaitCond(alice.stateName == CLOSING) @@ -3242,7 +3240,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test starts here // bob publishes his current commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(bobCommitTx) val addSettled = alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.ChannelFailureBeforeSigned.type]] assert(addSettled.htlc == htlc2) @@ -3257,7 +3255,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice sends 10 000 sat addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + bob.signCommitTx() } val txs = (0 until 10).map(_ => send()) @@ -3316,7 +3314,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // bob will publish this tx after it is revoked - val revokedTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() alice ! add sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] @@ -3354,7 +3352,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) - val bobRevokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobRevokedCommitTx = bob.signCommitTx() addHtlc(10_000 msat, alice, bob, alice2bob, bob2alice, sender.ref) sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] crossSign(alice, bob, alice2bob, bob2alice) @@ -3401,7 +3399,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob -> alice : 55 000 000 (alice does not have the preimage) => nothing to do, bob will get his money back after the timeout // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 6) // two main outputs and 4 pending htlcs @@ -3460,7 +3458,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice) // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 8) // two main outputs, two anchors and 4 pending htlcs @@ -3501,7 +3499,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two anchors @@ -3534,7 +3532,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // have lost its data and need assistance // an error occurs and alice publishes her commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() bob ! Error(ByteVector32.Zeroes, "oops") bob2blockchain.expectFinalTxPublished(bobCommitTx.txid) assert(bobCommitTx.txOut.size == 1) // only one main output diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index e52a89930e..cb1407ae40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -380,7 +380,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // we keep track of bob commitment tx for later - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // we simulate a disconnection disconnect(alice, bob) @@ -425,7 +425,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // the current state contains a pending htlc addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // we simulate a disconnection followed by a reconnection disconnect(alice, bob) @@ -458,7 +458,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ // we sign a new commitment to make sure the first one is revoked - val bobRevokedCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobRevokedCommitTx = bob.signCommitTx() addHtlc(250_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -613,8 +613,8 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelErrorOccurred]) val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val initialCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val htlcSuccessTx = initialState.commitments.latest.localCommit.htlcTxsAndRemoteSigs.head.htlcTx + val initialCommitTx = bob.signCommitTx() + val htlcSuccessTx = bob.htlcTxs().head assert(htlcSuccessTx.isInstanceOf[HtlcSuccessTx]) disconnect(alice, bob) @@ -673,14 +673,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection disconnect(alice, bob) - val aliceStateData = alice.stateData.asInstanceOf[DATA_NORMAL] - val aliceCommitTx = aliceStateData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - - val currentFeeratePerKw = aliceStateData.commitments.latest.localCommit.spec.commitTxFeerate + val aliceCommitTx = alice.signCommitTx() + val currentFeerate = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). - val networkFeeratePerKw = currentFeeratePerKw * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) - val networkFeerates = FeeratesPerKw.single(networkFeeratePerKw) + val networkFeerate = currentFeerate * (1.1 / alice.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Bob.nodeParams.nodeId).ratioLow) + val networkFeerates = FeeratesPerKw.single(networkFeerate) // alice is funder alice.setBitcoinCoreFeerates(networkFeerates) @@ -783,14 +781,12 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // we simulate a disconnection disconnect(alice, bob) - val bobStateData = bob.stateData.asInstanceOf[DATA_NORMAL] - val bobCommitTx = bobStateData.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - - val currentFeeratePerKw = bobStateData.commitments.latest.localCommit.spec.commitTxFeerate + val bobCommitTx = bob.signCommitTx() + val currentFeerate = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.commitTxFeerate // we receive a feerate update that makes our current feerate too low compared to the network's (we multiply by 1.1 // to ensure the network's feerate is 10% above our threshold). - val networkFeeratePerKw = currentFeeratePerKw * (1.1 / bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Alice.nodeParams.nodeId).ratioLow) - val networkFeerates = FeeratesPerKw.single(networkFeeratePerKw) + val networkFeerate = currentFeerate * (1.1 / bob.underlyingActor.nodeParams.onChainFeeConf.feerateToleranceFor(Alice.nodeParams.nodeId).ratioLow) + val networkFeerates = FeeratesPerKw.single(networkFeerate) // bob is fundee bob.setBitcoinCoreFeerates(networkFeerates) 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 559b65582d..fc90ea0b31 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 @@ -49,8 +49,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) @@ -216,7 +216,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFulfillHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fulfill = UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice ! fulfill alice2bob.expectMsgType[Error] @@ -230,7 +230,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFulfillHtlc (invalid preimage)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFulfillHtlc(ByteVector32.Zeroes, 42, ByteVector32.Zeroes) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -321,7 +321,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFailHtlc (unknown htlc id)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFailHtlc(ByteVector32.Zeroes, 42, ByteVector.fill(152)(0)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -342,7 +342,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFailMalformedHtlc (invalid failure_code)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, 1, Crypto.sha256(ByteVector.empty), 42) alice ! fail val error = alice2bob.expectMsgType[Error] @@ -410,7 +410,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CommitSig (no changes)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() // signature is invalid but it doesn't matter bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] @@ -422,7 +422,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CommitSig (invalid signature)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! CommitSig(ByteVector32.Zeroes, ByteVector64.Zeroes, Nil) bob2alice.expectMsgType[Error] awaitCond(bob.stateName == CLOSING) @@ -470,7 +470,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (invalid preimage)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! CMD_FULFILL_HTLC(0, r1) bob2alice.expectMsgType[UpdateFulfillHtlc] bob2alice.forward(alice) @@ -490,7 +490,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv RevokeAndAck (unexpectedly)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() awaitCond(alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.remoteNextCommitInfo.isRight) alice ! RevokeAndAck(ByteVector32.Zeroes, PrivateKey(randomBytes32()), PrivateKey(randomBytes32()).publicKey) alice2bob.expectMsgType[Error] @@ -581,7 +581,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee (when sender is not funder)") { f => import f._ - val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(12000 sat)) alice2bob.expectMsgType[Error] awaitCond(alice.stateName == CLOSING) @@ -594,7 +594,7 @@ 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 tx = bob.signCommitTx() val fee = UpdateFee(ByteVector32.Zeroes, FeeratePerKw(100000000 sat)) // we first update the feerates so that we don't trigger a 'fee too different' error bob.setBitcoinCoreFeerate(fee.feeratePerKw) @@ -609,7 +609,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee (local/remote feerates are too different)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(65000 sat)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == "local/remote feerates are too different: remoteFeeratePerKw=65000 localFeeratePerKw=10000") @@ -621,7 +621,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv UpdateFee (remote feerate is too small)") { f => import f._ - val tx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! UpdateFee(ByteVector32.Zeroes, FeeratePerKw(252 sat)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) == "remote fee rate is too small: remoteFeeratePerKw=252") @@ -650,8 +650,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CurrentBlockCount (an htlc timed out)") { f => import f._ - val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN] - val aliceCommitTx = initialState.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! CurrentBlockHeight(BlockHeight(400145)) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == aliceCommitTx.txid) // commit tx alice2blockchain.expectMsgType[PublishTx] // main delayed @@ -720,7 +719,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv WatchFundingSpentTriggered (their commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ // bob publishes his current commit tx, which contains two pending htlcs alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 6) // two main outputs and 2 pending htlcs alice ! WatchFundingSpentTriggered(bobCommitTx) awaitCond(alice.stateName == CLOSING) @@ -765,7 +764,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit // as far as alice knows, bob currently has two valid unrevoked commitment transactions // bob publishes his current commit tx, which contains one pending htlc alice->bob - val bobCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == 5) // two anchor outputs, two main outputs and 1 pending htlc alice ! WatchFundingSpentTriggered(bobCommitTx) awaitCond(alice.stateName == CLOSING) @@ -796,7 +795,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv WatchFundingSpentTriggered (revoked tx)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ - val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() // two main outputs + 2 htlc assert(revokedTx.txOut.size == 6) @@ -830,13 +829,13 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv WatchFundingSpentTriggered (revoked tx with updated commitment)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), Tag(ChannelStateTestsTags.StaticRemoteKey)) { f => import f._ - val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val initialCommitTx = bob.signCommitTx() assert(initialCommitTx.txOut.size == 6) // two main outputs + 2 htlc // bob fulfills one of the pending htlc (commitment update while in shutdown state) fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val revokedTx = bob.signCommitTx() assert(revokedTx.txOut.size == 5) // two anchor outputs, two main outputs + 1 htlc // bob fulfills the second pending htlc (and revokes the previous commitment) @@ -910,7 +909,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_FORCECLOSE") { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs val sender = TestProbe() @@ -952,7 +951,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv Error") { f => import f._ - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") alice2blockchain.expectFinalTxPublished(aliceCommitTx.txid) assert(aliceCommitTx.txOut.size == 4) // two main outputs and two htlcs diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 8b2b0e4aee..6ddb12b258 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -474,7 +474,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ aliceClose(f) val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = bob.signCommitTx() bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close signature")) @@ -951,7 +951,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike import f._ bobClose(f) alice2bob.expectMsgType[ClosingSigned] - val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val tx = alice.signCommitTx() alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) assert(alice2blockchain.expectMsgType[PublishFinalTx].tx.txid == tx.txid) 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 7dda541c9a..06050fd765 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 @@ -50,7 +50,7 @@ import scala.concurrent.duration._ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { - case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, eventListener: TestProbe, bobCommitTxs: List[CommitTxAndRemoteSig]) + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, bob2blockchain: TestProbe, alice2relayer: TestProbe, bob2relayer: TestProbe, channelUpdateListener: TestProbe, txListener: TestProbe, eventListener: TestProbe, bobCommitTxs: List[Transaction]) override def withFixture(test: OneArgTest): Outcome = { val setup = init() @@ -117,20 +117,20 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with systemA.eventStream.subscribe(txListener.ref, classOf[TransactionConfirmed]) systemB.eventStream.subscribe(txListener.ref, classOf[TransactionPublished]) systemB.eventStream.subscribe(txListener.ref, classOf[TransactionConfirmed]) - val bobCommitTxs: List[CommitTxAndRemoteSig] = (for (amt <- List(100000000 msat, 200000000 msat, 300000000 msat)) yield { + val bobCommitTxs = List(100_000_000 msat, 200_000_000 msat, 300_000_000 msat).flatMap(amt => { val (r, htlc) = addHtlc(amt, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) bob2relayer.expectMsgType[RelayForward] - val bobCommitTx1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig + val bobCommitTx1 = bob.signCommitTx() fulfillHtlc(htlc.id, r, bob, alice, bob2alice, alice2bob) // alice forwards the fulfill upstream alice2relayer.expectMsgType[RES_ADD_SETTLED[Origin, HtlcResult.Fulfill]] crossSign(bob, alice, bob2alice, alice2bob) // bob confirms that it has forwarded the fulfill to alice awaitCond(bob.nodeParams.db.pendingCommands.listSettlementCommands(htlc.channelId).isEmpty) - val bobCommitTx2 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig + val bobCommitTx2 = bob.signCommitTx() bobCommitTx1 :: bobCommitTx2 :: Nil - }).flatten + }) awaitCond(alice.stateName == NORMAL) awaitCond(bob.stateName == NORMAL) @@ -357,7 +357,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (local commit)") { f => import f._ // an error occurs and alice publishes her commit tx - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() localClose(alice, alice2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.localCommitPublished.isDefined) @@ -796,7 +796,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() // alice sends an htlc val (_, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) @@ -829,8 +829,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.forward(alice) alice2bob.expectMsgType[RevokeAndAck] alice2relayer.expectNoMessage(100 millis) // the HTLC is not relayed downstream - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) + val aliceCommitTx = alice.signCommitTx() // Note that alice has not signed the htlc yet! // We make her unilaterally close the channel. val (closingState, closingTxs) = localClose(alice, alice2blockchain) @@ -859,8 +859,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) bob2alice.expectMsgType[RevokeAndAck] // not received by Alice alice2relayer.expectNoMessage(100 millis) // the HTLC is not relayed downstream - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcTxsAndRemoteSigs.size == 1) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.size == 1) + val bobCommitTx = bob.signCommitTx() // We make Bob unilaterally close the channel. val (rcp, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -881,7 +881,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (r, htlc) = addHtlc(110_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) assert(alice2relayer.expectMsgType[RelayForward].add == htlc) - val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val aliceCommitTx = alice.signCommitTx() assert(aliceCommitTx.txOut.size == 3) // 2 main outputs + 1 htlc // alice fulfills the HTLC but bob doesn't receive the signature @@ -1260,7 +1260,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val listener = TestProbe() systemA.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain]) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() // alice sends an htlc val (_, htlc) = addHtlc(50_000_000 msat, alice, bob, alice2bob, bob2alice) // and signs it (but bob doesn't sign it) @@ -1309,9 +1309,8 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[RevokeAndAck] // not sent to alice // bob closes the channel using his latest commitment, which doesn't contain any htlc. - val bobCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - assert(bobCommit.htlcTxsAndRemoteSigs.isEmpty) - val commitTx = bobCommit.commitTxAndRemoteSig.commitTx.tx + assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.htlcRemoteSigs.isEmpty) + val commitTx = bob.signCommitTx() alice ! WatchFundingSpentTriggered(commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(42), 0, commitTx) // the two HTLCs have been overridden by the on-chain commit @@ -1332,7 +1331,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 2) // two main outputs val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.localOutput_opt.isEmpty) @@ -1351,7 +1350,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with systemA.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate]) // bob publishes his commit tx - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() remoteClose(bobCommitTx, alice, alice2blockchain) // alice notifies the network that the channel shouldn't be used anymore inside(listener.expectMsgType[LocalChannelUpdate]) { u => assert(!u.channelUpdate.channelFlags.isEnabled) } @@ -1361,7 +1360,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) // Bob publishes his last current commit tx, the one it had when entering NEGOTIATING state. - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.htlcOutputs.isEmpty) assert(closingTxs.mainTx_opt.isEmpty) @@ -1373,7 +1372,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_BUMP_FORCE_CLOSE_FEE (remote commit)", Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f => import f._ - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.anchorOutput_opt.nonEmpty) assert(closingTxs.anchorTx_opt.nonEmpty) @@ -1393,7 +1392,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1414,7 +1413,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE].commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 2) // two main outputs alice ! WatchFundingSpentTriggered(bobCommitTx) @@ -1432,7 +1431,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] assert(initialState.commitments.params.channelFeatures == ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxs.last.commitTx.tx + val bobCommitTx = bobCommitTxs.last assert(bobCommitTx.txOut.size == 4) // two main outputs + two anchors val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) @@ -1461,7 +1460,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with crossSign(alice, bob, alice2bob, bob2alice) // Bob publishes the latest commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() channelFeatures.commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs @@ -1514,7 +1513,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.expectMsgType[CommitSig] // We stop here: Alice sent her CommitSig, but doesn't hear back from Bob. // Now Bob publishes the first commit tx (force-close). - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.length == 6) // 2 main outputs + 2 anchor outputs + 2 HTLCs val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.localOutput_opt.nonEmpty) @@ -1561,7 +1560,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // 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 + val bobCommitTx = bob.signCommitTx() val (_, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) assert(closingTxs.htlcTimeoutTxs.size == 1) val htlcTimeoutTx = closingTxs.htlcTxs.head @@ -1610,7 +1609,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob) // Bob publishes the next commit tx. - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() channelFeatures.commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 7) // two main outputs + two anchors + 3 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 5) // two main outputs + 3 HTLCs @@ -1700,7 +1699,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[CommitSig] // not forwarded to Alice (malicious Bob) // Now Bob publishes the next commit tx (force-close). - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.length == 7) // 2 main outputs + 2 anchor outputs + 3 HTLCs val (closingState, closingTxs) = remoteClose(bobCommitTx, alice, alice2blockchain, htlcTimeoutCount = 1) assert(closingState.localOutput_opt.nonEmpty) @@ -1808,7 +1807,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice now waits for bob to publish its commitment awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() channelFeatures.commitmentFormat match { case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommitTx.txOut.length == 6) // two main outputs + two anchors + 2 HTLCs case DefaultCommitmentFormat => assert(bobCommitTx.txOut.length == 4) // two main outputs + 2 HTLCs @@ -1877,65 +1876,67 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == CLOSED) } - case class RevokedCloseFixture(bobRevokedTxs: Seq[LocalCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) + case class RevokedCommit(commitTx: Transaction, htlcTxs: Seq[HtlcTx]) + + case class RevokedCloseFixture(bobRevokedTxs: Seq[RevokedCommit], htlcsAlice: Seq[(UpdateAddHtlc, ByteVector32)], htlcsBob: Seq[(UpdateAddHtlc, ByteVector32)]) private def prepareRevokedClose(f: FixtureParam, channelFeatures: ChannelFeatures): RevokedCloseFixture = { import f._ // Bob's first commit tx doesn't contain any htlc - val localCommit1 = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit + val bobCommit1 = RevokedCommit(bob.signCommitTx(), Nil) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) // 2 main outputs + 2 anchors - case DefaultCommitmentFormat => assert(localCommit1.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) // 2 main outputs + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 4) // 2 main outputs + 2 anchors + case DefaultCommitmentFormat => assert(bobCommit1.commitTx.txOut.size == 2) // 2 main outputs } // Bob's second commit tx contains 1 incoming htlc and 1 outgoing htlc - val (localCommit2, htlcAlice1, htlcBob1) = { + val (bobCommit2, htlcAlice1, htlcBob1) = { val (ra, htlcAlice) = addHtlc(35_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val (rb, htlcBob) = addHtlc(20_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - (localCommit, (htlcAlice, ra), (htlcBob, rb)) + val bobCommit2 = RevokedCommit(bob.signCommitTx(), bob.htlcTxs()) + (bobCommit2, (htlcAlice, ra), (htlcBob, rb)) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size) + assert(alice.signCommitTx().txOut.size == bobCommit2.commitTx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case DefaultCommitmentFormat => assert(localCommit2.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 6) + case DefaultCommitmentFormat => assert(bobCommit2.commitTx.txOut.size == 4) } // Bob's third commit tx contains 2 incoming htlcs and 2 outgoing htlcs - val (localCommit3, htlcAlice2, htlcBob2) = { + val (bobCommit3, htlcAlice2, htlcBob2) = { val (ra, htlcAlice) = addHtlc(25_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) val (rb, htlcBob) = addHtlc(18_000_000 msat, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - val localCommit = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit - (localCommit, (htlcAlice, ra), (htlcBob, rb)) + val bobCommit3 = RevokedCommit(bob.signCommitTx(), bob.htlcTxs()) + (bobCommit3, (htlcAlice, ra), (htlcBob, rb)) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size) + assert(alice.signCommitTx().txOut.size == bobCommit3.commitTx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) - case DefaultCommitmentFormat => assert(localCommit3.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 8) + case DefaultCommitmentFormat => assert(bobCommit3.commitTx.txOut.size == 6) } // Bob's fourth commit tx doesn't contain any htlc - val localCommit4 = { + val bobCommit4 = { Seq(htlcAlice1, htlcAlice2).foreach { case (htlcAlice, _) => failHtlc(htlcAlice.id, bob, alice, bob2alice, alice2bob) } Seq(htlcBob1, htlcBob2).foreach { case (htlcBob, _) => failHtlc(htlcBob.id, alice, bob, alice2bob, bob2alice) } crossSign(alice, bob, alice2bob, bob2alice) - bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit + RevokedCommit(bob.signCommitTx(), Nil) } - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size) + assert(alice.signCommitTx().txOut.size == bobCommit4.commitTx.txOut.size) channelFeatures.commitmentFormat match { - case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 4) - case DefaultCommitmentFormat => assert(localCommit4.commitTxAndRemoteSig.commitTx.tx.txOut.size == 2) + case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 4) + case DefaultCommitmentFormat => assert(bobCommit4.commitTx.txOut.size == 2) } - RevokedCloseFixture(Seq(localCommit1, localCommit2, localCommit3, localCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) + RevokedCloseFixture(Seq(bobCommit1, bobCommit2, bobCommit3, bobCommit4), Seq(htlcAlice1, htlcAlice2), Seq(htlcBob1, htlcBob2)) } case class RevokedCloseTxs(mainTx_opt: Option[Transaction], mainPenaltyTx: Transaction, htlcPenaltyTxs: Seq[Transaction]) @@ -1947,7 +1948,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.params.channelFeatures == channelFeatures) // bob publishes one of his revoked txs - val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx + val bobRevokedTx = revokedCloseFixture.bobRevokedTxs(1).commitTx alice ! WatchFundingSpentTriggered(bobRevokedTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) @@ -2019,7 +2020,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv WatchFundingSpentTriggered (multiple revoked tx)") { f => import f._ val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey)) - assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTxAndRemoteSig.commitTx.tx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct + assert(revokedCloseFixture.bobRevokedTxs.map(_.commitTx.txid).toSet.size == revokedCloseFixture.bobRevokedTxs.size) // all commit txs are distinct def broadcastBobRevokedTx(revokedTx: Transaction, htlcCount: Int, revokedCount: Int): RevokedCloseTxs = { alice ! WatchFundingSpentTriggered(revokedTx) @@ -2039,16 +2040,16 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } // bob publishes a first revoked tx (no htlc in that commitment) - broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTxAndRemoteSig.commitTx.tx, 0, 1) + broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs.head.commitTx, 0, 1) // bob publishes a second revoked tx - val closingTxs = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx, 2, 2) + val closingTxs = broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(1).commitTx, 2, 2) // bob publishes a third revoked tx - broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTxAndRemoteSig.commitTx.tx, 4, 3) + broadcastBobRevokedTx(revokedCloseFixture.bobRevokedTxs(2).commitTx, 4, 3) // bob's second revoked tx confirms: once all penalty txs are confirmed, alice can move to the closed state // NB: if multiple txs confirm in the same block, we may receive the events in any order alice ! WatchTxConfirmedTriggered(BlockHeight(100), 1, closingTxs.mainPenaltyTx) - alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, revokedCloseFixture.bobRevokedTxs(1).commitTxAndRemoteSig.commitTx.tx) + alice ! WatchTxConfirmedTriggered(BlockHeight(100), 3, revokedCloseFixture.bobRevokedTxs(1).commitTx) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 0, closingTxs.htlcPenaltyTxs(0)) assert(alice.stateName == CLOSING) alice ! WatchTxConfirmedTriggered(BlockHeight(115), 2, closingTxs.htlcPenaltyTxs(1)) @@ -2099,12 +2100,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes one of his revoked txs val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) - alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) 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.commitTx == bobRevokedCommit.commitTx) if (channelFeatures.paysDirectlyToWallet) { assert(rvk.localOutput_opt.isEmpty) } else { @@ -2129,14 +2130,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes one of his HTLC-success transactions val (fulfilledHtlc, _) = revokedCloseFixture.htlcsAlice.head - val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) if txInfo.htlcId == fulfilledHtlc.id => txInfo }.get + val bobHtlcSuccessTx1 = bobRevokedCommit.htlcTxs.collectFirst { case txInfo: HtlcSuccessTx if txInfo.htlcId == fulfilledHtlc.id => txInfo }.get assert(bobHtlcSuccessTx1.paymentHash == fulfilledHtlc.paymentHash) alice ! WatchOutputSpentTriggered(bobHtlcSuccessTx1.amountIn, bobHtlcSuccessTx1.tx) alice2blockchain.expectWatchTxConfirmed(bobHtlcSuccessTx1.tx.txid) // bob publishes one of his HTLC-timeout transactions val (failedHtlc, _) = revokedCloseFixture.htlcsBob.last - val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxsAndRemoteSigs.collectFirst { case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) if txInfo.htlcId == failedHtlc.id => txInfo }.get + val bobHtlcTimeoutTx = bobRevokedCommit.htlcTxs.collectFirst { case txInfo: HtlcTimeoutTx if txInfo.htlcId == failedHtlc.id => txInfo }.get assert(bobHtlcTimeoutTx.paymentHash == failedHtlc.paymentHash) alice ! WatchOutputSpentTriggered(bobHtlcTimeoutTx.amountIn, bobHtlcTimeoutTx.tx) alice2blockchain.expectWatchTxConfirmed(bobHtlcTimeoutTx.tx.txid) @@ -2193,12 +2194,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob publishes one of his revoked txs val revokedCloseFixture = prepareRevokedClose(f, ChannelFeatures(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx)) val bobRevokedCommit = revokedCloseFixture.bobRevokedTxs(2) - alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTxAndRemoteSig.commitTx.tx) + alice ! WatchFundingSpentTriggered(bobRevokedCommit.commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) assert(alice.stateData.asInstanceOf[DATA_CLOSING].commitments.params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) 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.commitTx == bobRevokedCommit.commitTx) assert(rvk.htlcOutputs.size == 4) assert(rvk.htlcDelayedOutputs.isEmpty) @@ -2212,12 +2213,12 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // bob claims multiple htlc outputs in a single transaction (this is possible with anchor outputs because signatures // use sighash_single | sighash_anyonecanpay) - val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.collect { - case HtlcTxAndRemoteSig(txInfo: HtlcSuccessTx, _) => + val bobHtlcTxs = bobRevokedCommit.htlcTxs.collect { + case txInfo: HtlcSuccessTx => val preimage = revokedCloseFixture.htlcsAlice.collectFirst { case (add, preimage) if add.id == txInfo.htlcId => preimage }.get assert(Crypto.sha256(preimage) == txInfo.paymentHash) txInfo - case HtlcTxAndRemoteSig(txInfo: HtlcTimeoutTx, _) => + case txInfo: HtlcTimeoutTx => txInfo } assert(bobHtlcTxs.map(_.input.outPoint).size == 4) @@ -2261,7 +2262,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with 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 + val commitTx = bobRevokedCommit.commitTx alice ! WatchFundingSpentTriggered(commitTx) awaitCond(alice.stateData.isInstanceOf[DATA_CLOSING]) @@ -2275,7 +2276,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2blockchain.expectNoMessage(100 millis) // Bob claims HTLC outputs using aggregated transactions. - val bobHtlcTxs = bobRevokedCommit.htlcTxsAndRemoteSigs.map(_.htlcTx) + val bobHtlcTxs = bobRevokedCommit.htlcTxs assert(bobHtlcTxs.map(_.input.outPoint).size == 4) val bobHtlcTx1 = Transaction( 2, @@ -2366,14 +2367,14 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with case _: AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => 4 case DefaultCommitmentFormat => 2 } - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount) + assert(bob.signCommitTx().txOut.size == initOutputCount) // bob's second commit tx contains 2 incoming htlcs val (bobRevokedTx, htlcs1) = { val (_, htlc1) = addHtlc(35_000_000 msat, alice, bob, alice2bob, bob2alice) val (_, htlc2) = addHtlc(20_000_000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx + val bobCommitTx = bob.signCommitTx() assert(bobCommitTx.txOut.size == initOutputCount + 2) (bobCommitTx, Seq(htlc1, htlc2)) } @@ -2384,7 +2385,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (_, htlc4) = addHtlc(18_000_000 msat, alice, bob, alice2bob, bob2alice) failHtlc(htlcs1.head.id, bob, alice, bob2alice, alice2bob) crossSign(bob, alice, bob2alice, alice2bob) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.size == initOutputCount + 3) + assert(bob.signCommitTx().txOut.size == initOutputCount + 3) Seq(htlc3, htlc4) } 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 b86a48f43b..ee1206ce5e 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 @@ -401,15 +401,19 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val commitmentsF = sigListener.expectMsgType[ChannelSignatureReceived].commitments sigListener.expectNoMessage(1 second) assert(commitmentsF.params.commitmentFormat == commitmentFormat) + // we prepare the revoked transactions F will publish + val channelKeysF = nodes("F").nodeParams.channelKeyManager.channelKeys(commitmentsF.params.channelConfig, commitmentsF.params.localParams.fundingKeyPath) + val commitmentKeysF = commitmentsF.latest.localKeys(channelKeysF) + val revokedCommitTx = commitmentsF.latest.fullySignedLocalCommitTx(channelKeysF) // in this commitment, both parties should have a main output, there are four pending htlcs and anchor outputs if applicable - val localCommitF = commitmentsF.latest.localCommit commitmentFormat match { - case Transactions.DefaultCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 6) - case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(localCommitF.commitTxAndRemoteSig.commitTx.tx.txOut.size == 8) + case Transactions.DefaultCommitmentFormat => assert(revokedCommitTx.txOut.size == 6) + case _: Transactions.AnchorOutputsCommitmentFormat | _: SimpleTaprootChannelCommitmentFormat => assert(revokedCommitTx.txOut.size == 8) } - val outgoingHtlcExpiry = localCommitF.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max - val htlcTimeoutTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcTimeoutTx, _) => h } - val htlcSuccessTxs = localCommitF.htlcTxsAndRemoteSigs.collect { case h@HtlcTxAndRemoteSig(_: Transactions.HtlcSuccessTx, _) => h } + val outgoingHtlcExpiry = commitmentsF.latest.localCommit.spec.htlcs.collect { case OutgoingHtlc(add) => add.cltvExpiry }.max + val htlcTxsF = commitmentsF.latest.htlcTxs(channelKeysF) + val htlcTimeoutTxs = htlcTxsF.collect { case (tx: Transactions.HtlcTimeoutTx, remoteSig) => (tx, remoteSig) } + val htlcSuccessTxs = htlcTxsF.collect { case (tx: Transactions.HtlcSuccessTx, remoteSig) => (tx, remoteSig) } assert(htlcTimeoutTxs.size == 2) assert(htlcSuccessTxs.size == 2) // we fulfill htlcs to get the preimages @@ -435,24 +439,15 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]] val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("C").wallet.getReceivePublicKeyScript(renew = false)) - // we prepare the revoked transactions F will publish - val channelKeysF = nodes("F").nodeParams.channelKeyManager.channelKeys(commitmentsF.params.channelConfig, commitmentsF.params.localParams.fundingKeyPath) - val fundingKeyF = channelKeysF.fundingKey(commitmentsF.latest.fundingTxIndex) - val commitmentKeysF = commitmentsF.latest.localKeys(channelKeysF) - val revokedCommitTx = { - val commitTx = localCommitF.commitTxAndRemoteSig.commitTx - val localSig = commitTx.sign(fundingKeyF, commitmentsF.latest.remoteFundingPubKey) - val remoteSig = localCommitF.commitTxAndRemoteSig.remoteSig.asInstanceOf[ChannelSpendSignature.IndividualSignature] - commitTx.aggregateSigs(fundingKeyF.publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig) - } val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map { - case (htlcTxAndSigs, preimage) => - val localSig = htlcTxAndSigs.htlcTx.sign(commitmentKeysF, commitmentFormat, Map.empty) - htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcSuccessTx].addSigs(commitmentKeysF, localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.params.commitmentFormat).tx + case ((htlcTx, remoteSig), preimage) => + val localSig = htlcTx.sign(commitmentKeysF, commitmentFormat, Map.empty) + htlcTx.addSigs(commitmentKeysF, localSig, remoteSig, preimage, commitmentsF.params.commitmentFormat).tx } - val htlcTimeout = htlcTimeoutTxs.map { htlcTxAndSigs => - val localSig = htlcTxAndSigs.htlcTx.sign(commitmentKeysF, commitmentFormat, Map.empty) - htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcTimeoutTx].addSigs(commitmentKeysF, localSig, htlcTxAndSigs.remoteSig, commitmentsF.params.commitmentFormat).tx + val htlcTimeout = htlcTimeoutTxs.map { + case (htlcTx, remoteSig) => + val localSig = htlcTx.sign(commitmentKeysF, commitmentFormat, Map.empty) + htlcTx.addSigs(commitmentKeysF, localSig, remoteSig, commitmentsF.params.commitmentFormat).tx } htlcSuccess.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) htlcTimeout.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) @@ -595,9 +590,6 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { Script.write(Script.pay2wsh(toRemote)) } - // toRemote output of C as seen by F - val Some(toRemoteOutC) = initialStateDataF.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txOut.find(_.publicKeyScript == toRemoteAddress) - // let's make a payment to advance the commit index val amountMsat = 4200000.msat sender.send(nodes("F").paymentHandler, ReceiveStandardPayment(sender.ref.toTyped, Some(amountMsat), Left("1 coffee"))) @@ -620,16 +612,10 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val stateDataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data val commitmentIndex = stateDataF.commitments.localCommitIndex - val commitTx = stateDataF.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx - val Some(toRemoteOutCNew) = commitTx.txOut.find(_.publicKeyScript == toRemoteAddress) - + val commitTxId = stateDataF.commitments.latest.localCommit.txId // there is a new commitment index in the channel state assert(commitmentIndex > initialCommitmentIndex) - // script pubkeys of toRemote output remained the same across commitments - assert(toRemoteOutCNew.publicKeyScript == toRemoteOutC.publicKeyScript) - assert(toRemoteOutCNew.amount < toRemoteOutC.amount) - val stateListener = TestProbe() nodes("C").system.eventStream.subscribe(stateListener.ref, classOf[ChannelStateChanged]) @@ -642,16 +628,17 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec { awaitCond(stateListener.expectMsgType[ChannelStateChanged](max = 60 seconds).currentState == CLOSING, max = 60 seconds) val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) - awaitCond({ - bitcoinClient.getTransaction(commitTx.txid).map(tx => Some(tx)).recover(_ => None).pipeTo(sender.ref) + val commitTx = awaitAssert({ + bitcoinClient.getTransaction(commitTxId).map(tx => Some(tx)).recover(_ => None).pipeTo(sender.ref) val tx = sender.expectMsgType[Option[Transaction]] // the unilateral close contains the static toRemote output - tx.exists(_.txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + assert(tx.exists(_.txOut.exists(_.publicKeyScript == toRemoteAddress))) + tx.get }, max = 20 seconds, interval = 1 second) // bury the unilateral close in a block, C should claim its main output generateBlocks(2) - val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + val mainOutputC = OutPoint(commitTx, commitTx.txOut.indexWhere(_.publicKeyScript == toRemoteAddress)) awaitCond({ bitcoinClient.getMempool().pipeTo(sender.ref) sender.expectMsgType[Seq[Transaction]].exists(_.txIn.head.outPoint == mainOutputC) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala index d3f0e8c7db..ac2a500ecc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/SwitchboardSpec.scala @@ -4,18 +4,16 @@ import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.actor.{Actor, ActorContext, ActorRef, Props} import akka.testkit.{TestActorRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, OutPoint, Satoshi, TxHash, TxOut} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Satoshi} import fr.acinq.eclair.TestConstants._ -import fr.acinq.eclair.channel.SpliceStatus.NoSplice -import fr.acinq.eclair.channel.{ChannelFeatures, ChannelFlags, ChannelIdAssigned, ChannelParams, CommitTxAndRemoteSig, Commitment, Commitments, DATA_NORMAL, LocalCommit, LocalParams, PersistentChannelData, RemoteCommit, RemoteParams, Upstream} +import fr.acinq.eclair.channel.{ChannelIdAssigned, DATA_NORMAL, PersistentChannelData, Upstream} import fr.acinq.eclair.io.Peer.PeerNotFound import fr.acinq.eclair.io.Switchboard._ import fr.acinq.eclair.payment.relay.{OnTheFlyFunding, OnTheFlyFundingSpec} -import fr.acinq.eclair.transactions.Transactions.{CommitTx, InputInfo} import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{CltvExpiry, Features, InitFeature, MilliSatoshiLong, NodeParams, TestKitBaseClass, TimestampSecondLong, UInt64, randomBytes32, randomKey} +import fr.acinq.eclair.{CltvExpiry, Features, InitFeature, MilliSatoshiLong, NodeParams, TestKitBaseClass, TimestampSecondLong, randomBytes32, randomKey} import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits._ @@ -167,7 +165,7 @@ class SwitchboardSpec extends TestKitBaseClass with AnyFunSuiteLike { def dummyDataNormal(remoteNodeId: PublicKey, capacity: Satoshi): DATA_NORMAL = { val data = ChannelCodecsSpec.normal.modify(_.commitments.params.remoteParams.nodeId).setTo(remoteNodeId) - .modify(_.commitments.active).apply(_.map(_.modify(_.localCommit.commitTxAndRemoteSig.commitTx.input.txOut.amount).setTo(capacity))) + .modify(_.commitments.active).apply(_.map(_.modify(_.localCommit.input.txOut.amount).setTo(capacity))) assert(data.remoteNodeId == remoteNodeId) assert(data.commitments.capacity == capacity) data diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index ee5ef3f741..42b8c18d9e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -125,7 +125,7 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat val localParams = LocalParams(dummyPublicKey, DeterministicWallet.KeyPath(Seq(42L)), 546 sat, Long.MaxValue.msat, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, isChannelOpener = true, paysCommitTxFees = true, None, None, Features.empty) val remoteParams = RemoteParams(dummyPublicKey, 546 sat, UInt64.MaxValue, Some(1000 sat), 1 msat, CltvExpiryDelta(144), 50, dummyPublicKey, dummyPublicKey, dummyPublicKey, dummyPublicKey, Features.empty, None) val commitmentInput = Funding.makeFundingInputInfo(TxId(dummyBytes32), 0, 150_000 sat, dummyPublicKey, dummyPublicKey, DefaultCommitmentFormat) - val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 100_000_000 msat, 50_000_000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, Transaction(2, Nil, Nil, 0)), IndividualSignature(ByteVector64.Zeroes)), Nil) + val localCommit = LocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 100_000_000 msat, 50_000_000 msat), TxId(dummyBytes32), commitmentInput, IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(2500 sat), 50_000_000 msat, 100_000_000 msat), TxId(dummyBytes32), dummyPublicKey) val channelInfo = RES_GET_CHANNEL_INFO( PublicKey(hex"0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b"), @@ -207,13 +207,20 @@ class JsonSerializersSpec extends TestKitBaseClass with AnyFunSuiteLike with Mat | "localCommit": { | "index": 0, | "spec": { "htlcs": [], "commitTxFeerate": 2500, "toLocal": 100000000, "toRemote": 50000000 }, - | "commitTxAndRemoteSig": { "commitTx": { "txid": "4ebd325a4b394cff8c57e8317ccf5a8d0e2bdf1b8526f8aad6c8e43d8240621a", "tx": "02000000000000000000" },"remoteSig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" }, - | "htlcTxsAndRemoteSigs": [] + | "txId": "0202020202020202020202020202020202020202020202020202020202020202", + | "input": { + | "outPoint":"0202020202020202020202020202020202020202020202020202020202020202:0", + | "amountSatoshis": 150000 + | }, + | "remoteSig": { + | "sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + | }, + | "htlcRemoteSigs": [] | }, | "remoteCommit": { | "index": 0, | "spec": { "htlcs": [], "commitTxFeerate": 2500, "toLocal": 50000000, "toRemote": 100000000 }, - | "txid": "0202020202020202020202020202020202020202020202020202020202020202", + | "txId": "0202020202020202020202020202020202020202020202020202020202020202", | "remotePerCommitmentPoint": "031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f" | } | } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index a8d63586bb..b52d9acef6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.ActorRef import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPrivateKey -import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features._ import fr.acinq.eclair.TestUtils.randomTxId @@ -34,7 +34,6 @@ import fr.acinq.eclair.payment.send.{BlindedRecipient, ClearRecipient, Trampolin import fr.acinq.eclair.router.BaseRouterSpec.{blindedRouteFromHops, channelHopFromUpdate} import fr.acinq.eclair.router.BlindedRouteCreation import fr.acinq.eclair.router.Router.{NodeHop, Route} -import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.OfferTypes.{InvoiceRequest, Offer, PaymentInfo} import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload} @@ -749,7 +748,7 @@ object PaymentPacketSpec { val remoteParams = RemoteParams(randomKey().publicKey, null, UInt64.MaxValue, Some(channelReserve), null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, None) val fundingTx = Transaction(2, Nil, Seq(TxOut(testCapacity, Nil)), 0) val commitInput = InputInfo(OutPoint(fundingTx, 0), fundingTx.txOut.head, ByteVector.empty) - val localCommit = LocalCommit(0, null, CommitTxAndRemoteSig(Transactions.CommitTx(commitInput, Transaction(2, Seq(TxIn(commitInput.outPoint, Nil, 0)), Seq(TxOut(testCapacity, Nil)), 0)), IndividualSignature(ByteVector64.Zeroes)), Nil) + val localCommit = LocalCommit(0, null, randomTxId(), commitInput, IndividualSignature(ByteVector64.Zeroes), Nil) val remoteCommit = RemoteCommit(0, null, randomTxId(), randomKey().publicKey) val localChanges = LocalChanges(Nil, Nil, Nil) val remoteChanges = RemoteChanges(Nil, Nil, Nil) 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 fd3c014633..b0ce04a24d 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, TxId, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.WatchTxConfirmedTriggered import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel._ @@ -510,7 +510,7 @@ class PostRestartHtlcCleanerSpec extends TestKitBaseClass with FixtureAnyFunSuit val normal = ChannelCodecsSpec.makeChannelDataNormal(htlc_bc, origins) // 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 revokedCommitTx = Transaction(2, Seq(TxIn(normal.commitments.latest.localCommit.input.outPoint, Nil, 0)), Seq(TxOut(4500 sat, Script.pay2wpkh(randomKey().publicKey))), 0) 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 12958bf700..9af49f533a 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 @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire.internal.channel import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxId, TxIn} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, Satoshi, SatoshiLong, Transaction, TxId} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.ChannelSpendSignature.IndividualSignature @@ -68,11 +68,7 @@ class ChannelCodecsSpec extends AnyFunSuite { hex"304502210093fd7dfa3ef6cdf5b94cfadf83022be98062d53cd7097a73947453b210a481eb0220622e63a21b787ea7bb55f01ab6fe503fcb8ef4cb65adce7a264ae014403646fe01" ) - val sigs = c.commitments.latest - .localCommit - .htlcTxsAndRemoteSigs - .map(data => Scripts.der(data.remoteSig)) - + val sigs = c.commitments.latest.localCommit.htlcRemoteSigs.map(Scripts.der(_)) assert(ref == sigs) } @@ -191,8 +187,7 @@ class ChannelCodecsSpec extends AnyFunSuite { negotiating.closingTxProposed.flatten.foreach(tx => assert(tx.unsignedTx.toLocalOutput_opt.isEmpty)) val normal = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[DATA_NORMAL] - assert(normal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - normal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.foreach(tx => assert(tx.htlcTx.htlcId == 0)) + assert(normal.commitments.latest.localCommit.htlcRemoteSigs.nonEmpty) val closingLocal = channelDataCodec.decode(dataClosingLocal.bits).require.value.asInstanceOf[DATA_CLOSING] assert(closingLocal.localCommitPublished.nonEmpty) @@ -216,21 +211,21 @@ class ChannelCodecsSpec extends AnyFunSuite { assert(closingRevoked.revokedCommitPublished.head.irrevocablySpent.isEmpty) } - test("verify that we don't store local sigs for local commitment") { - case class TestCase(localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, encoded: ByteVector) + test("migrate remote commit sigs from local commit") { + case class TestCase(localFundingPublicKey: PublicKey, remoteFundingPublicKey: PublicKey, commitTx: Transaction, encoded: ByteVector) val testCases = List( - TestCase(PublicKey(hex"020d9b51f544c7a1c061a6604badd79d068a7e1078ee06a013e17a39c13cb302f2"), PublicKey(hex"028b8f6f6eb0ca68d25760a419c007d7e74c2228ee53448042e6e06b1eae254e5e"), hex"00000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000400633f73979e2e7cc9a76f2dbcccf4d9000000000000022200000002540be40000000000000003e800000000000000010090001e000bd48a6ca0842ad7ec52d29f4be4c7015ea447e435e5404380000000c5019a59a1964d2e91fa2ae22c629bd57e7458b98373a3503a6b3675778cc09c3298000000000000011e8000000002f34f6000000000000001f400000000000001f4004800f18145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f01df72613647456031c11cf28c90e06230848223b1064616b89bb2e8d2557c631f011d20840603112fa5faaa4053f09f9f50bcdf94f392941569ce09310340dc50020117810d980eb608cae903683abfc8438fef5421397e688d8c4aa3a47e0fc3418601b035c2fabbe71271d3a07be023eae63e4ac5272bd09a6b990a5409160f3ab28a000000008400800000000000000000000000186a00000000000000000000000002faf08000124e7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463800000000015d04300800000000011001070d0d1c4bd6ad8c9b7bcb36f6308b955dc28e4970b43991232fe217bf4a56deb0023a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957009801000000000080ce7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d7646380000000004e14ca4000a3318080000000000b000a0b0517d7dabd6f2da1a5bfef97681bd4958a94018200241822811080579dbcb14b3d3f2c7200693391d43977881357806ebd47b2633d0c1b5ac1a56481103c3a8f6a5e15cab0144f99ec5f875fabbdf32ef6748326f4a5b3a1b29b8c7f9e80a418228110806b4c99af1b3c8f74bef604336f1a1200a754f1680923a511f7af92391bac39ab811019085fba36cec85b5537807d07f2931858413a56c47b2cf7753fbcf4cc7206bc80a3a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957435cc1100000000000000000000000000000186a0000000002faf08000000000000000005fbb84fad0781822e1268df19eb42468a5041d0b44dc8c7a21aa7aae19409bab81e514d9069fc6064c17f35d1d0f227fbe1b74ca89fec6c67b9668fe96341d43e200000000000000000000000000000000000000000000000000000000000040ee389313797b7f5177522d46afba5c43e37edebee6598c72e5a94c81fcc50a840009273fbf78b1a6e126d34c0e73d82e357ed5724854114e96ca3d373f2c30ebb231c0000000000ae821804000000000088008386868e25eb56c64dbde59b7b1845caaee14724b85a1cc89197f10bdfa52b6f58011d488408366d47d5131e87018699812eb75e741a29f841e3b81a804f85e8e704f2cc0bc8840a2e3dbdbac329a3495d829067001f5f9d3088a3b94d12010b9b81ac7ab89539794ab8000139fdfbc58d3709369a60739ec171abf6ab9242a08a74b651e9b9f961875d918ece7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463cc80398ba555ae3310a427fe5a79299e970d0b199fdade631246d123ea6324e58f40736fc2307f587e516761e35b11c3d960991f1d905d523ff0cbdd88d527790"), - TestCase(PublicKey(hex"03038461f9a84dddcf1a1f8adf424a2aa5250b1ab0ad53505d7face53a1b70cc9d"), PublicKey(hex"02c5d5b5f7291d11c052ce00bde1b4517d309f2b73f99c798fc3e3329b215e3319"), hex"000002010000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009c5b105f42a2aea346cf8759f7c78135158b93878faac0659b26f723cc549fded80000000000000000000022200000004a817c8000000000000028f5c000000000000000102d0001e000b000a490f9af5166835d37b0f2155fa3a7a2b4fae66fd8000000105450189fed9a69c67c55d9743b3dde05b031088442ad94e9e3437e15f479f5cd66a7e000000000000011e80000001eefffe5c00000000000147ae00000000000001f403f000f18162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c8179a7ba86216d011a9c1c49684578a6e0ca111279e6641404dee194eabcbbd21c81aaa1f3d35df2711a08374da8278fe259f6085189cc539a144c1aa0744b23495601331777d0354951fa354a8a2ee70e2bf40c9bb3f4fe340c9dab02bba5b6bb1aa00158bc5a03c0c79b947e5ac70f76f600c7fab8561e244f1425935fc51ffbe330380000000181515080800000000000000000000000007e800000000000000000000001f3fffe0c00121c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af3800000000015ffffff80000000001100101621d570811311537fddef81ee6f7023e2d17c11d32a53f472887dd035a78a518023a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea9570097010000000000809c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af380000000005657b04000a47fff80000000000b000a66e6e46b946f3259039de2840809a4b340aa510e8200239822011030949de6ac0462e1b5a42ded6c7763779d9f6aab069cb3366a3ca330bd1fe52b81101564903650deeb2a1f719865742e99ea0986774184727607a69bfff9f552a45a00a398220110228710b1083bde27eeb793ad155142cb69655ad8424eeae2044808edc631840d8110225098637d7f91c20e00d4a82a56520ad3cead728dd5b82b0bdcfe40d4b6acfb00a3a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea95773ad9b100000000000000000000000000000007e80000001f3fffe0c000000000000000063aa3a9e7e5e45c265ae8bf12e96a239ddbae4224c6f4560e149cce8a2675b208178d03c15e7303b3415e3549606ecab96ff5250ce38b2052b0a9dd2ed0af5ee75800000000000000000000000000000000000000000000000000000000000409cb1d9e3463cd48a65e9a2fa95f0755e924dba3664026d3682892bbf9527396bc0090e15d1b8b9e11d80d0bc0f32e6f9b9f4b9caa937cf42a8b26d64ae1ba8174d79c0000000000affffffc0000000000880080b10eab8408988a9bfeef7c0f737b811f168be08e99529fa39443ee81ad3c528c011d48840b1756d7dca47447014b3802f786d145f4c27cadcfe671e63f0f8cca6c8578cc64840c0e1187e6a137773c687e2b7d0928aa94942c6ac2b54d4175feb394e86dc332754ab8000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce32de50000008000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce0656b85b7c0e2dd09a620e28ddf865c6197074354c922bb9b76fb27ffe7fbf19cc0"), - TestCase(PublicKey(hex"03571382973ec0873b6668463dff0e0d426e85059e30888c448416eaad4849061b"), PublicKey(hex"023c22f28ad8e7aef1e0e7c2ea8902768933ca0a7f391591f3f63e1d66fe8b948e"), hex"00000203933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400041507665ff588dba1b9d4949e739ad05b0000000000000222000000003b9aca00000000000002718000000000000000010090001e000bd48a65a1ef54e50a013d73e46815abed7672835c83f34380000000c101d9ed460a901187b39b2c00d859f885104116abf778bf31c369342e4147db56e4800000000000011e80000001d87278e000000000000138c000000000000001f403c200f1811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca4701e06722e502349a2857e99bf9299fe781a343aebce7557c0c3738e20f14b9e96f81985a99219fd30dcdf40438cc4d01d6027087f65d6dea4e0cd9864016febcbcbf8176c51f0ee44572f83a8a02f50053ed3cfcedbf5ce6c70f70970f5784c0bb0dfe0195a18fc266e7e7f1dabcc5234f493620de9f8e8d5932de3e8a44626974e0dd1080000000c100000000000000000000000005b8d8000000000000000000000001dd3826e0001208e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f10080000000158c2b7a00000000001100101395ec5380c9641495ac8c4bb01605cf6ac58ee89a33da93d5eb09ab4be0e0ab8023a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da95700980100000000008088e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f100800000003eb8c1c0008006f600000000000b000a0eb37e79f65de8a4ca4df80979505338bcea6e76020024182281108070acce2513bf0df155ec51404333ac0397b6015bff2d87120d51d8dbf0b0f698811013a616842936434a093f8e9f85cbccb846ec2e0c49c9b541d46563cfb181886b00a418228110807ab650f61af7ce294df5a26e8cc1abb3658f0201c0cb9a2484e43fb9e61622d081102767e9f7608de2330774556121ddbd48c7a45d9598e38c821a3d9846c37e75f580a3a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da957360f66900000000000000000000000000005b8d800000001dd3826e000000000000000005ef1d8da098d1f363033e64f2272a12b638bd3fe501c829d8350e8f3fc1c6b1481f12f3b5425786f43870f00174ddd36710d1864bbcf24aa54694cb183b43ebcbf000000000000000000000000000000000000000000000000000000000000408200a48c21439ebc4d7c70b310000bef69e454f808dee73be19aa960d54cac8100090474c0e41a2090596e710d56e7450d6cf58091d2ba6c4c498e9e8a24d19cb37880400000000ac615bd000000000008800809caf629c064b20a4ad64625d80b02e7b562c7744d19ed49eaf584d5a5f07055c011d488408f08bca2b639ebbc7839f0baa2409da24cf2829fce45647cfd8f8759bfa2e5238840d5c4e0a5cfb021ced99a118f7fc383509ba141678c2223112105baab52124186d4ab8000023a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc62b21ec0008d8000223a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc6049c6fe56892029844393a0afbf8c2c19088f565e80764b75a7e0591b97c1108ac0"), - TestCase(PublicKey(hex"02022b6d4bc22a31b82b31f79006031e7b0fdb58f1bcdcaa89ff0d1759700690b8"), PublicKey(hex"02f9cb479f8f313003191a891347276348ded1214f34ace6ff0cf7f4ad8a235471"), hex"000005010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400045d37887e8d909085b00af1d8b09c514b000000000000022200000004a817c8000000000000000b18000000000000000102d0001e000bd48a3d3ca19286d69a373eb6dda039da0cccf0bb6330c380000000c501df7e8244b99cc46d750bcef5830755886bad20416438b5973b3c8174f9d85b93000000000000011e80000000086131ec000000000000058c00000000000001f4004800f1817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa388107f85642fedecfcb7ef3cea6b38e26aead2759263d123692fd0c7d458fbc188881580b52cf6cdb449abc060bce9db3f35eebec2c87b316fd7a2c3193296d4a6e2e81fd2e032dd33ba9fa974246fccfc3ab5ccc1eb9ede9c900fc4e5d455c42150b3a8113237ef34839fc8055cbb63c0b9892defe6554c9dc70813681c2f3f35dedc13300000000c08080000000000000000000000010de8000000000000000000000000876dccc00125f6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a915808000000015b7aa82000000000011001004a9c0705b42fa733f200639ed816601f0f413b9f29616f9c23f02f0dfc1df7e0023a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a957009781000000000080df6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a91580800000003df2714000811e8200000000000b000a11e33689533b2499781f9b2f9c1893add8c57c4e02002418228110806567eeed21d6e87a045f3debf5ca10a05339c25f021400d96f7b492a957a94d301101df62a1c365062cce3b9a3ef530a4cd37eae4346491ed37e050dfd54213ef54f00a3982201103484562bcbb22ae3d892808bb04ff25f309a4732474a57bf6dd7691e17fc3df6811006854f362bcec61184d4721dfd30f777485be3580cd116564d139668bb6ee3a280a3a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a9573b51e910000000000000000000000000000010de800000000876dccc000000000000000025f43cb5a86018845618974486d0cb225355649b88690a914ee6cfec1a75286481d9daff2fcb7edb8d99f8fab9f77ff1033d5fc33a42ea7f8b70d29614b6d3195d000000000000000000000000000000000000000000000000000000000000409799acc6bbef72e7bc3dbed7831ac4e4ecfdad169d19628429edaa495d3c79ef00092fb68559440749003f98e1c6e05423064ad260a948d12649578de549326c548ac0400000000adbd54100000000000880080254e0382da17d399f90031cf6c0b300f87a09dcf94b0b7ce11f81786fe0efbf0011d48840808adb52f08a8c6e0acc7de40180c79ec3f6d63c6f372aa27fc345d65c01a42e0840be72d1e7e3cc4c00c646a244d1c9d8d237b44853cd2b39bfc33dfd2b6288d51c54ab800017db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a4557db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002f5228f4f2864a1b5a68dcfadb7680e7683333c2ed8cc30f7db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002c0028c95c468f1c569d43c9af6d335b8820bdda4888fe000200000"), - TestCase(PublicKey(hex"03d57bc818cee6523ac976e32c532baffa68028e165ca74640663bf2ac4256182e"), PublicKey(hex"03efea3bb157f8b21fd6cad3adecad654c40be673d852fcf9204737e8154865d3a"), hex"00000503933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000454699830eddf332655e0ff86e6c536f30000000000000222000000003b9aca0000000000000186a000000000000000010090001e000bd48a482e9d4f3df4621c059bdf7df20f5ec9c4df26364380000000c101bf0694efa44b50d2817c3ac271bd38ad5110d72162deb6328cc919097ed7727f8000000000000111000000001dcd6500000000000000c35000000000000000008048000f01f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d01125974b8fc60d0122160b22db09edf15265c9f8d42c407a4b6e286445ce2897e01c8dfdd96184b8a8a0faa4d5d92acea243bb257e2bf911767efbf3e0fee216d3b81f4207be48f4ebbc397fb0bf3d223696ce431919a583ad69a7f3825323977b38a811b7e73e5b685f78f56e8d250e68d99452deed356a826fb5f207d314d0b61b97780000000c50000000000000000000000000001770000000000000000000000012a05f20000121a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c75686800000000015c04b4c000000000011001029e7f0fe79b14835816eb3cb89338bf442f040ee84ea6fdfb401ac2aac96919a0023a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570097810000000000809a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c7568680000000003b78dfc000b0ca4c00000000000b000a62d6449887f18b11f7c38a7d73a58620a23bda4682002418228110805b5b0d8c18572b99a6496d58eefe9bc20bcf359d91cc480c022591ba7c86d2280110096244c4df48199bd71b654c29ac49aa2a1dfcb6f2554a89b7d38bd28de98f6200a39822011013d445ecdc92ced0ebe8920580d3e5e7a1cdfd19fd39a5e57495a80e8a7cc0630110163a3f38ba3f23acc687ac397fb98d8dedb9a07a4085dec28d5a792e9ea2aece00a3a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570cf71b1000000000000000000000000000000177000000012a05f200000000000000000054e507323b7ab08f00c834e2ba694233f9d50293312291fc893691e512805f45814c861ee65261b9b7bdaaaebd59d316e5bf00417c2c9afeb5db143096feff164480000000000000000000000000000000000000000000000000000000000040b625f1f1e5239ce80103faf7851d4adbfbed7cf5bace51b959d5d6050f88e65d40090d45bbc65cd06922641ad65f6dfe21b898baecc7168d2418b57cdba8fa63ab4340000000000ae025a6000000000008800814f3f87f3cd8a41ac0b759e5c499c5fa21782077427537efda00d615564b48cd0011d48840f55ef20633b9948eb25db8cb14caebfe9a00a3859729d190198efcab1095860b8840fbfa8eec55fe2c87f5b2b4eb7b2b5953102f99cf614bf3e4811cdfa05521974e94ab800006a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522920ba753cf7d18870166f7df7c83d7b27137c98d90e6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522994760b5d8d708a41fe54bdd87888baf41f10e7bd0e000200000"), - TestCase(PublicKey(hex"0297d3589bbeddeb079bcd9495d52cb47ea20515f962501f0de15a8f4a66293a11"), PublicKey(hex"0242199d21cc7a2fac5c4168b6c3635bb0cc63ed4b920651833f8395f010bcc01c"), hex"00000603933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004004c3bfd866d20986aa90ec265fdfcbe0000000000000222000000012a05f20000000000000186a000000000000000010090001e000bd48a3b48ca7482f0442970651ba40bf45091fddef3a84380000000c501a0528fcd6c721246634deb024236eb2f4c124e13b587093a6652e7bfc12db3250000000000000111000000009502f900000000000000c350000000000000000083f0000f01210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e0110d8d9aa7be8b4553b0af05b921c72c48356c4a0af61ffed7f1eadb7c5071c64816bf131172fe61d3145d39511755ee8095927534b76c4bb2464b706eaaee1486e01dd6caa8417804e26ce56f83a2525aa05fba25d3bab6db2cae5a203ce126da1ec01c8dfca1943df0dd0d59ffda66e207316c4af1cd3a4b6443fc5298a7a3864658f00000001404500000000000000001400000000017700000000015b95aa0000000128aa5c560012338c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a45800000000015c04b4c00000000001100104c97a415e599a3817965ff9a64e87d2af9bbcd77cae148ea934f9488216ffee10023a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a95700ad01000000000080b38c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a4580000000003eef56c0017b588000000000001100100783bea5072cc7b3be1475a5a8d7c45f16978bbc8b914937c89631a4e7f0d7e3b5714b80000000000b000a0c933f5731198ebb4e0fda5b7ba130c1c4d2fc870200239822011032e32cd21310cf1864c862bb32b9c78b1b8a626d1094d47d0e1938166388243f81101bbf4ba992687498419d57da8e6d700809fdf1f882425296292ff55b67ec82bd80a4182281108054815d830690f798ccd75722576882f62a088d37e81cde50c40bc5b080615c3e011030965e18229cbf3a03024a97988d75cb1b1569eaa1ce381d0689958161c4799900a3a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a9571b2c3e90000000000000000000140000000001770000000128aa5c5600000000015b95aa392494f078bec5b681d4997f2497d52112ad54eaaa81dec63f6480336139048b81fc2e019907aefb5efc5922187f575e10635611bde69ae0bd46b0b6ea44aea7748000000000000000000000000000000000000000000000000000000a000040f7dd9de11426234d0121ec1f5d2d19c3b4e78679b2e07a5c4f91bc624458352bc00919c6270129982a1028611c3db4f5db274e442089dd8822f4cb5739f158d80522c0000000000ae025a60000000000088008264bd20af2ccd1c0bcb2ffcd32743e957cdde6bbe570a47549a7ca4410b7ff708011d4884090866748731e8beb17105a2db0d8d6ec3318fb52e4819460cfe0e57c042f30070840a5f4d626efb77ac1e6f36525754b2d1fa881457e589407c37856a3d2998a4e8454ab8000800ec0003ffffffffff80106b5a98105912cee7f3cd2a4ed131283ee6fa4f5aef2e74aec08ebc2d6770ee38001e80007fffffffffec0082e868d9d8ebcc94bff771ca9d328bccfdfafdbc17e2b078b728caa0bb7179c6420001ffffffffffb0ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c02916000200e60400000002ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e000000000002029e04000000000202ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e08008e608804401c9f21ea6ff52325709bc8fec460de1d9f584cbf6aced6abeda5842595fbb968044076a3a4b918db66a59ee47c4d3a2f8bb0f17665e6d904df5cab74ee06cbafbb3e028e60880440b6af6af4787b1229039a7351f7156d0cc30755a1902c6d483ea85915a29b40f6044023ea5be6088489f6dda062498c485465c7451d7bf49eb46dae206bf952ddc860028ea4420484333a4398f45f58b882d16d86c6b76198c7da97240ca3067f072be02179803842052fa6b1377dbbd60f379b292baa5968fd440a2bf2c4a03e1bc2b51e94cc527422a55c0000000000000"), - TestCase(PublicKey(hex"03455b390d9fbc70d538f3e38abaf1a217e66b45c87ff025f3be7878697a84796a"), PublicKey(hex"03c6b8a41701c6abdc9951432c94873cfac9fa5c3bbad8215e28012c8e212748d8"), hex"000008010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004515822333393159ec64e3694540844cd000000000000022200000004a817c80000000000000003e8000000000000000102d0001e000bd48a04a8223d31a871ecced64af1c4690532d4b5de984380000000c5012e53ad31acde84a88f55e981f2c33e8d6b451e15463d3da9335dbe9e30a474c60000000000000111000000009502f90000000000000001f400000000000000008168000f01e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c0126a65afe85868caba0a468ae6adf48b02e3dbd527a7d6e86c8b6426af06a5cce81ece00b5452805039314df65a98f5eeb21eab3d917853cf49567c28392e094806012f5f8f9177f320504b51ee8112a62fb5c505b8d9167737731545d99141d3485501004c1a5ece2ff034dcc63ec008df0ec8185934a2fcaf3075a75a5a999989d96580000000c50080000000000000000000000057e400000000000000000000000002faf08000125d32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c800000000015d04300800000000011001006333ad899044f5d624983aa15a497eb65df4372319091299696efb7f3d7b4cd8023a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c2957009781000000000080dd32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c80000000003b1f4bc000ae038080000000000b000a12c0490f3a2d18421362bc7a0b9aa7302410903302002418228110807b9455b3bbed8d0994cf1fd0d8fcd2e63a03710f470943da74ef0f4513fc6f43811009275b2d4020f8bfc179ae4dd5cb3935c30767f48ee723cca0d708184b01f2c400a3982201102f192e0d772fc5b08cba1e7b1c39dbed4f64439569c493af18a9539818a6f36c0110272c9b5f9058e69485ca53388311c00ff69b5631334ed4e002cef0958389e42880a3a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c295717042f10000000000000000000000000000057e40000000002faf080000000000000000046ec68ce2b1bca12747a13122270527bdad325cd34478d112adcc4bd0fc25f1e814f2919e34f1842677f99901e2aaf7f0abfafd39e6350238b52b8c2bf3b00ff7f00000000000000000000000000000000000000000000000000000000000040f5ca8901722651214939b99ca3c79307f3380af44ae1aecd4d317784fda7a15d80092e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513640000000000ae82180400000000008800803199d6c4c8227aeb124c1d50ad24bf5b2efa1b918c84894cb4b77dbf9ebda66c011d48840d156ce4367ef1c354e3cf8e2aebc6885f99ad1721ffc097cef9e1e1a5ea11e5a8840f1ae2905c071aaf7265450cb2521cf3eb27e970eeeb608578a004b238849d23614ab8000174cb1e745e44eb76e4f9b1ee54afa30be4dc6219f078e42636a92e17f15a89b2000000005d353b426e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513660192a0c75d44f2a6f4e1bc66fddf10b2949b9896cbe8548edeba49c0dd8300d82f42ef7330fb4601c7f2e1af3be67e3c3e86bb4c93a3bd9085c53732111c42dc"), - TestCase(PublicKey(hex"03d4c8565cb50638dc9a36d147e271aa4b052af498adb423ed1402922c1e9e491a"), PublicKey(hex"02a4adefc639aff3bcc872187529c75bea3a199857a002645e99991a942647f6fd"), hex"000009010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000411bde5f686930a7085566a22fd7fac49000000000000022200000004a817c80000000000000005dc000000000000000102d0001e000bd48a142af4dfb86cd991ad356e17ef245bd58413568ec3800101000000c5017ebac21a9e7d42c23db38b88b689f1efb9ad52d0e728e4553493dcde20dff8f70000000000000111000000009502f90000000000000002ee000000000000000083f0000f015256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e81290a232a7ee5a18660742d9368628ddb929322bf8e55695aa3af19002b1fea2e810acfc782fa2344e096e930b4850396e2e004d875727f0839975bd3d79e3419eb81cc09de2631507630dd3788182d4e3e2c16c246fa0277f68efb26f195d11d6ce801a2b78a195c345f107a9dbd8d4927ca9c95b68c9c7431bd64132f1d5012d9c8dd8000000140450000000000000000030000000001770000000002549e60000000000223ca6000122b8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c3000000000015f824810000000000110010682606ad0a329b07977b91626c3018d8c82cf084a7d4ef1f800efa5cd36985e90023a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d295700ad01000000000080ab8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c30000000000625ece40012c8b0080000000000b000a23484912e6810c08eeb52dfc67cee7a1d9321e1abc1880800000000011001028f4b0e4b17b183bbc7432902506a3accedb2459daf3146d8e0b1ac8d2a4b27782002418228110806873db49b74aa163df0331441bcc8cae3935262ce6accc9ce7e70dec462535c4811036156baddb0c8598cacfd150a6f7f3893d53b784ea0e188bd2400f0d68abf5ff00a398220110054bfe99fb9bd42af0827b4092a86f8c113ffecd69a7e7ac15d057da965d20ea01100c244a344b23872542da30b81f0dadb9790f4370e224de0b75ad606d79f76daf00a3a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d2957663d311000000000000000000003000000000177000000000223ca600000000002549e602184aefb8063b9f14a8ef4be8ba84c09107359d62c994df78cc3b07e4c2abd7d018c4e716e3df369d6d8b17aed7197e453ac4ebb7a99b55e101cd2347794edc471000000000000000000000000000000000000000000000000000000010000409d47ae68afd388ad7bf011aac01308facdcab44edb8dc6f39409b69c4837f76d400915c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa6180000000000afc124080000000000880083413035685194d83cbbdc8b136180c6c6416784253ea778fc0077d2e69b4c2f48011d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694ab8000800f80003fffffffffff0020f2d63ebf3cd801d8cd1d3e6be1841a0e1027cff13d633b8efc7362951ad8c040003f0000fffffffffffa00407685484eaf45d16eae74cfb9a045e3e475f69d6362cda0f327659264a2db94d30000fffffffffffa571efe1ac29e368873c71b8a2c20becc0c72651d42c4c91b548454226d3fe986000000002eb1182c8000000040568080000000004055c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa618000000000312f672000964580400000000005800511a4248973408604775a96fe33e773d0ec990f0d5e0c404000000000088008147a587258bd8c1dde3a1948128351d6676d922ced798a36c7058d646952593bc100120c114088403439eda4dba550b1ef8198a20de646571c9a93167356664e73f386f623129ae240881b0ab5d6ed8642cc6567e8a8537bf9c49ea9dbc275070c45e9200786b455faff8051cc11008802a5ff4cfdcdea1578413da0495437c6089fff66b4d3f3d60ae82bed4b2e907500880612251a2591c392a16d185c0f86d6dcbc87a1b871126f05bad6b036bcfbb6d78051d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694abb31e9888201dc04000000000202a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a80200000001c00e00000216600200000000002f522850abd37ee1b36646b4d5b85fbc916f56104d5a3b0e0690608a04420138a23062b387099c97d9458403dd33953e565d054b209e98684f4bd602cca268044076e7132bc91195ff251682cdaf978e6e7998f065ef6d4384c0ae7176888774ec02009ac64206e1126632e2d08e50f47925f11f8b04aba20cad7a65a25d0a32e2931d17945424ce05c00f64ea4206a88d1fa87c84e0c5228a2a5b045dccb605a01117740b58a7384f5d66528d7062d1580000000000000000000000040048ae3dfc35853c6d10e78e371458417d9818e4ca3a85899236a908a844da7fd30c00000001a920ea2ec3e76f7b403ba673b35954861170448627cbe7ecaf7c00ece94988a40048a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a8020000009d78c152837378b3cbbc9b373ffbf698792e9046ee8f942855f69cd7808891e000000"), - TestCase(PublicKey(hex"03d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c46"), PublicKey(hex"028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb26572"), hex"0100220000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400092dd581059d2bb431f96f6c199e59a9cdf4ddaea58dca8229725660804592523a80000000000000000000022200000004a817c80000000000000004b0000000000000000102d0001e0016001411e6e9810bf2d21b329be20c89fa19556bdd933c00000003080a8a037245a5d1fc8a4d0cf1bf882c121732b9255a327efba1c1c63e10f6ff481199f30000000000000222000000012a05f20000000000000004b0000000000000000107e0001e028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb26572021021b7b90467b6d39f86b9160d4801dc1e92eedbf5303ffc419fc013bd24a91d02766d36e2bc109b08ec5b0850c53a59372cd306cc1f95cefc4ceb24a6d7886e1b03a9ca87c8642c6770627ee356f92ceb075fa23aed68a6b952b50455ba939c9d85038ffc2170ef33aca1f4741347f1de06cda9f60cd2d5121e6bc8892953a38bb00e000000028a8a0000000000000000080000000002ee0000000000b2872000000000067486e0240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aefd015a020000000001010098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f00000000005141888002b42d000000000000220020143e4a3411ea01f087e0904a2ae0f4a312355368091aa08acf2279f9b6b72711eda40100000000001600149be0e57e4e3561b30fb11014bc3ed2036855381c04004730440220068466106db388aabc63e77d137b815649614d0ac2530589ab4c5e05d78afe3f022064c9ee1be2b7b794b6645c7b5ec8f0e69e727980dc7219fe1715e9803178a98201483045022100e8e5ad22d38e27f56ad53e43e34656026a74ae3527ceb06752f9114483b057d702205978489fb86927818e0b71deeed44c8c5827237f40f92483b36688cc48171bb101475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aee93b9820000000000000000000080000000002ee00000000067486e00000000000b28720ed5846e908bbaaa6b2f994f3c03f8f96350d2825cd6849104207a8ee263849b302bcc40fecd594b6fa15d22cd5e9a8df070ce97e8a1e90b4fdd8a1ef0ddb4c0d97000000000000000000000000000000000000000000000000000000010000ff036fd9558ffbcee019936ae3251b1696cf0ec92a58a73dcc6e0c33ea64738242e4240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652ae0001003d0000fffffffffff80100a03da9ba67d38e18a51216c7d501d200aa3127445c959dd972193d3dd5951e7c0003ffffffffffe00098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f1bffef0000420000ff0088d2195367160d41c7c2bf78ce8996e62b632adfb829c8382955b1e8a5c27008dd7e9d91440a520b59c49af0c1a5a5e1768e5eab47556a81c6248278acc7f5757643497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001bffef00004200005f644c69010100900000000000000001000003e8000000640000000007270e000000"), - TestCase(PublicKey(hex"029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff2"), PublicKey(hex"02b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c23"), hex"0100250000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000425ea3f2629ba65b6849f78829099a1d10000000000000222000000003b9aca000000000000027efb00000000000000010090001e0017a914d73900cac21afc97ce994c27af395e7f4d681bcb870000000182030540ba863ec0edc426981ffa8eace0be4e9a03295b3a0ff6ca545d21983e3b6b0000000000000222000000003b9aca000000000000027efb00000000000000010090001e02b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2303ac60bfe54dbbd771b9d4c92dd33b584d91cda6f4da296aa10f009f043743aa90034980f41ef05b9248f6793f77666534685df92b54dcb4ee1030f5dbef2e88a4e102177e1b10f681a1a46221c0f87aaa8e2a49d8257924d229faf8525e3f447374e5023c6191ef9b284cc6aa0c88b3913d91bf1bde6c5ec6f9d6f8dafa6bb65c2dcbbd000000010a01000000000000011f00000000251c000000000000000000000003cf01bee02412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aefd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000011f00000000251c00000003cf01bee00000000000000000a6b6d346b134eeb64ed57d0b125934490af31cbb650813ae6454ab83d12042550331d262498b6ff23a23ac5a3e834afddf752bf1b5e81259233316a9ec72f5562f000000000000000000000000000000000000000000000000000000000000ff02db56e0e64c827f44aade7b016a37e6a7c0780db3666f7d8e8b422b6b02474b362412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352ae0006003f0000fffffffffee20041f7ae2006d76bc11dc18f8457cb4f76df375a85312315edbcfe508aca1596bcf4007a0001fffffffffdd002022e11a1cea6e7a8d662f74967d072ac654a72b59ada363c0e363ec4d26f8ef6800380000ffffffffff002065215a990d4af376ceb7647998ceea281d44fba94ec18c3a505f595fae0efe79003c0000fffffffffef00209a10496ebab959fe95ddfbe43f26cc9566bdc3a9cebc509e098367a2916b5e86003e0000fffffffffee40083551db0a0be081e7788bf7b13023ab3f306657022a9623a2179ca37c92f2e0d3801000003fffffffffb84008210ec1faedb71958c276e8e1502e7e1e588d4406cc9986157958e9d84f02a667e0001fffffffffdc212f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c00000000005f47acf200000000fffd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000012412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000083b5c6292f61ad3cba1e702abb6d4ec1aec503c5b1b393e09c942ad4448a1b930000000000"), - TestCase(PublicKey(hex"03f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba"), PublicKey(hex"026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c27316"), hex"0200050000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009e9907c0bc4f4e5b55a3b7012180e63b0ebf76498c543b991c056f0930a6ac09d80000000000000000000022200000004a817c800000000000000c350000000000000000102d0001e0016001495cde5680fa7aaa16935b74cc11b4ee695aa6b1500000004080aa98202cc1bef9c46bae2f3a965d7bf4bfca249098c5847b71d700ca0eba00db523986a0000000000000222000000012a05f200000000000000c350000000000000000107e0001e026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c2731603d49ddadc0517b1bb1f9c2773621e62b7720b2a622e4a5548becc37164bcd70f10368718d6b2887b302f75e5af0bac2f3b883663bde0fd61344ce16ed9a31c55eda03530674aa8610a599d0cd8be226923b5d8fe2a422cfbdc4fb790e39c7b05e0b7e0246ce4c46c2ee4110cce17bae3c7e6de01a8f0f8c1970cf58b2122b8fc77ffdeb000000028a8a0000000000000001aa0000000002ee0000000019545f970000000110b19269240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd015a020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d00000000008dc3288002ff7b0600000000002200206444bace1cb90460914f5c964acfdd7846104e93a205d0bd5d00eb73f03acced21cd4500000000001600148636658ef4d15c8d9824bfea9b70ddc592b24bb404004730440220137ad66cce66df0041e172036c0c6de1e1cc8981130883bf693960778a2160fa022039d9505c98f78f54aa30172f4deccae26f9c4a7b55e2b72d4bd8875809143d5501483045022100ced9a63de710f1339c5d9086b1cbe0097f942872fbf1d0ca3a643409933cc86802201d5e311f655f64fb64eae62c9a3e321329158d055ecce9c537657979c17fe62501475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae38497c20000000000000000001aa0000000002ee0000000110b192690000000019545f97359430d3b112dd6d24edb449958073132a7721c737f2a72edf12ff74606d6c6f022030518cd036759a25ae66be2bbd46855ba6cdd214742f42aecd72b9093a444a000000000000000000000000000000000000000000000000000000d10000ff0200c2341c9b6d1ba8955188aa913cbf5b303c65ad19491df7d15fa0a811c44d38240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae0005003d0000fffffffffe580103151b3b767e35873dd3babc0f5bc9ea4a99069c269456b9e1157dda87db454b7801f80007fffffffff2b0020b4a390fde1b8bf038232ea79e48b825b5d1e930a97f2532aacbe0ddfaf24e9be00380000ffffffffff002038da5bbdce565d1eea32b7b9f4c4932e63892a154197054f5eb0116aab0f41de003b0000fffffffffe60041298f04989916fa9ac7b83da8a340a8bc6fcad73925c190576d728b28dfcf8de400720001fffffffffd00204225a2a872ea98ff1b22991635a2d6cb598839bd8a3988eeeb53adf784149fa880007fffffffff2b000e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000001e2d5a0004240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15eecd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15c4cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15bacd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b150001240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd014f020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c87040047304402203167bba6344d2d82aa11db437dcfd17903de22079f58597f8927813cb96c32c3022036ead9938307bcc82561f85cebd8123b273ca26e9aeafc6e0502e97a38b508da01483045022100e0cffda9bd2bd045b99024d99597698a25803d42cd22c8b58d7a9e29363e79e802202a0e27d553bce59bb7b4e2cd89b847ade01d39bcb1027e0a7c0b6e0d4fff096601475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae00000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15000000000000"), + TestCase(PublicKey(hex"020d9b51f544c7a1c061a6604badd79d068a7e1078ee06a013e17a39c13cb302f2"), PublicKey(hex"028b8f6f6eb0ca68d25760a419c007d7e74c2228ee53448042e6e06b1eae254e5e"), Transaction.read("02000000019cfefde2c69b849b4d3039cf60b8d5fb55c92150453a5b28f4dcfcb0c3aec8c700000000009c299480014663010000000000160014160a2fafb57ade5b434b7fdf2ed037a92b15280386b98220"), hex"00000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000400633f73979e2e7cc9a76f2dbcccf4d9000000000000022200000002540be40000000000000003e800000000000000010090001e000bd48a6ca0842ad7ec52d29f4be4c7015ea447e435e5404380000000c5019a59a1964d2e91fa2ae22c629bd57e7458b98373a3503a6b3675778cc09c3298000000000000011e8000000002f34f6000000000000001f400000000000001f4004800f18145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f01df72613647456031c11cf28c90e06230848223b1064616b89bb2e8d2557c631f011d20840603112fa5faaa4053f09f9f50bcdf94f392941569ce09310340dc50020117810d980eb608cae903683abfc8438fef5421397e688d8c4aa3a47e0fc3418601b035c2fabbe71271d3a07be023eae63e4ac5272bd09a6b990a5409160f3ab28a000000008400800000000000000000000000186a00000000000000000000000002faf08000124e7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463800000000015d04300800000000011001070d0d1c4bd6ad8c9b7bcb36f6308b955dc28e4970b43991232fe217bf4a56deb0023a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957009801000000000080ce7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d7646380000000004e14ca4000a3318080000000000b000a0b0517d7dabd6f2da1a5bfef97681bd4958a94018200241822811080579dbcb14b3d3f2c7200693391d43977881357806ebd47b2633d0c1b5ac1a56481103c3a8f6a5e15cab0144f99ec5f875fabbdf32ef6748326f4a5b3a1b29b8c7f9e80a418228110806b4c99af1b3c8f74bef604336f1a1200a754f1680923a511f7af92391bac39ab811019085fba36cec85b5537807d07f2931858413a56c47b2cf7753fbcf4cc7206bc80a3a9108106cda8faa263d0e030d33025d6ebce83453f083c77035009f0bd1ce09e598179108145c7b7b7586534692bb0520ce003ebf3a611147729a240217370358f5712a72f2957435cc1100000000000000000000000000000186a0000000002faf08000000000000000005fbb84fad0781822e1268df19eb42468a5041d0b44dc8c7a21aa7aae19409bab81e514d9069fc6064c17f35d1d0f227fbe1b74ca89fec6c67b9668fe96341d43e200000000000000000000000000000000000000000000000000000000000040ee389313797b7f5177522d46afba5c43e37edebee6598c72e5a94c81fcc50a840009273fbf78b1a6e126d34c0e73d82e357ed5724854114e96ca3d373f2c30ebb231c0000000000ae821804000000000088008386868e25eb56c64dbde59b7b1845caaee14724b85a1cc89197f10bdfa52b6f58011d488408366d47d5131e87018699812eb75e741a29f841e3b81a804f85e8e704f2cc0bc8840a2e3dbdbac329a3495d829067001f5f9d3088a3b94d12010b9b81ac7ab89539794ab8000139fdfbc58d3709369a60739ec171abf6ab9242a08a74b651e9b9f961875d918ece7f7ef1634dc24da6981ce7b05c6afdaae490a8229d2d947a6e7e5861d76463cc80398ba555ae3310a427fe5a79299e970d0b199fdade631246d123ea6324e58f40736fc2307f587e516761e35b11c3d960991f1d905d523ff0cbdd88d527790"), + TestCase(PublicKey(hex"03038461f9a84dddcf1a1f8adf424a2aa5250b1ab0ad53505d7face53a1b70cc9d"), PublicKey(hex"02c5d5b5f7291d11c052ce00bde1b4517d309f2b73f99c798fc3e3329b215e3319"), Transaction.read("0200000001385746e2e784760342f03ccb9be6e7d2e72aa4df3d0aa2c9b592b86ea05d35e70000000000acaf60800148ffff0000000000160014cdcdc8d728de64b2073bc508101349668154a21de75b3620"), hex"000002010000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009c5b105f42a2aea346cf8759f7c78135158b93878faac0659b26f723cc549fded80000000000000000000022200000004a817c8000000000000028f5c000000000000000102d0001e000b000a490f9af5166835d37b0f2155fa3a7a2b4fae66fd8000000105450189fed9a69c67c55d9743b3dde05b031088442ad94e9e3437e15f479f5cd66a7e000000000000011e80000001eefffe5c00000000000147ae00000000000001f403f000f18162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c8179a7ba86216d011a9c1c49684578a6e0ca111279e6641404dee194eabcbbd21c81aaa1f3d35df2711a08374da8278fe259f6085189cc539a144c1aa0744b23495601331777d0354951fa354a8a2ee70e2bf40c9bb3f4fe340c9dab02bba5b6bb1aa00158bc5a03c0c79b947e5ac70f76f600c7fab8561e244f1425935fc51ffbe330380000000181515080800000000000000000000000007e800000000000000000000001f3fffe0c00121c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af3800000000015ffffff80000000001100101621d570811311537fddef81ee6f7023e2d17c11d32a53f472887dd035a78a518023a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea9570097010000000000809c2ba37173c23b01a1781e65cdf373e97395526f9e855164dac95c37502e9af380000000005657b04000a47fff80000000000b000a66e6e46b946f3259039de2840809a4b340aa510e8200239822011030949de6ac0462e1b5a42ded6c7763779d9f6aab069cb3366a3ca330bd1fe52b81101564903650deeb2a1f719865742e99ea0986774184727607a69bfff9f552a45a00a398220110228710b1083bde27eeb793ad155142cb69655ad8424eeae2044808edc631840d8110225098637d7f91c20e00d4a82a56520ad3cead728dd5b82b0bdcfe40d4b6acfb00a3a9108162eadafb948e88e02967005ef0da28be984f95b9fcce3cc7e1f1994d90af198c908181c230fcd426eee78d0fc56fa125155292858d5856a9a82ebfd6729d0db8664ea95773ad9b100000000000000000000000000000007e80000001f3fffe0c000000000000000063aa3a9e7e5e45c265ae8bf12e96a239ddbae4224c6f4560e149cce8a2675b208178d03c15e7303b3415e3549606ecab96ff5250ce38b2052b0a9dd2ed0af5ee75800000000000000000000000000000000000000000000000000000000000409cb1d9e3463cd48a65e9a2fa95f0755e924dba3664026d3682892bbf9527396bc0090e15d1b8b9e11d80d0bc0f32e6f9b9f4b9caa937cf42a8b26d64ae1ba8174d79c0000000000affffffc0000000000880080b10eab8408988a9bfeef7c0f737b811f168be08e99529fa39443ee81ad3c528c011d48840b1756d7dca47447014b3802f786d145f4c27cadcfe671e63f0f8cca6c8578cc64840c0e1187e6a137773c687e2b7d0928aa94942c6ac2b54d4175feb394e86dc332754ab8000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce32de50000008000070ae8dc5cf08ec0685e0799737cdcfa5ce5549be7a1545936b2570dd40ba6bce0656b85b7c0e2dd09a620e28ddf865c6197074354c922bb9b76fb27ffe7fbf19cc0"), + TestCase(PublicKey(hex"03571382973ec0873b6668463dff0e0d426e85059e30888c448416eaad4849061b"), PublicKey(hex"023c22f28ad8e7aef1e0e7c2ea8902768933ca0a7f391591f3f63e1d66fe8b948e"), Transaction.read("020000000111d3039068824165b9c4355b9d1435b3d602474ae9b131263a7a28934672cde201000000007d71838001000dec00000000001600141d66fcf3ecbbd149949bf012f2a0a67179d4dcec6c1ecd20"), hex"00000203933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400041507665ff588dba1b9d4949e739ad05b0000000000000222000000003b9aca00000000000002718000000000000000010090001e000bd48a65a1ef54e50a013d73e46815abed7672835c83f34380000000c101d9ed460a901187b39b2c00d859f885104116abf778bf31c369342e4147db56e4800000000000011e80000001d87278e000000000000138c000000000000001f403c200f1811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca4701e06722e502349a2857e99bf9299fe781a343aebce7557c0c3738e20f14b9e96f81985a99219fd30dcdf40438cc4d01d6027087f65d6dea4e0cd9864016febcbcbf8176c51f0ee44572f83a8a02f50053ed3cfcedbf5ce6c70f70970f5784c0bb0dfe0195a18fc266e7e7f1dabcc5234f493620de9f8e8d5932de3e8a44626974e0dd1080000000c100000000000000000000000005b8d8000000000000000000000001dd3826e0001208e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f10080000000158c2b7a00000000001100101395ec5380c9641495ac8c4bb01605cf6ac58ee89a33da93d5eb09ab4be0e0ab8023a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da95700980100000000008088e981c8344120b2dce21aadce8a1ad9eb0123a574d898931d3d1449a33966f100800000003eb8c1c0008006f600000000000b000a0eb37e79f65de8a4ca4df80979505338bcea6e76020024182281108070acce2513bf0df155ec51404333ac0397b6015bff2d87120d51d8dbf0b0f698811013a616842936434a093f8e9f85cbccb846ec2e0c49c9b541d46563cfb181886b00a418228110807ab650f61af7ce294df5a26e8cc1abb3658f0201c0cb9a2484e43fb9e61622d081102767e9f7608de2330774556121ddbd48c7a45d9598e38c821a3d9846c37e75f580a3a910811e1179456c73d778f073e17544813b4499e5053f9c8ac8f9fb1f0eb37f45ca471081ab89c14b9f60439db334231eff8706a1374282cf18444622420b7556a424830da957360f66900000000000000000000000000005b8d800000001dd3826e000000000000000005ef1d8da098d1f363033e64f2272a12b638bd3fe501c829d8350e8f3fc1c6b1481f12f3b5425786f43870f00174ddd36710d1864bbcf24aa54694cb183b43ebcbf000000000000000000000000000000000000000000000000000000000000408200a48c21439ebc4d7c70b310000bef69e454f808dee73be19aa960d54cac8100090474c0e41a2090596e710d56e7450d6cf58091d2ba6c4c498e9e8a24d19cb37880400000000ac615bd000000000008800809caf629c064b20a4ad64625d80b02e7b562c7744d19ed49eaf584d5a5f07055c011d488408f08bca2b639ebbc7839f0baa2409da24cf2829fce45647cfd8f8759bfa2e5238840d5c4e0a5cfb021ced99a118f7fc383509ba141678c2223112105baab52124186d4ab8000023a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc62b21ec0008d8000223a60720d10482cb73886ab73a286b67ac048e95d362624c74f451268ce59bc6049c6fe56892029844393a0afbf8c2c19088f565e80764b75a7e0591b97c1108ac0"), + TestCase(PublicKey(hex"02022b6d4bc22a31b82b31f79006031e7b0fdb58f1bcdcaa89ff0d1759700690b8"), PublicKey(hex"02f9cb479f8f313003191a891347276348ded1214f34ace6ff0cf7f4ad8a235471"), Transaction.read("0200000001beda1565101d2400fe63871b81508c192b4982a5234499255e379524c9b1522b01000000007be4e28001023d04000000000016001423c66d12a6764932f03f365f3831275bb18af89c76a3d220"), hex"000005010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400045d37887e8d909085b00af1d8b09c514b000000000000022200000004a817c8000000000000000b18000000000000000102d0001e000bd48a3d3ca19286d69a373eb6dda039da0cccf0bb6330c380000000c501df7e8244b99cc46d750bcef5830755886bad20416438b5973b3c8174f9d85b93000000000000011e80000000086131ec000000000000058c00000000000001f4004800f1817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa388107f85642fedecfcb7ef3cea6b38e26aead2759263d123692fd0c7d458fbc188881580b52cf6cdb449abc060bce9db3f35eebec2c87b316fd7a2c3193296d4a6e2e81fd2e032dd33ba9fa974246fccfc3ab5ccc1eb9ede9c900fc4e5d455c42150b3a8113237ef34839fc8055cbb63c0b9892defe6554c9dc70813681c2f3f35dedc13300000000c08080000000000000000000000010de8000000000000000000000000876dccc00125f6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a915808000000015b7aa82000000000011001004a9c0705b42fa733f200639ed816601f0f413b9f29616f9c23f02f0dfc1df7e0023a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a957009781000000000080df6d0ab2880e92007f31c38dc0a8460c95a4c15291a24c92af1bca9264d8a91580800000003df2714000811e8200000000000b000a11e33689533b2499781f9b2f9c1893add8c57c4e02002418228110806567eeed21d6e87a045f3debf5ca10a05339c25f021400d96f7b492a957a94d301101df62a1c365062cce3b9a3ef530a4cd37eae4346491ed37e050dfd54213ef54f00a3982201103484562bcbb22ae3d892808bb04ff25f309a4732474a57bf6dd7691e17fc3df6811006854f362bcec61184d4721dfd30f777485be3580cd116564d139668bb6ee3a280a3a910810115b6a5e11518dc1598fbc803018f3d87edac78de6e5544ff868bacb803485c10817ce5a3cfc79898018c8d4489a393b1a46f6890a79a56737f867bfa56c511aa38a9573b51e910000000000000000000000000000010de800000000876dccc000000000000000025f43cb5a86018845618974486d0cb225355649b88690a914ee6cfec1a75286481d9daff2fcb7edb8d99f8fab9f77ff1033d5fc33a42ea7f8b70d29614b6d3195d000000000000000000000000000000000000000000000000000000000000409799acc6bbef72e7bc3dbed7831ac4e4ecfdad169d19628429edaa495d3c79ef00092fb68559440749003f98e1c6e05423064ad260a948d12649578de549326c548ac0400000000adbd54100000000000880080254e0382da17d399f90031cf6c0b300f87a09dcf94b0b7ce11f81786fe0efbf0011d48840808adb52f08a8c6e0acc7de40180c79ec3f6d63c6f372aa27fc345d65c01a42e0840be72d1e7e3cc4c00c646a244d1c9d8d237b44853cd2b39bfc33dfd2b6288d51c54ab800017db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a4557db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002f5228f4f2864a1b5a68dcfadb7680e7683333c2ed8cc30f7db42aca203a4801fcc70e3702a118325693054a4689324abc6f2a499362a454002c0028c95c468f1c569d43c9af6d335b8820bdda4888fe000200000"), + TestCase(PublicKey(hex"03d57bc818cee6523ac976e32c532baffa68028e165ca74640663bf2ac4256182e"), PublicKey(hex"03efea3bb157f8b21fd6cad3adecad654c40be673d852fcf9204737e8154865d3a"), Transaction.read("02000000013516ef197341a489906b597db7f886e262ebb31c5a349062d5f36ea3e98ead0d000000000076f1bf80016194980000000000160014c5ac89310fe31623ef8714fae74b0c414477b48d19ee3620"), hex"00000503933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000454699830eddf332655e0ff86e6c536f30000000000000222000000003b9aca0000000000000186a000000000000000010090001e000bd48a482e9d4f3df4621c059bdf7df20f5ec9c4df26364380000000c101bf0694efa44b50d2817c3ac271bd38ad5110d72162deb6328cc919097ed7727f8000000000000111000000001dcd6500000000000000c35000000000000000008048000f01f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d01125974b8fc60d0122160b22db09edf15265c9f8d42c407a4b6e286445ce2897e01c8dfdd96184b8a8a0faa4d5d92acea243bb257e2bf911767efbf3e0fee216d3b81f4207be48f4ebbc397fb0bf3d223696ce431919a583ad69a7f3825323977b38a811b7e73e5b685f78f56e8d250e68d99452deed356a826fb5f207d314d0b61b97780000000c50000000000000000000000000001770000000000000000000000012a05f20000121a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c75686800000000015c04b4c000000000011001029e7f0fe79b14835816eb3cb89338bf442f040ee84ea6fdfb401ac2aac96919a0023a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570097810000000000809a8b778cb9a0d244c835acbedbfc43713175d98e2d1a48316af9b751f4c7568680000000003b78dfc000b0ca4c00000000000b000a62d6449887f18b11f7c38a7d73a58620a23bda4682002418228110805b5b0d8c18572b99a6496d58eefe9bc20bcf359d91cc480c022591ba7c86d2280110096244c4df48199bd71b654c29ac49aa2a1dfcb6f2554a89b7d38bd28de98f6200a39822011013d445ecdc92ced0ebe8920580d3e5e7a1cdfd19fd39a5e57495a80e8a7cc0630110163a3f38ba3f23acc687ac397fb98d8dedb9a07a4085dec28d5a792e9ea2aece00a3a91081eabde40c6773291d64bb71962995d7fd3401470b2e53a320331df956212b0c171081f7f51dd8abfc590feb6569d6f656b2a6205f339ec297e7c90239bf40aa432e9d29570cf71b1000000000000000000000000000000177000000012a05f200000000000000000054e507323b7ab08f00c834e2ba694233f9d50293312291fc893691e512805f45814c861ee65261b9b7bdaaaebd59d316e5bf00417c2c9afeb5db143096feff164480000000000000000000000000000000000000000000000000000000000040b625f1f1e5239ce80103faf7851d4adbfbed7cf5bace51b959d5d6050f88e65d40090d45bbc65cd06922641ad65f6dfe21b898baecc7168d2418b57cdba8fa63ab4340000000000ae025a6000000000008800814f3f87f3cd8a41ac0b759e5c499c5fa21782077427537efda00d615564b48cd0011d48840f55ef20633b9948eb25db8cb14caebfe9a00a3859729d190198efcab1095860b8840fbfa8eec55fe2c87f5b2b4eb7b2b5953102f99cf614bf3e4811cdfa05521974e94ab800006a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522920ba753cf7d18870166f7df7c83d7b27137c98d90e6a2dde32e683491320d6b2fb6ff10dc4c5d76638b46920c5abe6dd47d31d5a1a002f522994760b5d8d708a41fe54bdd87888baf41f10e7bd0e000200000"), + TestCase(PublicKey(hex"0297d3589bbeddeb079bcd9495d52cb47ea20515f962501f0de15a8f4a66293a11"), PublicKey(hex"0242199d21cc7a2fac5c4168b6c3635bb0cc63ed4b920651833f8395f010bcc01c"), Transaction.read("020000000167189c04a660a840a18470f6d3d76c9d3910822776208bd32d5ce7c56360148b00000000007ddead8002f6b10000000000002200200f077d4a0e598f677c28eb4b51af88be2d2f17791722926f912c6349cfe1afc76ae297000000000016001419267eae62331d769c1fb4b6f742618389a5f90e36587d20"), hex"00000603933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004004c3bfd866d20986aa90ec265fdfcbe0000000000000222000000012a05f20000000000000186a000000000000000010090001e000bd48a3b48ca7482f0442970651ba40bf45091fddef3a84380000000c501a0528fcd6c721246634deb024236eb2f4c124e13b587093a6652e7bfc12db3250000000000000111000000009502f900000000000000c350000000000000000083f0000f01210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e0110d8d9aa7be8b4553b0af05b921c72c48356c4a0af61ffed7f1eadb7c5071c64816bf131172fe61d3145d39511755ee8095927534b76c4bb2464b706eaaee1486e01dd6caa8417804e26ce56f83a2525aa05fba25d3bab6db2cae5a203ce126da1ec01c8dfca1943df0dd0d59ffda66e207316c4af1cd3a4b6443fc5298a7a3864658f00000001404500000000000000001400000000017700000000015b95aa0000000128aa5c560012338c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a45800000000015c04b4c00000000001100104c97a415e599a3817965ff9a64e87d2af9bbcd77cae148ea934f9488216ffee10023a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a95700ad01000000000080b38c4e025330542050c2387b69ebb64e9c884113bb1045e996ae73e2b1b00a4580000000003eef56c0017b588000000000001100100783bea5072cc7b3be1475a5a8d7c45f16978bbc8b914937c89631a4e7f0d7e3b5714b80000000000b000a0c933f5731198ebb4e0fda5b7ba130c1c4d2fc870200239822011032e32cd21310cf1864c862bb32b9c78b1b8a626d1094d47d0e1938166388243f81101bbf4ba992687498419d57da8e6d700809fdf1f882425296292ff55b67ec82bd80a4182281108054815d830690f798ccd75722576882f62a088d37e81cde50c40bc5b080615c3e011030965e18229cbf3a03024a97988d75cb1b1569eaa1ce381d0689958161c4799900a3a91081210cce90e63d17d62e20b45b61b1add86631f6a5c90328c19fc1caf8085e600e10814be9ac4ddf6ef583cde6ca4aea965a3f51028afcb1280f86f0ad47a533149d08a9571b2c3e90000000000000000000140000000001770000000128aa5c5600000000015b95aa392494f078bec5b681d4997f2497d52112ad54eaaa81dec63f6480336139048b81fc2e019907aefb5efc5922187f575e10635611bde69ae0bd46b0b6ea44aea7748000000000000000000000000000000000000000000000000000000a000040f7dd9de11426234d0121ec1f5d2d19c3b4e78679b2e07a5c4f91bc624458352bc00919c6270129982a1028611c3db4f5db274e442089dd8822f4cb5739f158d80522c0000000000ae025a60000000000088008264bd20af2ccd1c0bcb2ffcd32743e957cdde6bbe570a47549a7ca4410b7ff708011d4884090866748731e8beb17105a2db0d8d6ec3318fb52e4819460cfe0e57c042f30070840a5f4d626efb77ac1e6f36525754b2d1fa881457e589407c37856a3d2998a4e8454ab8000800ec0003ffffffffff80106b5a98105912cee7f3cd2a4ed131283ee6fa4f5aef2e74aec08ebc2d6770ee38001e80007fffffffffec0082e868d9d8ebcc94bff771ca9d328bccfdfafdbc17e2b078b728caa0bb7179c6420001ffffffffffb0ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c02916000200e60400000002ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e000000000002029e04000000000202ce3138094cc150814308e1eda7aed93a7221044eec4117a65ab9cf8ac6c029160000000001fffffffe05ed620000000000002f5228ed2329d20bc110a5c1946e902fd14247f77bcea10f17c52e00000000002f52295b2c2f65d18b177c1a91981b0335c8d209f93ea10e08008e608804401c9f21ea6ff52325709bc8fec460de1d9f584cbf6aced6abeda5842595fbb968044076a3a4b918db66a59ee47c4d3a2f8bb0f17665e6d904df5cab74ee06cbafbb3e028e60880440b6af6af4787b1229039a7351f7156d0cc30755a1902c6d483ea85915a29b40f6044023ea5be6088489f6dda062498c485465c7451d7bf49eb46dae206bf952ddc860028ea4420484333a4398f45f58b882d16d86c6b76198c7da97240ca3067f072be02179803842052fa6b1377dbbd60f379b292baa5968fd440a2bf2c4a03e1bc2b51e94cc527422a55c0000000000000"), + TestCase(PublicKey(hex"03455b390d9fbc70d538f3e38abaf1a217e66b45c87ff025f3be7878697a84796a"), PublicKey(hex"03c6b8a41701c6abdc9951432c94873cfac9fa5c3bbad8215e28012c8e212748d8"), Transaction.read("0200000001ba658f3a2f2275bb727cd8f72a57d185f26e310cf83c72131b54970bf8ad44d90000000000763e9780015c070100000000001600142580921e745a308426c578f417354e60482120662e085e20"), hex"000008010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340004515822333393159ec64e3694540844cd000000000000022200000004a817c80000000000000003e8000000000000000102d0001e000bd48a04a8223d31a871ecced64af1c4690532d4b5de984380000000c5012e53ad31acde84a88f55e981f2c33e8d6b451e15463d3da9335dbe9e30a474c60000000000000111000000009502f90000000000000001f400000000000000008168000f01e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c0126a65afe85868caba0a468ae6adf48b02e3dbd527a7d6e86c8b6426af06a5cce81ece00b5452805039314df65a98f5eeb21eab3d917853cf49567c28392e094806012f5f8f9177f320504b51ee8112a62fb5c505b8d9167737731545d99141d3485501004c1a5ece2ff034dcc63ec008df0ec8185934a2fcaf3075a75a5a999989d96580000000c50080000000000000000000000057e400000000000000000000000002faf08000125d32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c800000000015d04300800000000011001006333ad899044f5d624983aa15a497eb65df4372319091299696efb7f3d7b4cd8023a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c2957009781000000000080dd32c79d17913addb93e6c7b952be8c2f93718867c1e39098daa4b85fc56a26c80000000003b1f4bc000ae038080000000000b000a12c0490f3a2d18421362bc7a0b9aa7302410903302002418228110807b9455b3bbed8d0994cf1fd0d8fcd2e63a03710f470943da74ef0f4513fc6f43811009275b2d4020f8bfc179ae4dd5cb3935c30767f48ee723cca0d708184b01f2c400a3982201102f192e0d772fc5b08cba1e7b1c39dbed4f64439569c493af18a9539818a6f36c0110272c9b5f9058e69485ca53388311c00ff69b5631334ed4e002cef0958389e42880a3a91081a2ad9c86cfde386a9c79f1c55d78d10bf335a2e43ff812f9df3c3c34bd423cb51081e35c520b80e355ee4ca8a1964a439e7d64fd2e1ddd6c10af140096471093a46c295717042f10000000000000000000000000000057e40000000002faf080000000000000000046ec68ce2b1bca12747a13122270527bdad325cd34478d112adcc4bd0fc25f1e814f2919e34f1842677f99901e2aaf7f0abfafd39e6350238b52b8c2bf3b00ff7f00000000000000000000000000000000000000000000000000000000000040f5ca8901722651214939b99ca3c79307f3380af44ae1aecd4d317784fda7a15d80092e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513640000000000ae82180400000000008800803199d6c4c8227aeb124c1d50ad24bf5b2efa1b918c84894cb4b77dbf9ebda66c011d48840d156ce4367ef1c354e3cf8e2aebc6885f99ad1721ffc097cef9e1e1a5ea11e5a8840f1ae2905c071aaf7265450cb2521cf3eb27e970eeeb608578a004b238849d23614ab8000174cb1e745e44eb76e4f9b1ee54afa30be4dc6219f078e42636a92e17f15a89b2000000005d353b426e9963ce8bc89d6edc9f363dca95f4617c9b8c433e0f1c84c6d525c2fe2b513660192a0c75d44f2a6f4e1bc66fddf10b2949b9896cbe8548edeba49c0dd8300d82f42ef7330fb4601c7f2e1af3be67e3c3e86bb4c93a3bd9085c53732111c42dc"), + TestCase(PublicKey(hex"03d4c8565cb50638dc9a36d147e271aa4b052af498adb423ed1402922c1e9e491a"), PublicKey(hex"02a4adefc639aff3bcc872187529c75bea3a199857a002645e99991a942647f6fd"), Transaction.read("0200000001571efe1ac29e368873c71b8a2c20becc0c72651d42c4c91b548454226d3fe9860000000000c4bd9c8002591601000000000016001446909225cd021811dd6a5bf8cf9dcf43b2643c35783101000000000022002051e961c962f6307778e865204a0d47599db648b3b5e628db1c163591a54964efcc7a6220"), hex"000009010000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000411bde5f686930a7085566a22fd7fac49000000000000022200000004a817c80000000000000005dc000000000000000102d0001e000bd48a142af4dfb86cd991ad356e17ef245bd58413568ec3800101000000c5017ebac21a9e7d42c23db38b88b689f1efb9ad52d0e728e4553493dcde20dff8f70000000000000111000000009502f90000000000000002ee000000000000000083f0000f015256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e81290a232a7ee5a18660742d9368628ddb929322bf8e55695aa3af19002b1fea2e810acfc782fa2344e096e930b4850396e2e004d875727f0839975bd3d79e3419eb81cc09de2631507630dd3788182d4e3e2c16c246fa0277f68efb26f195d11d6ce801a2b78a195c345f107a9dbd8d4927ca9c95b68c9c7431bd64132f1d5012d9c8dd8000000140450000000000000000030000000001770000000002549e60000000000223ca6000122b8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c3000000000015f824810000000000110010682606ad0a329b07977b91626c3018d8c82cf084a7d4ef1f800efa5cd36985e90023a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d295700ad01000000000080ab8f7f0d614f1b4439e38dc516105f660639328ea162648daa422a11369ff4c30000000000625ece40012c8b0080000000000b000a23484912e6810c08eeb52dfc67cee7a1d9321e1abc1880800000000011001028f4b0e4b17b183bbc7432902506a3accedb2459daf3146d8e0b1ac8d2a4b27782002418228110806873db49b74aa163df0331441bcc8cae3935262ce6accc9ce7e70dec462535c4811036156baddb0c8598cacfd150a6f7f3893d53b784ea0e188bd2400f0d68abf5ff00a398220110054bfe99fb9bd42af0827b4092a86f8c113ffecd69a7e7ac15d057da965d20ea01100c244a344b23872542da30b81f0dadb9790f4370e224de0b75ad606d79f76daf00a3a910815256f7e31cd7f9de64390c3a94e3adf51d0ccc2bd001322f4ccc8d4a1323fb7e9081ea642b2e5a831c6e4d1b68a3f138d52582957a4c56da11f68a0149160f4f248d2957663d311000000000000000000003000000000177000000000223ca600000000002549e602184aefb8063b9f14a8ef4be8ba84c09107359d62c994df78cc3b07e4c2abd7d018c4e716e3df369d6d8b17aed7197e453ac4ebb7a99b55e101cd2347794edc471000000000000000000000000000000000000000000000000000000010000409d47ae68afd388ad7bf011aac01308facdcab44edb8dc6f39409b69c4837f76d400915c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa6180000000000afc124080000000000880083413035685194d83cbbdc8b136180c6c6416784253ea778fc0077d2e69b4c2f48011d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694ab8000800f80003fffffffffff0020f2d63ebf3cd801d8cd1d3e6be1841a0e1027cff13d633b8efc7362951ad8c040003f0000fffffffffffa00407685484eaf45d16eae74cfb9a045e3e475f69d6362cda0f327659264a2db94d30000fffffffffffa571efe1ac29e368873c71b8a2c20becc0c72651d42c4c91b548454226d3fe986000000002eb1182c8000000040568080000000004055c7bf86b0a78da21cf1c6e28b082fb3031c994750b13246d52115089b4ffa618000000000312f672000964580400000000005800511a4248973408604775a96fe33e773d0ec990f0d5e0c404000000000088008147a587258bd8c1dde3a1948128351d6676d922ced798a36c7058d646952593bc100120c114088403439eda4dba550b1ef8198a20de646571c9a93167356664e73f386f623129ae240881b0ab5d6ed8642cc6567e8a8537bf9c49ea9dbc275070c45e9200786b455faff8051cc11008802a5ff4cfdcdea1578413da0495437c6089fff66b4d3f3d60ae82bed4b2e907500880612251a2591c392a16d185c0f86d6dcbc87a1b871126f05bad6b036bcfbb6d78051d48840a92b7bf18e6bfcef321c861d4a71d6fa8e866615e8009917a66646a50991fdbf4840f53215972d418e37268db451f89c6a92c14abd262b6d08fb4500a48b07a7924694abb31e9888201dc04000000000202a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a80200000001c00e00000216600200000000002f522850abd37ee1b36646b4d5b85fbc916f56104d5a3b0e0690608a04420138a23062b387099c97d9458403dd33953e565d054b209e98684f4bd602cca268044076e7132bc91195ff251682cdaf978e6e7998f065ef6d4384c0ae7176888774ec02009ac64206e1126632e2d08e50f47925f11f8b04aba20cad7a65a25d0a32e2931d17945424ce05c00f64ea4206a88d1fa87c84e0c5228a2a5b045dccb605a01117740b58a7384f5d66528d7062d1580000000000000000000000040048ae3dfc35853c6d10e78e371458417d9818e4ca3a85899236a908a844da7fd30c00000001a920ea2ec3e76f7b403ba673b35954861170448627cbe7ecaf7c00ece94988a40048a58948e8ec017cafede7ca2686457010875559b273a63b417b6fe6c22eeb21a8020000009d78c152837378b3cbbc9b373ffbf698792e9046ee8f942855f69cd7808891e000000"), + TestCase(PublicKey(hex"03d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c46"), PublicKey(hex"028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb26572"), Transaction.read("02000000010098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f00000000005141888002b42d000000000000220020143e4a3411ea01f087e0904a2ae0f4a312355368091aa08acf2279f9b6b72711eda40100000000001600149be0e57e4e3561b30fb11014bc3ed2036855381ce93b9820"), hex"0100220000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400092dd581059d2bb431f96f6c199e59a9cdf4ddaea58dca8229725660804592523a80000000000000000000022200000004a817c80000000000000004b0000000000000000102d0001e0016001411e6e9810bf2d21b329be20c89fa19556bdd933c00000003080a8a037245a5d1fc8a4d0cf1bf882c121732b9255a327efba1c1c63e10f6ff481199f30000000000000222000000012a05f20000000000000004b0000000000000000107e0001e028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb26572021021b7b90467b6d39f86b9160d4801dc1e92eedbf5303ffc419fc013bd24a91d02766d36e2bc109b08ec5b0850c53a59372cd306cc1f95cefc4ceb24a6d7886e1b03a9ca87c8642c6770627ee356f92ceb075fa23aed68a6b952b50455ba939c9d85038ffc2170ef33aca1f4741347f1de06cda9f60cd2d5121e6bc8892953a38bb00e000000028a8a0000000000000000080000000002ee0000000000b2872000000000067486e0240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aefd015a020000000001010098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f00000000005141888002b42d000000000000220020143e4a3411ea01f087e0904a2ae0f4a312355368091aa08acf2279f9b6b72711eda40100000000001600149be0e57e4e3561b30fb11014bc3ed2036855381c04004730440220068466106db388aabc63e77d137b815649614d0ac2530589ab4c5e05d78afe3f022064c9ee1be2b7b794b6645c7b5ec8f0e69e727980dc7219fe1715e9803178a98201483045022100e8e5ad22d38e27f56ad53e43e34656026a74ae3527ceb06752f9114483b057d702205978489fb86927818e0b71deeed44c8c5827237f40f92483b36688cc48171bb101475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652aee93b9820000000000000000000080000000002ee00000000067486e00000000000b28720ed5846e908bbaaa6b2f994f3c03f8f96350d2825cd6849104207a8ee263849b302bcc40fecd594b6fa15d22cd5e9a8df070ce97e8a1e90b4fdd8a1ef0ddb4c0d97000000000000000000000000000000000000000000000000000000010000ff036fd9558ffbcee019936ae3251b1696cf0ec92a58a73dcc6e0c33ea64738242e4240098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f000000002bc0d40100000000002200205be011cd1ed87926bd56a773c1b1907fe457ba0bcc876dc61f32c1ca85470e49475221028f1b5095a886ec9efca0b014aa794c434f072fc8a435d2e98fe3db571fb265722103d5ddbf3cf1c396ad4be4c715759d8389a1f0b9bf1be1344c1258ec9bd79d2c4652ae0001003d0000fffffffffff80100a03da9ba67d38e18a51216c7d501d200aa3127445c959dd972193d3dd5951e7c0003ffffffffffe00098a98f1d736e4650bea1303e122e5e8c80005950b33590856332a2b1bea51f1bffef0000420000ff0088d2195367160d41c7c2bf78ce8996e62b632adfb829c8382955b1e8a5c27008dd7e9d91440a520b59c49af0c1a5a5e1768e5eab47556a81c6248278acc7f5757643497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001bffef00004200005f644c69010100900000000000000001000003e8000000640000000007270e000000"), + TestCase(PublicKey(hex"029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff2"), PublicKey(hex"02b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c23"), Transaction.read("020000000112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3a375b220"), hex"0100250000000003933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134000425ea3f2629ba65b6849f78829099a1d10000000000000222000000003b9aca000000000000027efb00000000000000010090001e0017a914d73900cac21afc97ce994c27af395e7f4d681bcb870000000182030540ba863ec0edc426981ffa8eace0be4e9a03295b3a0ff6ca545d21983e3b6b0000000000000222000000003b9aca000000000000027efb00000000000000010090001e02b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2303ac60bfe54dbbd771b9d4c92dd33b584d91cda6f4da296aa10f009f043743aa90034980f41ef05b9248f6793f77666534685df92b54dcb4ee1030f5dbef2e88a4e102177e1b10f681a1a46221c0f87aaa8e2a49d8257924d229faf8525e3f447374e5023c6191ef9b284cc6aa0c88b3913d91bf1bde6c5ec6f9d6f8dafa6bb65c2dcbbd000000010a01000000000000011f00000000251c000000000000000000000003cf01bee02412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aefd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000011f00000000251c00000003cf01bee00000000000000000a6b6d346b134eeb64ed57d0b125934490af31cbb650813ae6454ab83d12042550331d262498b6ff23a23ac5a3e834afddf752bf1b5e81259233316a9ec72f5562f000000000000000000000000000000000000000000000000000000000000ff02db56e0e64c827f44aade7b016a37e6a7c0780db3666f7d8e8b422b6b02474b362412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c000000002b0c9af90000000000220020b2ad00d661059d790dea8056dfd8a962a53023a3603fda636c20eb8b4e434ecb475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352ae0006003f0000fffffffffee20041f7ae2006d76bc11dc18f8457cb4f76df375a85312315edbcfe508aca1596bcf4007a0001fffffffffdd002022e11a1cea6e7a8d662f74967d072ac654a72b59ada363c0e363ec4d26f8ef6800380000ffffffffff002065215a990d4af376ceb7647998ceea281d44fba94ec18c3a505f595fae0efe79003c0000fffffffffef00209a10496ebab959fe95ddfbe43f26cc9566bdc3a9cebc509e098367a2916b5e86003e0000fffffffffee40083551db0a0be081e7788bf7b13023ab3f306657022a9623a2179ca37c92f2e0d3801000003fffffffffb84008210ec1faedb71958c276e8e1502e7e1e588d4406cc9986157958e9d84f02a667e0001fffffffffdc212f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c00000000005f47acf200000000fffd01300200000000010112f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000000e3400480012e7ff90000000000160014c6a763425c341e0ba7d0c12ed9126a661e38aac3040048304502210097df6e1d55051150897b7196be2944239dc1d820ae08ba72cffdd2822d57da320220403951e2355f52e385a34665156402cb2e85e6133b3d2606e37947809ccf4888014830450221008700f11d141bad2fc7eb66163a8f621685420b75bfb9f2c49c7227bf8fdb2ef702202f4a8485c71dad72263bcae61d6e17f21fbbe24331c3119deda2eaa1420cd74e01475221029eb94e19945896a4cbe830d925272d3edb4426096b53cd88012edd1cc0518ff22102b8f1b6e1830cfc069650ea1d7571f7fffc591dd80ba0e2280a6643677de52c2352aea375b2200000000000000000012412f1713bd4417815932d254d0e2e1ddc9292f81c41c90de45e6ae6301934c73c0000000083b5c6292f61ad3cba1e702abb6d4ec1aec503c5b1b393e09c942ad4448a1b930000000000"), + TestCase(PublicKey(hex"03f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba"), PublicKey(hex"026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c27316"), Transaction.read("02000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d00000000008dc3288002ff7b0600000000002200206444bace1cb90460914f5c964acfdd7846104e93a205d0bd5d00eb73f03acced21cd4500000000001600148636658ef4d15c8d9824bfea9b70ddc592b24bb438497c20"), hex"0200050000000103933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b1340009e9907c0bc4f4e5b55a3b7012180e63b0ebf76498c543b991c056f0930a6ac09d80000000000000000000022200000004a817c800000000000000c350000000000000000102d0001e0016001495cde5680fa7aaa16935b74cc11b4ee695aa6b1500000004080aa98202cc1bef9c46bae2f3a965d7bf4bfca249098c5847b71d700ca0eba00db523986a0000000000000222000000012a05f200000000000000c350000000000000000107e0001e026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c2731603d49ddadc0517b1bb1f9c2773621e62b7720b2a622e4a5548becc37164bcd70f10368718d6b2887b302f75e5af0bac2f3b883663bde0fd61344ce16ed9a31c55eda03530674aa8610a599d0cd8be226923b5d8fe2a422cfbdc4fb790e39c7b05e0b7e0246ce4c46c2ee4110cce17bae3c7e6de01a8f0f8c1970cf58b2122b8fc77ffdeb000000028a8a0000000000000001aa0000000002ee0000000019545f970000000110b19269240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd015a020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d00000000008dc3288002ff7b0600000000002200206444bace1cb90460914f5c964acfdd7846104e93a205d0bd5d00eb73f03acced21cd4500000000001600148636658ef4d15c8d9824bfea9b70ddc592b24bb404004730440220137ad66cce66df0041e172036c0c6de1e1cc8981130883bf693960778a2160fa022039d9505c98f78f54aa30172f4deccae26f9c4a7b55e2b72d4bd8875809143d5501483045022100ced9a63de710f1339c5d9086b1cbe0097f942872fbf1d0ca3a643409933cc86802201d5e311f655f64fb64eae62c9a3e321329158d055ecce9c537657979c17fe62501475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae38497c20000000000000000001aa0000000002ee0000000110b192690000000019545f97359430d3b112dd6d24edb449958073132a7721c737f2a72edf12ff74606d6c6f022030518cd036759a25ae66be2bbd46855ba6cdd214742f42aecd72b9093a444a000000000000000000000000000000000000000000000000000000d10000ff0200c2341c9b6d1ba8955188aa913cbf5b303c65ad19491df7d15fa0a811c44d38240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae0005003d0000fffffffffe580103151b3b767e35873dd3babc0f5bc9ea4a99069c269456b9e1157dda87db454b7801f80007fffffffff2b0020b4a390fde1b8bf038232ea79e48b825b5d1e930a97f2532aacbe0ddfaf24e9be00380000ffffffffff002038da5bbdce565d1eea32b7b9f4c4932e63892a154197054f5eb0116aab0f41de003b0000fffffffffe60041298f04989916fa9ac7b83da8a340a8bc6fcad73925c190576d728b28dfcf8de400720001fffffffffd00204225a2a872ea98ff1b22991635a2d6cb598839bd8a3988eeeb53adf784149fa880007fffffffff2b000e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000001e2d5a0004240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15eecd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15c4cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15bacd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae7202000000010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c8700000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b150001240e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d000000002b404b4c0000000000220020c670f08ef02bd0e9226f315eb2e2d193d2946d4aa3d03e612a0bdda231cfe9c4475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52aefd014f020000000001010e985f9ec14d882a73f50843ad0fbcf37c644100b13c7eb3d91da703246a669d0000000000ffffffff02ff7b06000000000016001495cde5680fa7aaa16935b74cc11b4ee695aa6b15b8cd45000000000017a9142a90566bc7c0515d50552fb8bdf9e7f5c541637c87040047304402203167bba6344d2d82aa11db437dcfd17903de22079f58597f8927813cb96c32c3022036ead9938307bcc82561f85cebd8123b273ca26e9aeafc6e0502e97a38b508da01483045022100e0cffda9bd2bd045b99024d99597698a25803d42cd22c8b58d7a9e29363e79e802202a0e27d553bce59bb7b4e2cd89b847ade01d39bcb1027e0a7c0b6e0d4fff096601475221026345c7976588f6d96e7438f8df34e3225d527e9da5e201dbf83b43a1f4c273162103f4b51b541e6a9d935e83828bedbfcfb0ce404c809712376e672b624bb250beba52ae00000000ff000000000000000000067bff16001495cde5680fa7aaa16935b74cc11b4ee695aa6b15000000000000"), ) testCases.foreach { testCase => @@ -241,13 +236,12 @@ class ChannelCodecsSpec extends AnyFunSuite { // make sure that round-trip yields the same data val newnormal = channelDataCodec.decode(newbin.bits).require.value.asInstanceOf[ChannelDataWithCommitments] assert(newnormal == oldnormal) - // make sure that we have stripped sigs from the transactions - assert(newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.forall(_.witness.stack.isEmpty)) - assert(newnormal.commitments.latest.localCommit.htlcTxsAndRemoteSigs.forall(_.htlcTx.tx.txIn.forall(_.witness.stack.isEmpty))) // make sure that we have extracted the remote sig of the local tx - val remoteSig = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.remoteSig - val commitTx = newnormal.commitments.latest.localCommit.commitTxAndRemoteSig.commitTx - assert(commitTx.checkRemoteSig(testCase.localFundingPublicKey, testCase.remoteFundingPublicKey, remoteSig.asInstanceOf[ChannelSpendSignature.IndividualSignature])) + val remoteSig = newnormal.commitments.latest.localCommit.remoteSig.asInstanceOf[ChannelSpendSignature.IndividualSignature] + val commitTxId = newnormal.commitments.latest.localCommit.txId + assert(testCase.commitTx.txid == commitTxId) + val commitTx = CommitTx(newnormal.commitments.latest.localCommit.input, testCase.commitTx) + assert(commitTx.checkRemoteSig(testCase.localFundingPublicKey, testCase.remoteFundingPublicKey, remoteSig)) } } @@ -320,17 +314,7 @@ object ChannelCodecsSpec { val remoteFundingPubKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)) :+ 1.toByte).publicKey val commitmentInput = Funding.makeFundingInputInfo(fundingTx.txid, 0, fundingAmount, localFundingPubKey, remoteFundingPubKey, DefaultCommitmentFormat) val remoteSig = ByteVector64(hex"2148d2d4aac8c793eb82d31bcf22d4db707b9fd7eee1b89b4b1444c9e19ab7172bab8c3d997d29163fa0cb255c75afb8ade13617ad1350c1515e9be4a222a04d") - val commitTx = Transaction( - version = 2, - txIn = TxIn( - outPoint = commitmentInput.outPoint, - signatureScript = ByteVector.empty, - sequence = 0, - witness = Scripts.witness2of2(randomBytes64(), remoteSig, localFundingPubKey, remoteFundingPubKey)) :: Nil, - txOut = Nil, - lockTime = 0 - ) - val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), CommitTxAndRemoteSig(CommitTx(commitmentInput, commitTx), IndividualSignature(remoteSig)), Nil) + val localCommit = LocalCommit(0, CommitmentSpec(htlcs.toSet, FeeratePerKw(1500 sat), 50000000 msat, 70000000 msat), TxId.fromValidHex("2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"), commitmentInput, IndividualSignature(remoteSig), Nil) val remoteCommit = RemoteCommit(0, CommitmentSpec(htlcs.map(_.opposite).toSet, FeeratePerKw(1500 sat), 50000 msat, 700000 msat), TxId.fromValidHex("0303030303030303030303030303030303030303030303030303030303030303"), PrivateKey(ByteVector.fill(32)(4)).publicKey) val channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes) val channelFlags = ChannelFlags(announceChannel = true) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala index 3aa76a4714..52f4fea035 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2Spec.scala @@ -1,9 +1,9 @@ package fr.acinq.eclair.wire.internal.channel.version2 -import fr.acinq.bitcoin.scalacompat.{ByteVector64, OutPoint, Transaction} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Transaction} import fr.acinq.eclair.Features import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.channel.{ChannelConfig, ChannelDataWithCommitments, ChannelFeatures, HtlcTxAndRemoteSig} +import fr.acinq.eclair.channel.{ChannelConfig, ChannelDataWithCommitments, ChannelFeatures} import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.channelDataCodec import org.scalatest.funsuite.AnyFunSuite @@ -24,17 +24,6 @@ class ChannelCodecs2Spec extends AnyFunSuite { assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require == map) } - test("remove signatures from commitment txs") { - val commitments = channelDataCodec.decode(dataNormal.bits).require.value.asInstanceOf[ChannelDataWithCommitments].commitments.latest - commitments.localCommit.commitTxAndRemoteSig.commitTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - assert(commitments.localCommit.htlcTxsAndRemoteSigs.nonEmpty) - commitments.localCommit.htlcTxsAndRemoteSigs.foreach { - case HtlcTxAndRemoteSig(htlcTx, remoteSig) => - assert(remoteSig !== ByteVector64.Zeroes) - htlcTx.tx.txIn.foreach(txIn => assert(txIn.witness.isNull)) - } - } - test("split channel version into channel config and channel features") { { // Standard channel 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 7e1dce9ee1..c6dab1b41d 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 @@ -2,7 +2,7 @@ package fr.acinq.eclair.wire.internal.channel.version4 import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey -import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Script, TxId, TxOut} import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} import fr.acinq.eclair.TestUtils.randomTxId @@ -132,15 +132,11 @@ class ChannelCodecs4Spec extends AnyFunSuite { localOutputs = Nil, remoteOutputs = Nil, lockTime = 0 ) - val commitTx = CommitTx( - fundingInput, - Transaction(2, Seq(TxIn(fundingInput.outPoint, Nil, 0)), Seq(TxOut(150_000 sat, Script.pay2wpkh(randomKey().publicKey))), 0), - ) val waitingForSigs = InteractiveTxSigningSession.WaitingForSigs( InteractiveTxParams(channelId, isInitiator = true, 100_000 sat, 75_000 sat, None, randomKey().publicKey, Nil, 0, 330 sat, FeeratePerKw(500 sat), RequireConfirmedInputs(forLocal = false, forRemote = false)), fundingTxIndex = 0, PartiallySignedSharedTransaction(fundingTx, TxSignatures(channelId, randomTxId(), Nil)), - Left(UnsignedLocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 100_000_000 msat, 75_000_000 msat), commitTx, Nil)), + Left(UnsignedLocalCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 100_000_000 msat, 75_000_000 msat), randomTxId(), fundingInput)), RemoteCommit(0, CommitmentSpec(Set.empty, FeeratePerKw(1000 sat), 75_000_000 msat, 100_000_000 msat), randomTxId(), randomKey().publicKey), Some(LiquidityAds.PurchaseBasicInfo(isBuyer = true, 100_000 sat, LiquidityAds.Fees(1000 sat, 500 sat))), ) @@ -196,6 +192,44 @@ class ChannelCodecs4Spec extends AnyFunSuite { assert(fundingTxStatusCodec.decode(fundingTxStatusCodec.encode(dualFundedUnconfirmedFundingTx1).require).require.value == dualFundedUnconfirmedFundingTx1) } + test("decode dual-funded unsigned local commit") { + val bin = ByteVector.fromValidHex("00130d411da7b37693960ef0a495ffa8c0fcbf82c1fd8034ab3bf37f8a184242b7230101041000100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa0009f22f74ef88d63eb6ece1076957feb94a7cf7f20b07f3ee710a9f2ac024d3872180000001000000000000044c000000001dcd6500000000000000000000900064c0000392a15254566846b06ea693dbeedb1a6ded24b02d53ee40f091f628dc117a1a5d0000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000001808220a598202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca0000000000000003e80090001e021ef6e2ee1a5d15aba3c663f1b608ed66315fa8a855b91f53eefa8debec663b5e0392a15254566846b06ea693dbeedb1a6ded24b02d53ee40f091f628dc117a1a5d0289365d47702d068083d9444c9167be1addfe353d9466a9805215c89361aaf85903e60c20e9744d7a3ff07a72c0170569a9841ea2fb6e0dac7d90ee550a491e931b0000001408000000000000000000000000001008228a598200000262866d2d58787368382a8fc145e34c08f5fbfd54d582b582e088de5e32fd8736000000000000000000000000000000000d411da7b37693960ef0a495ffa8c0fcbf82c1fd8034ab3bf37f8a184242b723ff00000000000f4240000000000007a120000230d9b88fad260907c43f97b88bb9992bc334ef29015cae8d7a8aa5063539931d000000061a80000000000000044c000027100000000000000002000000000000000422002059106401877b19606b711f9980431fd80e830d2a30c59103d7f12e16a7e141d3000000003b9aca00000000001dcd6500000000000000000000010100000000000000005202000000014a12a0174ed233d1e65150f14ccb948a0508dbeaaa49236dce4c4fde7b69d31201000000000000000001e0c81000000000001600149e0bb5d89fbd10f3054bdde0fa3e22d42d1e7abc000000000000000000000000000101000000000000000124e3b1062400aaf012a8e25bb0932a3de9ab16a1accf7302b333f6020dfea5a22b000000002bc0270900000000002251202ee2991cf9a98cb8be01ed4ebc8279bcd84536bed6f73f4dd0012d1927d5d8250000000000010100000000000000020000000000016ec21600149e0bb5d89fbd10f3054bdde0fa3e22d42d1e7abc000101000000000000000300000000000176d82251202ee2991cf9a98cb8be01ed4ebc8279bcd84536bed6f73f4dd0012d1927d5d82500061a80af0d411da7b37693960ef0a495ffa8c0fcbf82c1fd8034ab3bf37f8a184242b723b0e1b56831c5dd8ef32c85df9896c057c36a7e8d3c95aeba8c9a4da79b0aa1430001006b0247304402203f4fdd467e2d27c89946d2851004c01988294b0071748c4acd315c90b9ab0b1902202c86d68228cf322d4b676ece89a5106d2518565cb859c436b9284d78f81b4d1201210392a15254566846b06ea693dbeedb1a6ded24b02d53ee40f091f628dc117a1a5d000000000000000000000000002710000000003b9aca00000000001dcd650024b0e1b56831c5dd8ef32c85df9896c057c36a7e8d3c95aeba8c9a4da79b0aa143020000002b60e316000000000022002059106401877b19606b711f9980431fd80e830d2a30c59103d7f12e16a7e141d3007d0200000001b0e1b56831c5dd8ef32c85df9896c057c36a7e8d3c95aeba8c9a4da79b0aa1430200000000a9d907800220a10700000000001600149e0bb5d89fbd10f3054bdde0fa3e22d42d1e7abcf8250f00000000002200203737db5c8aa01d33fd415cabd8b8680a16527c8d9c6fe32ab5cedbe610484aa187c9772000000000000000000000000000002710000000001dcd6500000000003b9aca007c6fd871c702806ae018d8609df56a1705d275b99994bb3563878d554fed62f4039fa3697fcaf99e44e3a5b601600667e00d22e940cba46708e3bf0beae9fda8e20000") + val decoded = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_WAIT_FOR_DUAL_FUNDING_SIGNED] + assert(decoded.signingSession.localCommit.isLeft) + val unsignedLocalCommit = decoded.signingSession.localCommit.left.toOption.get + assert(unsignedLocalCommit.index == 0) + assert(unsignedLocalCommit.txId == TxId.fromValidHex("ab3c0f7a2242e5656cc59db4852347b9ad4c6b54186639bf3b5698df2b129f88")) + assert(unsignedLocalCommit.input == InputInfo(OutPoint(TxId.fromValidHex("43a10a9ba74d9a8cbaae953c8d7e6ac357c09698df852cf38eddc53168b5e1b0"), 2), TxOut(1_500_000 sat, hex"002059106401877b19606b711f9980431fd80e830d2a30c59103d7f12e16a7e141d3"), ByteVector.empty)) + } + + test("decode splice unsigned local commit") { + val bin = ByteVector.fromValidHex("001801d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d230101041000100002bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e630009439468f0eab4a6375ce444fc12eaf57df47aba7e2644be598e5249349f8172218000000000000000000003e8000000003b9aca0000000000000003e80090001e000003fcfbfa87a789958625832770826a2e391b87880a5698e601033942c75173460a0000001408000000000000000000000000001008228a598202aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000000000000044c000000001dcd6500000000000000000000900064027800eedc36e641dff2b5cd5041895a8dbff6b52bf1d8fe58511fe0b2ae60c19503fcfbfa87a789958625832770826a2e391b87880a5698e601033942c75173460a03c6804903002dbb70e488fb53009ea0a024f87acafda1d96f8a0f1fd2f98b3e1302a26a217b4263be92199009db2b503e48272c0600d753a97ae34bd9827251c6700000186b02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000001808220a59820000000000000000000000000000000000000000000200000000000000020004fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000000e4e1c0342bb8ca8412a52cbaa04072e0e285ff414a4bf36be1828482705a6bd13501a200061b100003e368b6d07bc35ff7e3475feddeb313912771046d96fa86ec9a5ba2e9024f5df44c2609468b1f86640de3863c1e3ff60042667392d43809462bc3d18d454ad70de3d85e738909aaa1930559a093e363449e7a00c85cb62562298a108131f6f88635d990d4a6de980d076bfee5281d62828c19da4db7843c304c6d2fe742a8421ffb46c26b5e06249fb8fb4ff0b4d1f08d44d8d7d87953db3da6154f8d31d5af9780f6e45f4867fc7d77a1e438dd87172758779414784275dd05b88eafca27ed4424f1249c14d0894202377b2a394e3cc063badd9bd57f66122bb391fff3718520fc9dffa0b225b1eba27be4f0a386aeda91f85b126771f356afa9d1c9aeed249ac100ece76fc8b32903eaa9caeb2c0e3e6d05f69ad025d7ba625d536e6ad06d8b77aceb6d7a01dbb17adee7e17eca234b251e353e26de28849876b8cf88bd57f4a04745d6a88d32f00b35afdf416efd8f84826840e9e974cbd8aa4a102507f0c7e441ad9e1c1275ccc89266bde55f4a3582b78fa8aafa1994ef36b35d3943d3cd326bc70a3ddd377e895b498d79cbc857b5bb2e61f30ac8ca48fe186e40326fd9fef0215e2ef31844bcaec6334490a89dcd42219d64bfeacec7978294ac964b66b49958130a77f7569b083b1c280d4e451e872403454a3947d2cb10ad2f96f92b070f886a5ed771332f51889d18cc2ad941c6638eebefe5fe6cc4b79468b1e469adb08c34982783d49be95f32a774f51db6788d34f26a8af1b31fc709d78338c6c4d9834fa25a910b4b63e05aa6d383d282bba31c9378e3f27f985088e682e11a46a9a1c8e18be9b7c7ef02d9faa8bb08a53d5ffe059d48d440c6364292fb42df68a04aacdfaaba029f1460912b11a6f8eb57578832fb3c10675c7989114177bdb3b587a80856f351177d01eefbb1de210839c9b8b9ba621942f250dc6b4bbf014899b77f52e8e40cb06f7063371251c419bb8aa9d3c0bd83ce651520b6e2911dd38db6c0fff7cc64550320c5cac621765175460cc8f54ee4a0026ec8e57fd789e0fef66c4940cabc3944ff4e3fe10229294e6fcb3059adb4f9868e53b52a7da60f5ae75d4e42e6a94c44e375960f9ed44a56c0b6c49e7c5775dcc1a819547bf3f146e8bdb13f305ea0d15b5616623db4c0657059d07df7acf1062069705ae5699ce86d9d0de16f3afae97f5327fd340693f756aba0bf338af880927cd7f976705c2b511077bbd19b9a842cb7a9b2f713f183c1e4eac23bee3ac081be66de275451773444e81536e183c8e2f033766b854cc92773205abb0f39d90e762c1f0673af8e4051205f752757d2deb7dce6adf8a55297a3a56fe1265368970e8d480b8ee61fa38313e5a7069579f10ac2db37c05c58b38544a8750e186c43a153c7780cb8ddfa309fac85b487b43b1452624afbc4005bbc0777eda63c96edc94016d1fe880dcb2faab472802921adc29dd3410628b8431186a7ef3a6839eb16123ffccf40c4c0907764e40d1dabbaa12a3a9ed3f341c070fb622600b6f4595f4601f67f2d03e7221d57a20bad2e0fcde738107a57fea1e1257a89970bc94517fecfe4990c67f7bfde2ad523f0f4dc760e0e8958eb32c6a7b0e7d952e75fdf08550ab72b6824122697846e599c8a2cd7f3066c8c444e2bb41aa38cb063493af2106eeae5228247706a53169f306d6b0d74867db69d29e9a6eca2b77aeb3353fa5a593b9f4fa379ffd01464a6929841f0fe934d68c8ae9e523a87d02ce5df5636cc5dcd72536d044cedd2a1d1e9047712a399627a44c8776cc5d0b2fa7e3dbef52102602f97090afab7a6070af4a1e50d19fb646f25212a7e750d6d0f765c0d5324721afe49d1070a686b05dafd39114bc65cc11cb30cda09b872c79521273ff2f075ebc4a2e5444c6880b3854d623bf1c42be1026d8b0409fe0001a1470107fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0175abe3cc14d94456c949372ed6ace0b5b92e58c595a6297e6c1cdfbe5bd750a00061b1000025b15002f7b27451dba144086e3ccebd9726c2e1af20883b676631e13908f2a8a0dcabd2fe4d39cd5b0c9d007a4dc3f1a5624208693e461d934fcbb48428eb1cfba41682d19ab1221e11648459eee58b855ea196e854047d82869b62b2efc978f520eb13e35e3752e53026a0f7b22ddf65de7a4e3fea56fc86b9c85cb8df4cfd0262cf3bbe1e16eb25574db8fd56cee33cbfdb73073f700b9992c19ac808354db41e488aa31d6b8a3be8d151732aaf6353f30a18cd773285cddbd3b279e6455f9b90aa37d7055ccd9fdafa66ab0ad216fd123e4a4aadb25f9e004c5a8e794abced52cd6e208d8d2657d03f5179b6353133a9eaf151cf5d47298058ad85ada4e42d29b9c61524ac20264d3b6551efef60789c6a91cf1ce4e069e3ad123fba8d515d705c0e2f3aec108ab317116d593100a4db4affd32b4091a1368cfe3c91b422ff53c75ade0ed61dd562e55cd674e5a0c5ca5952c2204c401d9c61c25b565c83c38cc09eaa6e25c42d234875f2a353813b8db46f031f0392f73a9cc8a88925347e04cfb477f3a7c5578edaa447927c048aa631b4f3dafcb608fafef2e125a549e029f2598cae566f93febee55ab1175359913732673a44fd2a8e28f71e281917c7d0fc04816a5774a3411d1db8f290fe442e3d3141ab366633f07e46528d44528a422c9225129b0ae4e7f3b76c9c8fb25b9c7722be9b5862ce866980003752c178e7a94224a2b0b84fa1e2635b5e2207ade589baa96372d4453868b3e01e9ac2d1d80bc0c7e06a22c205f85e578d55e3283e9b2defb70f46a6084ea40e92b25f35550e7f3fc3c9e764dd4eea7b85df10175a0799e6a46cae937ddb1fbc43795a88850870cdd0f5bfb4506f227b1420f6e8d39b7ccbdc888a7df7066cfcd8da6ba6bceb2aa80bf14fd5d096f604af41ae92b9f4e3c6a7f29208e8b3fb70cf81fd56ff17ee56adf35cb18b39a067f67f9fc3f94e8c30d44760da2322a77679bd6ca0da0e47f206ef7b644448e7330bf19455ca777418896f0e07e551ae4995c7ac5026a7b84c0cd07baf9569135ae4cc3893339ac886ab704b37d46aa3edcfeaa6c4c8aec3ec8bf8ffb51a591ff02f04367314c267a3152070069950d384b99235e5398547d5084c4afbe86ce6a6a60964b8cd89b20c690e8de12606f64ea559a36431dc98d6189a3f15d439dcafd831a9dab64bf8dd5722c91d59fab00fad5b27a89d5f4237cb20ca55648a8256e8a0ba18394608840336d15a84451821619a2363eb51c5ee83e1722476106248962ccb5f7bd843e7dd14620a2b40107d972fa6b1e4ed7bd6b13b659b012870b64906229c93c2625b02da587d2239e09d64bec81f40c50398eb666ff8912711c2377165cbb0ad16999c0b09bc36f354cd495d63fc6cd7b89e4063ff1b63b319afa4a7a64da69aa41ba9af3131df5cb384ce68ab74898d699ea09730b53d3d862521927b0cde151844a9eb11aef8e0e25a3fb607d57c2a9f83eaf01f1c387e09be5fc5ac0eb091359ca26c8fb36cea3ccae8a63e57dcbc60ee66975bfee8964cfdfc840b114dc6ed5eac0babd26f99ec251c45ec602a44fcc6965c0d7c065c186569be6426658b8a5b32fc2dee315a0457e963635795a0d699db9587eeacf8f893699410a87b6390d8c8736552e325f5ccec1b6d4b45002b678649b234301610ecd605f9e56c5dc45d81cccf73e442b872369cbbd3a03eaba79c0af36e23ed97cf73808dc5ed0f5fcd751772b57e2666c5a70b1a5af61a0dd461e8a383e8426369ee6a4e03cf256a6bc3570997c3c06879946ffdd2924aa329e006759c0d6b564c89198c0c09f88c2c9d0c2cd4e0163e8f98145456cc859f55aa8d11a9969ae253d93d56d9fc6a386ae7c6ee884f483913bd739124f69b4bd5238fdc33d860a9cd4670de5dafe4a24fe0001a147010700fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000001312d00af8cea7a0a1be6687852d7f2249be3b445ae0cddc097a05ea468ef797567856d00061b100003e5507553c137054ae33b5d7cdaf142fbdbfae51246ea3db4441251aa49dac2e8cac063650741353682da85f0df1b1db72997177b74940bcabcb961f364c1861a493066a9cf2533aca5f866b280419fe04f9e9807d3049b759f78d7ce322554ed7891184e73d934c62f88ca33f7cd75d56617664cc674d74f553b8674093e60f014a89e1ab4ea7adde5d54f28a9976d2a3ddb20854b3a1f3ecac8490a9243234c7ce9c5df1b0e45d862c847fd93b99ed1f4d5fd5b06ca416ed048433782d4bd867d36196a7ebe1106f687d0868cd2f2cc1fd94f999de8e59e9a9b333aac6304ac7c01cb91d20c09d4e4f38397ccc1a0cbb46865b7292b84d4f11a783abb0d5da2e31fdc7c2885c6c90053f716264d0b8ba1ce53dc72e8f04e702e319029ca234b8bf72f5321382723010153eaf113e0c823914bf6ea319a1083efe2f0736e1f9b36125c4b77b9e4a581e6fecec959be70f2e21b8c9bd568641d45feb3ffdd39491abc973f310cb1f9c46a874471e5edc3ea7d4063274a9e10e2d21a9e836e99d386d062ac31bf19a14ad6d39c19752eb749c4dacc64aa4591228c039a1f3c0dfd919a2576edf70cbe018075d90c94bebbb91290487cb9cc613eef312a87025891ea6d8b02435b26abf72fea504aad333100e4f14ee916c678c6c9a42b3421bb8f7389b642f89b5abaf99f0df25e5859bee216c3845bbc9fdb032acb88cc144306cc17fe754280bccfecd86670d6994833c4847c71913caed6d904525d22d28aadf3d4e867a617f20a9cf61e95152eb1cab90bae7885da01effee11b7780bd2398d37f04096e7d14252e2026f329422c077762996f45a7c4d546d7ad0422996e659c98f1feebc5c73eeecab837c025a44d94cb5ee39d5a1bbd95973e8e5cdd97cabb7b8f4f86a5bb228c0d12be6b9b2032c54795f4f7180828e94377bf6e0f56545dfcd5252da0936d79aef70caa8715e8416579ca1a928c2aecfa1e03b6cd795528a967fb3dbe524996be92b6168222112063077fb39811ebf9dbb97abafe1b315488c8812224faef56c1eb86837ffcf61bc22a60de7e4f6e0df67c332389556df7da838200dcceeafcb6e8a94aaad013c30934ec0ada06fa17afb5414de4812deb6fa96517df57d503e1e521562ed62a65073252dd08aafba343c029e04faf58b4c451061f156669be5ef68a2adff506cba6351e407d3c6077b71807f21e93d0ca69843f2d5fd6b7c7f415d740fac1f1f8c8bd8f95297ee1ffafbe1b41c548b69dd6d211352c556f14bb4e5af76244e7656f882452201e2bfd90a87413904081a78c2d6849a6a07ad77920465141da19034b70c3edac53a560e7b5c07923c9bc6e17b8091257fe7431e7cb18d770ba1457e62714a47beb8e503874f24405c49121d2cb024edee3458b31ebe1bf40a13f564b8ff95dc537ec926311f59150544fcc57efcef53d9f43c41b0dc32f93b91d2663e9f6b09f9483b4a788b0642d667c722d10ddf09a70886153fc8c943a9d6f3eecec3ab7144669c10b3c5864a174c7343a74e4b58da29f0ae480d5be8adb5d5b55e9cc3c11da1a935c2b1cb966d85612b6c4bbd42aaaeeebada9f062175f502c2cde7daa345239f99201be98ae989cd8a3cd0610779fd413ef1c6c48cd6e8bcb454b847815016e214f1e6162df1b388503b06bad1e5e35ca424fa1ac4407757504e311f4ee0f4d8e5dce45759a96031460842697d577205255b9ba2bbbe4eb08528a9224087a40129906655170405ad4491052ba71d6dde4000b7d7154634db60db540a16cc430b5998dc1465bc57b5a4125c967e4fc451d66026b650b9a70c26998f559793809df549ca1d5e730db0759ed40d3a030c8a1c6bccb28b5fd9230c55ef83d23b369938200099274978bcc017a96bd1b506c6d28b46788f9a483f3e0b234fe0001a147010700fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0415149b9751963f87c1583f34e4fb1bc5599cb48dc36e0ab744c37fb0630f93e00061b1000033fabe908e0bb47028814d9361e2518822ec95a72c8e611b1562fb595ad452db3272b17a6f9280115fa6e17434cfc1639b3b1795ff6cfe4aaa80b3393100ba4a2308b9e42dbbb3cba55bec53bbf574b9307e2023f3e13e6d0424ce7b4376280ea0d3a187c8585212fe9effee9aa102873a48f1b71a2ae81cc97fdf6075b4ee17716bbcbcd930a23d483f04907dcf89d9d55d62ac6935ce45afe8549de48a363c595876e32e8ba33806671cb54a92a81409f9bc8964983552f709335efb962d17c2bcc348fcf9af666ae7cff45833b4b0449481a6cc869a6cc2d928ccba9499e6415a52c455ebce14cd94a36fa0bf127682e890be81a1dcc77dd2b94f886103d793ffce97430edf04f4b8a0aaf9622b34bfceaef96bd6848717430d2212f2df30105354306fe8d5d452c32e1b0f1872cb7c953295d2794805fcad0c50cebd26b14cc903fd995d4eb397d85d3bbcc92a4b640ec4448a16eaae0941f2f074ea2f292b55c8cd8051f5b43c4f0d14ae04038d06216cf01b344fb3b8b42ae2680d7998c5f24c16813dcfc521525b5bda78bf604edab7edcac3934cbb9e5e1baae044ea136613ac003e86e3caa9f0e93c38c5997a17ab472004726730f79248be824fbe894422e2b07f951d31ccc32c7f513a61c7e0a4f3790faf2dca5fc4afca1a0f14b0a7a07178402f1875430c905250a5d4d8d70521418cca1c82a2b3721831087e342d74bab5395c5099fe8710c667fb463e50f59da0b67d7f8b2a4b47896c84954a82d604a570f41fb1a5a9906592ee050b0cad8fdc652a411f8dd0d4aa5c46540479b202f7752985311671be2fca007d87b2f27f16be6a590357aeb87f9f75d9f2c798599f3e8bc2fe5b1e7b5d4e3a779761d983f52e5d4844c7eabbd4e543436954a9e361f2f099bcb603a94f2cfc1f1409aa50aee8644c5a1222e2df52fd8a1f884c0a1a800c92be47b7bed41ceff4ee44700c88e527b55c0abdd04f86ce1dae36f0c2802bc4cc74e0698092fed0e8ab7a95e1bfdf0b6fd160a7c80b0f1bba8dde89740e4caa6681f1d4c926a49ee2088282d18e3646e4ade293e1b2cb5a0155d3ee662d5c468cb5d2ac7557e7666396336e3608d84c97fb1f48dd8ca7f8528b218eeac9a825b161147849d1c2a1ee461150cf8984f492d02221ea43d994821790f5e7357e615ce721817e89495bc3422b98a7e6d6288d75c3b727d1669742b682560660079d8860ed65022d279ca414cd56e169e1147d27ad808290f7daeb665a949b793ae503c1dc9d3f1ae0e57b02d373357408d881e2354337143f7d0b99dd2adf48e6c4f4983bc1de85345892e246c6edbd9aee3747fcc2efa956e3e89e9f5a9be0401933b51d625e25c8296be8ef13720d15411d029a471387fa2c28a168f72e0802d84b3085937f9a83353e002b39596e1567d712e729aa55e648f87043be90b359d3e1fd439091d3ded3b72c0c0bdac8e3033a2b2d6e4e074094b7ff6eb631e2b62ec36e35ea434154e0189b1b1e8b9b60da771cf0077ff267e7e861045d24d0c3fa57b9408d3e2116bf75ba02b4d8cd78701ca9836074440c6c0c0ccdb2cd490ee616bc6d3f8d10ddaf60dfe77003ea326673193d21032997f8fb77c2393ce7af5f1d3d3abef5550e80c5a4e3b7ae75ae3f79950a2f8e5065e8d8fecd2f195aac557d7a4b4a5d1e0c5f1e60c0005d73df261401196c93384fcc271b0ebf0a9da2adee213319ed1fb5bec02202b4a0778bdd48420e701105a135b8bf2d4f50b6939ca0890174d7ce9852473456059b9b7a85db8e4664fa67dbc2d729080df312f3d8970e28e7304510b4828624279cf789d9deebf08cce0c1cf84ffb2abc52784230a5b488fe167632882be37fd1564c22b0dc89cf86ea5ffe6d7880b7199eecb9957b28ba3035316b763a4c0a0691bfe0001a147010700010000000000000000000000000218d33da90930d78dfda97b94815fd8543f4485d0574727143932171cc4d796ab0afd01810200000000010223c6a41f797be480b870e70fbc0b567935abc812d5b0e9bdb57e9992dbb54d770000000000000000005b11e204242c6a81fdfaf3d00b8825ca2b3eb16548c572ff9d1d8eef653d7e240000000000000000000360e316000000000022002049205995639f7357505aed54f7b392d2662276bc5d8968da78e6d623d9e6c7fc0a77010000000000160014cf85091a7295547ebd2066aecb1a295440a9f6f0906e010000000000225120a4ac7358d683a4a296a84a196286ea8bf7c80683f3ea8c30be932eeb30b4cfac0140ec5f7239b44a84754b02b947a6210e6d76ab2e57ad71a68b4080b3e68e3d3355f426455a86dcd678677d62262352258d5a170621aeca6643b8f94677667fa44102483045022100ff4ba957db4a6f149ed4513bc8cd64c9cc430f4c5810c1d01d73df72926e492e022013495ad376bd22c6683cc334542192411369201e4eaef605b59d34403c1f5de0012103fcfbfa87a789958625832770826a2e391b87880a5698e601033942c75173460a801a0600061a8000002a0000ffb0d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2344fde6f10018b4cec394cce110bac5d84cce63839bed6752d3c1498c5662b9620001006c02483045022100ff4ba957db4a6f149ed4513bc8cd64c9cc430f4c5810c1d01d73df72926e492e022013495ad376bd22c6683cc334542192411369201e4eaef605b59d34403c1f5de0012103fcfbfa87a789958625832770826a2e391b87880a5698e601033942c75173460a000200000000000000020004ff0000000000000000ff0000000000000001000000000000000000000000000000000001000027100000000027a31840000000002de544802444fde6f10018b4cec394cce110bac5d84cce63839bed6752d3c1498c5662b962000000002b60e316000000000022002049205995639f7357505aed54f7b392d2662276bc5d8968da78e6d623d9e6c7fc00fd0129020000000144fde6f10018b4cec394cce110bac5d84cce63839bed6752d3c1498c5662b9620000000000521a7a8006983a000000000000220020016c1483322b7ef8721387e87a51948fa2e8ecc28dfe33a48327a9eaafd70376983a000000000000220020092dc847c0de4b3408a7b0dced30f091ef9d73e5f8e719794e4267399ed5286d983a0000000000002200209c01da2ecb83e740748c0120909a95cd06387b45902c49810e55f86d34189a85204e000000000000220020c5a1bd48f7ffbc9cd20dadd15f11380a4e878dac76e56080c555134940f2e66ca8250a00000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfea8880b0000000000160014cf85091a7295547ebd2066aecb1a295440a9f6f080f0fc2021396d48be2b7fbc88fd5fe4ab91a19d7af24be98161251a487a8616cf6b425a7980d99b2a2105ccd02fcc73f3449fb169573f0d411ff78aa338eaa344cf549300041124d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b000000002b983a000000000000220020016c1483322b7ef8721387e87a51948fa2e8ecc28dfe33a48327a9eaafd70376005e0200000001d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b00000000000000000001221f0000000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfe00000000175abe3cc14d94456c949372ed6ace0b5b92e58c595a6297e6c1cdfbe5bd750a000000000000000100061b100a7a13c66544686dfc48ad7fc7d5abfb15a1404f9cb3df4abbb78e810023923100d382bd7186e9d7b47a3be1d8bf7b54eca322931d7514a3260bc6dd4cbb56131124d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b010000002b983a000000000000220020092dc847c0de4b3408a7b0dced30f091ef9d73e5f8e719794e4267399ed5286d005e0200000001d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b01000000000000000001221f0000000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfe00000000342bb8ca8412a52cbaa04072e0e285ff414a4bf36be1828482705a6bd13501a2000000000000000000061b10c5e470c9b1c309e76daf590ed9f1a7a1111ddef24029bef966622220d059acaf72d241dcade8b164a4309422d2d02130b50f81c0652cef0944d410b3a38a04531324d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b020000002b983a0000000000002200209c01da2ecb83e740748c0120909a95cd06387b45902c49810e55f86d34189a85005e0200000001d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b02000000000000000001b2200000000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfe101b0600415149b9751963f87c1583f34e4fb1bc5599cb48dc36e0ab744c37fb0630f93e000000000000000100061b10cd3756e951fcd4e7638026ce54e7962dfa1d6cc829a0ee998281b23118f5c83e2d7ff2aef3c1842b2fb552f2d2783f3d8187db9021cf41450d4f1ca15c1582f71324d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b030000002b204e000000000000220020c5a1bd48f7ffbc9cd20dadd15f11380a4e878dac76e56080c555134940f2e66c005e0200000001d570c959ea1c621f803b28f343b929732718012795559fe1532e03c42b29e59b030000000000000000013a340000000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfe101b0600af8cea7a0a1be6687852d7f2249be3b445ae0cddc097a05ea468ef797567856d000000000000000000061b10c2ed771e0c811887d21c9d5ad6b72039505e19d879892aa99b83b943374b380e188a459147347953f4a3c34a29d471309e672b9d72c97bd6e64a6e9ff26e31f300000000000000010004000000000000000000000000000000000001ff0000000000000000ff000000000000000100002710000000002de544800000000027a31840b315b404d6bae38833774438441fb36bd2925416d27eeec63326e772032c950f029ced6a1eb5263d34d0d6877178f5b9027cbd9014780197c22fb6fe6849d8c966000000ff0357ca5aa7f38f8ae405263cb1adf2e1469c7f39ee4f9c9b1e70ba2bd02b625478000100400000ffffffffffff0020a409b8983df586e5b71d21ad4824f82a2719d3cad0fdb071c20d0f265b9ba9d080007fffffffffff800002000000000000000000034273b111141c4f04ba55c7b2db01fb50000000000000000100039eb27ed9be0947c7be5941507f4f316200000101eecdd250c851e3ff0344421a4e791eaa008829834f77192587723797bbc77e9f05a886000220d741fabc6a2c7be66dde92517ef89f6ce9ee5288e02f46727ecc12038e9491ad408776ba708b6dafa314db2906226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f01eecdd250c851e368414f7c030100900000000000000000000858b800000014000000001dcd650000000003d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d230000000000000000000000000000061a80ff00012444fde6f10018b4cec394cce110bac5d84cce63839bed6752d3c1498c5662b962000000002b60e316000000000022002049205995639f7357505aed54f7b392d2662276bc5d8968da78e6d623d9e6c7fc00000000000218d33da90930d78dfda97b94815fd8543f4485d0574727143932171cc4d796ab02790a3f53b2f208b1ff65cbd654baf8c0f105b405e8e587438f88eb38bd081479000000061a80000000000000044c00002710000000000001ff0300000000000000022444fde6f10018b4cec394cce110bac5d84cce63839bed6752d3c1498c5662b9620000000022002049205995639f7357505aed54f7b392d2662276bc5d8968da78e6d623d9e6c7fcfffffffd0000000027a31840000000002de544800000000003dfd24002000000000000000622002029fedca3b62676f613aaaea53ff97b497f8799f40a5dc586e67f023c5b1a58750000000027a318400000000045bcc8800000000003dfd2400000000101000000000000000024f48b66833f2ecbd6f125eb8b4c38dd3836ea3e122b64fbe4e293fff54f32ffd0000000002b200b200000000000225120a4ac7358d683a4a296a84a196286ea8bf7c80683f3ea8c30be932eeb30b4cfac00000000000000020100000000000000040000000000183c10225120a4ac7358d683a4a296a84a196286ea8bf7c80683f3ea8c30be932eeb30b4cfac01000000000000000800000000000186a0220020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00061a8086d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2341f6c672e644f3aefdda12824a642ca2cce409ef69c5530dcbb2438d64a56ce70000fd025940e6616fcd9dce55776bd871840e88a65415dae895324ce3d454a59626ddc029474733535c6f675d83cade543d03cf7e422d8bb7f1dea3c9821d40cd55035691f10000000000000000020004fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000000e4e1c0342bb8ca8412a52cbaa04072e0e285ff414a4bf36be1828482705a6bd13501a200061b100003e368b6d07bc35ff7e3475feddeb313912771046d96fa86ec9a5ba2e9024f5df44c2609468b1f86640de3863c1e3ff60042667392d43809462bc3d18d454ad70de3d85e738909aaa1930559a093e363449e7a00c85cb62562298a108131f6f88635d990d4a6de980d076bfee5281d62828c19da4db7843c304c6d2fe742a8421ffb46c26b5e06249fb8fb4ff0b4d1f08d44d8d7d87953db3da6154f8d31d5af9780f6e45f4867fc7d77a1e438dd87172758779414784275dd05b88eafca27ed4424f1249c14d0894202377b2a394e3cc063badd9bd57f66122bb391fff3718520fc9dffa0b225b1eba27be4f0a386aeda91f85b126771f356afa9d1c9aeed249ac100ece76fc8b32903eaa9caeb2c0e3e6d05f69ad025d7ba625d536e6ad06d8b77aceb6d7a01dbb17adee7e17eca234b251e353e26de28849876b8cf88bd57f4a04745d6a88d32f00b35afdf416efd8f84826840e9e974cbd8aa4a102507f0c7e441ad9e1c1275ccc89266bde55f4a3582b78fa8aafa1994ef36b35d3943d3cd326bc70a3ddd377e895b498d79cbc857b5bb2e61f30ac8ca48fe186e40326fd9fef0215e2ef31844bcaec6334490a89dcd42219d64bfeacec7978294ac964b66b49958130a77f7569b083b1c280d4e451e872403454a3947d2cb10ad2f96f92b070f886a5ed771332f51889d18cc2ad941c6638eebefe5fe6cc4b79468b1e469adb08c34982783d49be95f32a774f51db6788d34f26a8af1b31fc709d78338c6c4d9834fa25a910b4b63e05aa6d383d282bba31c9378e3f27f985088e682e11a46a9a1c8e18be9b7c7ef02d9faa8bb08a53d5ffe059d48d440c6364292fb42df68a04aacdfaaba029f1460912b11a6f8eb57578832fb3c10675c7989114177bdb3b587a80856f351177d01eefbb1de210839c9b8b9ba621942f250dc6b4bbf014899b77f52e8e40cb06f7063371251c419bb8aa9d3c0bd83ce651520b6e2911dd38db6c0fff7cc64550320c5cac621765175460cc8f54ee4a0026ec8e57fd789e0fef66c4940cabc3944ff4e3fe10229294e6fcb3059adb4f9868e53b52a7da60f5ae75d4e42e6a94c44e375960f9ed44a56c0b6c49e7c5775dcc1a819547bf3f146e8bdb13f305ea0d15b5616623db4c0657059d07df7acf1062069705ae5699ce86d9d0de16f3afae97f5327fd340693f756aba0bf338af880927cd7f976705c2b511077bbd19b9a842cb7a9b2f713f183c1e4eac23bee3ac081be66de275451773444e81536e183c8e2f033766b854cc92773205abb0f39d90e762c1f0673af8e4051205f752757d2deb7dce6adf8a55297a3a56fe1265368970e8d480b8ee61fa38313e5a7069579f10ac2db37c05c58b38544a8750e186c43a153c7780cb8ddfa309fac85b487b43b1452624afbc4005bbc0777eda63c96edc94016d1fe880dcb2faab472802921adc29dd3410628b8431186a7ef3a6839eb16123ffccf40c4c0907764e40d1dabbaa12a3a9ed3f341c070fb622600b6f4595f4601f67f2d03e7221d57a20bad2e0fcde738107a57fea1e1257a89970bc94517fecfe4990c67f7bfde2ad523f0f4dc760e0e8958eb32c6a7b0e7d952e75fdf08550ab72b6824122697846e599c8a2cd7f3066c8c444e2bb41aa38cb063493af2106eeae5228247706a53169f306d6b0d74867db69d29e9a6eca2b77aeb3353fa5a593b9f4fa379ffd01464a6929841f0fe934d68c8ae9e523a87d02ce5df5636cc5dcd72536d044cedd2a1d1e9047712a399627a44c8776cc5d0b2fa7e3dbef52102602f97090afab7a6070af4a1e50d19fb646f25212a7e750d6d0f765c0d5324721afe49d1070a686b05dafd39114bc65cc11cb30cda09b872c79521273ff2f075ebc4a2e5444c6880b3854d623bf1c42be1026d8b0409fe0001a1470107fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0175abe3cc14d94456c949372ed6ace0b5b92e58c595a6297e6c1cdfbe5bd750a00061b1000025b15002f7b27451dba144086e3ccebd9726c2e1af20883b676631e13908f2a8a0dcabd2fe4d39cd5b0c9d007a4dc3f1a5624208693e461d934fcbb48428eb1cfba41682d19ab1221e11648459eee58b855ea196e854047d82869b62b2efc978f520eb13e35e3752e53026a0f7b22ddf65de7a4e3fea56fc86b9c85cb8df4cfd0262cf3bbe1e16eb25574db8fd56cee33cbfdb73073f700b9992c19ac808354db41e488aa31d6b8a3be8d151732aaf6353f30a18cd773285cddbd3b279e6455f9b90aa37d7055ccd9fdafa66ab0ad216fd123e4a4aadb25f9e004c5a8e794abced52cd6e208d8d2657d03f5179b6353133a9eaf151cf5d47298058ad85ada4e42d29b9c61524ac20264d3b6551efef60789c6a91cf1ce4e069e3ad123fba8d515d705c0e2f3aec108ab317116d593100a4db4affd32b4091a1368cfe3c91b422ff53c75ade0ed61dd562e55cd674e5a0c5ca5952c2204c401d9c61c25b565c83c38cc09eaa6e25c42d234875f2a353813b8db46f031f0392f73a9cc8a88925347e04cfb477f3a7c5578edaa447927c048aa631b4f3dafcb608fafef2e125a549e029f2598cae566f93febee55ab1175359913732673a44fd2a8e28f71e281917c7d0fc04816a5774a3411d1db8f290fe442e3d3141ab366633f07e46528d44528a422c9225129b0ae4e7f3b76c9c8fb25b9c7722be9b5862ce866980003752c178e7a94224a2b0b84fa1e2635b5e2207ade589baa96372d4453868b3e01e9ac2d1d80bc0c7e06a22c205f85e578d55e3283e9b2defb70f46a6084ea40e92b25f35550e7f3fc3c9e764dd4eea7b85df10175a0799e6a46cae937ddb1fbc43795a88850870cdd0f5bfb4506f227b1420f6e8d39b7ccbdc888a7df7066cfcd8da6ba6bceb2aa80bf14fd5d096f604af41ae92b9f4e3c6a7f29208e8b3fb70cf81fd56ff17ee56adf35cb18b39a067f67f9fc3f94e8c30d44760da2322a77679bd6ca0da0e47f206ef7b644448e7330bf19455ca777418896f0e07e551ae4995c7ac5026a7b84c0cd07baf9569135ae4cc3893339ac886ab704b37d46aa3edcfeaa6c4c8aec3ec8bf8ffb51a591ff02f04367314c267a3152070069950d384b99235e5398547d5084c4afbe86ce6a6a60964b8cd89b20c690e8de12606f64ea559a36431dc98d6189a3f15d439dcafd831a9dab64bf8dd5722c91d59fab00fad5b27a89d5f4237cb20ca55648a8256e8a0ba18394608840336d15a84451821619a2363eb51c5ee83e1722476106248962ccb5f7bd843e7dd14620a2b40107d972fa6b1e4ed7bd6b13b659b012870b64906229c93c2625b02da587d2239e09d64bec81f40c50398eb666ff8912711c2377165cbb0ad16999c0b09bc36f354cd495d63fc6cd7b89e4063ff1b63b319afa4a7a64da69aa41ba9af3131df5cb384ce68ab74898d699ea09730b53d3d862521927b0cde151844a9eb11aef8e0e25a3fb607d57c2a9f83eaf01f1c387e09be5fc5ac0eb091359ca26c8fb36cea3ccae8a63e57dcbc60ee66975bfee8964cfdfc840b114dc6ed5eac0babd26f99ec251c45ec602a44fcc6965c0d7c065c186569be6426658b8a5b32fc2dee315a0457e963635795a0d699db9587eeacf8f893699410a87b6390d8c8736552e325f5ccec1b6d4b45002b678649b234301610ecd605f9e56c5dc45d81cccf73e442b872369cbbd3a03eaba79c0af36e23ed97cf73808dc5ed0f5fcd751772b57e2666c5a70b1a5af61a0dd461e8a383e8426369ee6a4e03cf256a6bc3570997c3c06879946ffdd2924aa329e006759c0d6b564c89198c0c09f88c2c9d0c2cd4e0163e8f98145456cc859f55aa8d11a9969ae253d93d56d9fc6a386ae7c6ee884f483913bd739124f69b4bd5238fdc33d860a9cd4670de5dafe4a24fe0001a147010700fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000001312d00af8cea7a0a1be6687852d7f2249be3b445ae0cddc097a05ea468ef797567856d00061b100003e5507553c137054ae33b5d7cdaf142fbdbfae51246ea3db4441251aa49dac2e8cac063650741353682da85f0df1b1db72997177b74940bcabcb961f364c1861a493066a9cf2533aca5f866b280419fe04f9e9807d3049b759f78d7ce322554ed7891184e73d934c62f88ca33f7cd75d56617664cc674d74f553b8674093e60f014a89e1ab4ea7adde5d54f28a9976d2a3ddb20854b3a1f3ecac8490a9243234c7ce9c5df1b0e45d862c847fd93b99ed1f4d5fd5b06ca416ed048433782d4bd867d36196a7ebe1106f687d0868cd2f2cc1fd94f999de8e59e9a9b333aac6304ac7c01cb91d20c09d4e4f38397ccc1a0cbb46865b7292b84d4f11a783abb0d5da2e31fdc7c2885c6c90053f716264d0b8ba1ce53dc72e8f04e702e319029ca234b8bf72f5321382723010153eaf113e0c823914bf6ea319a1083efe2f0736e1f9b36125c4b77b9e4a581e6fecec959be70f2e21b8c9bd568641d45feb3ffdd39491abc973f310cb1f9c46a874471e5edc3ea7d4063274a9e10e2d21a9e836e99d386d062ac31bf19a14ad6d39c19752eb749c4dacc64aa4591228c039a1f3c0dfd919a2576edf70cbe018075d90c94bebbb91290487cb9cc613eef312a87025891ea6d8b02435b26abf72fea504aad333100e4f14ee916c678c6c9a42b3421bb8f7389b642f89b5abaf99f0df25e5859bee216c3845bbc9fdb032acb88cc144306cc17fe754280bccfecd86670d6994833c4847c71913caed6d904525d22d28aadf3d4e867a617f20a9cf61e95152eb1cab90bae7885da01effee11b7780bd2398d37f04096e7d14252e2026f329422c077762996f45a7c4d546d7ad0422996e659c98f1feebc5c73eeecab837c025a44d94cb5ee39d5a1bbd95973e8e5cdd97cabb7b8f4f86a5bb228c0d12be6b9b2032c54795f4f7180828e94377bf6e0f56545dfcd5252da0936d79aef70caa8715e8416579ca1a928c2aecfa1e03b6cd795528a967fb3dbe524996be92b6168222112063077fb39811ebf9dbb97abafe1b315488c8812224faef56c1eb86837ffcf61bc22a60de7e4f6e0df67c332389556df7da838200dcceeafcb6e8a94aaad013c30934ec0ada06fa17afb5414de4812deb6fa96517df57d503e1e521562ed62a65073252dd08aafba343c029e04faf58b4c451061f156669be5ef68a2adff506cba6351e407d3c6077b71807f21e93d0ca69843f2d5fd6b7c7f415d740fac1f1f8c8bd8f95297ee1ffafbe1b41c548b69dd6d211352c556f14bb4e5af76244e7656f882452201e2bfd90a87413904081a78c2d6849a6a07ad77920465141da19034b70c3edac53a560e7b5c07923c9bc6e17b8091257fe7431e7cb18d770ba1457e62714a47beb8e503874f24405c49121d2cb024edee3458b31ebe1bf40a13f564b8ff95dc537ec926311f59150544fcc57efcef53d9f43c41b0dc32f93b91d2663e9f6b09f9483b4a788b0642d667c722d10ddf09a70886153fc8c943a9d6f3eecec3ab7144669c10b3c5864a174c7343a74e4b58da29f0ae480d5be8adb5d5b55e9cc3c11da1a935c2b1cb966d85612b6c4bbd42aaaeeebada9f062175f502c2cde7daa345239f99201be98ae989cd8a3cd0610779fd413ef1c6c48cd6e8bcb454b847815016e214f1e6162df1b388503b06bad1e5e35ca424fa1ac4407757504e311f4ee0f4d8e5dce45759a96031460842697d577205255b9ba2bbbe4eb08528a9224087a40129906655170405ad4491052ba71d6dde4000b7d7154634db60db540a16cc430b5998dc1465bc57b5a4125c967e4fc451d66026b650b9a70c26998f559793809df549ca1d5e730db0759ed40d3a030c8a1c6bccb28b5fd9230c55ef83d23b369938200099274978bcc017a96bd1b506c6d28b46788f9a483f3e0b234fe0001a147010700fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0415149b9751963f87c1583f34e4fb1bc5599cb48dc36e0ab744c37fb0630f93e00061b1000033fabe908e0bb47028814d9361e2518822ec95a72c8e611b1562fb595ad452db3272b17a6f9280115fa6e17434cfc1639b3b1795ff6cfe4aaa80b3393100ba4a2308b9e42dbbb3cba55bec53bbf574b9307e2023f3e13e6d0424ce7b4376280ea0d3a187c8585212fe9effee9aa102873a48f1b71a2ae81cc97fdf6075b4ee17716bbcbcd930a23d483f04907dcf89d9d55d62ac6935ce45afe8549de48a363c595876e32e8ba33806671cb54a92a81409f9bc8964983552f709335efb962d17c2bcc348fcf9af666ae7cff45833b4b0449481a6cc869a6cc2d928ccba9499e6415a52c455ebce14cd94a36fa0bf127682e890be81a1dcc77dd2b94f886103d793ffce97430edf04f4b8a0aaf9622b34bfceaef96bd6848717430d2212f2df30105354306fe8d5d452c32e1b0f1872cb7c953295d2794805fcad0c50cebd26b14cc903fd995d4eb397d85d3bbcc92a4b640ec4448a16eaae0941f2f074ea2f292b55c8cd8051f5b43c4f0d14ae04038d06216cf01b344fb3b8b42ae2680d7998c5f24c16813dcfc521525b5bda78bf604edab7edcac3934cbb9e5e1baae044ea136613ac003e86e3caa9f0e93c38c5997a17ab472004726730f79248be824fbe894422e2b07f951d31ccc32c7f513a61c7e0a4f3790faf2dca5fc4afca1a0f14b0a7a07178402f1875430c905250a5d4d8d70521418cca1c82a2b3721831087e342d74bab5395c5099fe8710c667fb463e50f59da0b67d7f8b2a4b47896c84954a82d604a570f41fb1a5a9906592ee050b0cad8fdc652a411f8dd0d4aa5c46540479b202f7752985311671be2fca007d87b2f27f16be6a590357aeb87f9f75d9f2c798599f3e8bc2fe5b1e7b5d4e3a779761d983f52e5d4844c7eabbd4e543436954a9e361f2f099bcb603a94f2cfc1f1409aa50aee8644c5a1222e2df52fd8a1f884c0a1a800c92be47b7bed41ceff4ee44700c88e527b55c0abdd04f86ce1dae36f0c2802bc4cc74e0698092fed0e8ab7a95e1bfdf0b6fd160a7c80b0f1bba8dde89740e4caa6681f1d4c926a49ee2088282d18e3646e4ade293e1b2cb5a0155d3ee662d5c468cb5d2ac7557e7666396336e3608d84c97fb1f48dd8ca7f8528b218eeac9a825b161147849d1c2a1ee461150cf8984f492d02221ea43d994821790f5e7357e615ce721817e89495bc3422b98a7e6d6288d75c3b727d1669742b682560660079d8860ed65022d279ca414cd56e169e1147d27ad808290f7daeb665a949b793ae503c1dc9d3f1ae0e57b02d373357408d881e2354337143f7d0b99dd2adf48e6c4f4983bc1de85345892e246c6edbd9aee3747fcc2efa956e3e89e9f5a9be0401933b51d625e25c8296be8ef13720d15411d029a471387fa2c28a168f72e0802d84b3085937f9a83353e002b39596e1567d712e729aa55e648f87043be90b359d3e1fd439091d3ded3b72c0c0bdac8e3033a2b2d6e4e074094b7ff6eb631e2b62ec36e35ea434154e0189b1b1e8b9b60da771cf0077ff267e7e861045d24d0c3fa57b9408d3e2116bf75ba02b4d8cd78701ca9836074440c6c0c0ccdb2cd490ee616bc6d3f8d10ddaf60dfe77003ea326673193d21032997f8fb77c2393ce7af5f1d3d3abef5550e80c5a4e3b7ae75ae3f79950a2f8e5065e8d8fecd2f195aac557d7a4b4a5d1e0c5f1e60c0005d73df261401196c93384fcc271b0ebf0a9da2adee213319ed1fb5bec02202b4a0778bdd48420e701105a135b8bf2d4f50b6939ca0890174d7ce9852473456059b9b7a85db8e4664fa67dbc2d729080df312f3d8970e28e7304510b4828624279cf789d9deebf08cce0c1cf84ffb2abc52784230a5b488fe167632882be37fd1564c22b0dc89cf86ea5ffe6d7880b7199eecb9957b28ba3035316b763a4c0a0691bfe0001a1470107000027100000000027a318400000000045bcc8802441f6c672e644f3aefdda12824a642ca2cce409ef69c5530dcbb2438d64a56ce7010000002be0fd1c000000000022002029fedca3b62676f613aaaea53ff97b497f8799f40a5dc586e67f023c5b1a587500fd0129020000000141f6c672e644f3aefdda12824a642ca2cce409ef69c5530dcbb2438d64a56ce70100000000521a7a8006983a000000000000220020016c1483322b7ef8721387e87a51948fa2e8ecc28dfe33a48327a9eaafd70376983a000000000000220020092dc847c0de4b3408a7b0dced30f091ef9d73e5f8e719794e4267399ed5286d983a0000000000002200209c01da2ecb83e740748c0120909a95cd06387b45902c49810e55f86d34189a85204e000000000000220020c5a1bd48f7ffbc9cd20dadd15f11380a4e878dac76e56080c555134940f2e66ca8250a00000000002200202b0c7734d2e70db4213bca81a2e46458f6e105ff6e2e66aaa3b76133134c3dfe28a3110000000000160014cf85091a7295547ebd2066aecb1a295440a9f6f080f0fc2000000000000000000001000400fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000000e4e1c0342bb8ca8412a52cbaa04072e0e285ff414a4bf36be1828482705a6bd13501a200061b100003e368b6d07bc35ff7e3475feddeb313912771046d96fa86ec9a5ba2e9024f5df44c2609468b1f86640de3863c1e3ff60042667392d43809462bc3d18d454ad70de3d85e738909aaa1930559a093e363449e7a00c85cb62562298a108131f6f88635d990d4a6de980d076bfee5281d62828c19da4db7843c304c6d2fe742a8421ffb46c26b5e06249fb8fb4ff0b4d1f08d44d8d7d87953db3da6154f8d31d5af9780f6e45f4867fc7d77a1e438dd87172758779414784275dd05b88eafca27ed4424f1249c14d0894202377b2a394e3cc063badd9bd57f66122bb391fff3718520fc9dffa0b225b1eba27be4f0a386aeda91f85b126771f356afa9d1c9aeed249ac100ece76fc8b32903eaa9caeb2c0e3e6d05f69ad025d7ba625d536e6ad06d8b77aceb6d7a01dbb17adee7e17eca234b251e353e26de28849876b8cf88bd57f4a04745d6a88d32f00b35afdf416efd8f84826840e9e974cbd8aa4a102507f0c7e441ad9e1c1275ccc89266bde55f4a3582b78fa8aafa1994ef36b35d3943d3cd326bc70a3ddd377e895b498d79cbc857b5bb2e61f30ac8ca48fe186e40326fd9fef0215e2ef31844bcaec6334490a89dcd42219d64bfeacec7978294ac964b66b49958130a77f7569b083b1c280d4e451e872403454a3947d2cb10ad2f96f92b070f886a5ed771332f51889d18cc2ad941c6638eebefe5fe6cc4b79468b1e469adb08c34982783d49be95f32a774f51db6788d34f26a8af1b31fc709d78338c6c4d9834fa25a910b4b63e05aa6d383d282bba31c9378e3f27f985088e682e11a46a9a1c8e18be9b7c7ef02d9faa8bb08a53d5ffe059d48d440c6364292fb42df68a04aacdfaaba029f1460912b11a6f8eb57578832fb3c10675c7989114177bdb3b587a80856f351177d01eefbb1de210839c9b8b9ba621942f250dc6b4bbf014899b77f52e8e40cb06f7063371251c419bb8aa9d3c0bd83ce651520b6e2911dd38db6c0fff7cc64550320c5cac621765175460cc8f54ee4a0026ec8e57fd789e0fef66c4940cabc3944ff4e3fe10229294e6fcb3059adb4f9868e53b52a7da60f5ae75d4e42e6a94c44e375960f9ed44a56c0b6c49e7c5775dcc1a819547bf3f146e8bdb13f305ea0d15b5616623db4c0657059d07df7acf1062069705ae5699ce86d9d0de16f3afae97f5327fd340693f756aba0bf338af880927cd7f976705c2b511077bbd19b9a842cb7a9b2f713f183c1e4eac23bee3ac081be66de275451773444e81536e183c8e2f033766b854cc92773205abb0f39d90e762c1f0673af8e4051205f752757d2deb7dce6adf8a55297a3a56fe1265368970e8d480b8ee61fa38313e5a7069579f10ac2db37c05c58b38544a8750e186c43a153c7780cb8ddfa309fac85b487b43b1452624afbc4005bbc0777eda63c96edc94016d1fe880dcb2faab472802921adc29dd3410628b8431186a7ef3a6839eb16123ffccf40c4c0907764e40d1dabbaa12a3a9ed3f341c070fb622600b6f4595f4601f67f2d03e7221d57a20bad2e0fcde738107a57fea1e1257a89970bc94517fecfe4990c67f7bfde2ad523f0f4dc760e0e8958eb32c6a7b0e7d952e75fdf08550ab72b6824122697846e599c8a2cd7f3066c8c444e2bb41aa38cb063493af2106eeae5228247706a53169f306d6b0d74867db69d29e9a6eca2b77aeb3353fa5a593b9f4fa379ffd01464a6929841f0fe934d68c8ae9e523a87d02ce5df5636cc5dcd72536d044cedd2a1d1e9047712a399627a44c8776cc5d0b2fa7e3dbef52102602f97090afab7a6070af4a1e50d19fb646f25212a7e750d6d0f765c0d5324721afe49d1070a686b05dafd39114bc65cc11cb30cda09b872c79521273ff2f075ebc4a2e5444c6880b3854d623bf1c42be1026d8b0409fe0001a147010700fd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0175abe3cc14d94456c949372ed6ace0b5b92e58c595a6297e6c1cdfbe5bd750a00061b1000025b15002f7b27451dba144086e3ccebd9726c2e1af20883b676631e13908f2a8a0dcabd2fe4d39cd5b0c9d007a4dc3f1a5624208693e461d934fcbb48428eb1cfba41682d19ab1221e11648459eee58b855ea196e854047d82869b62b2efc978f520eb13e35e3752e53026a0f7b22ddf65de7a4e3fea56fc86b9c85cb8df4cfd0262cf3bbe1e16eb25574db8fd56cee33cbfdb73073f700b9992c19ac808354db41e488aa31d6b8a3be8d151732aaf6353f30a18cd773285cddbd3b279e6455f9b90aa37d7055ccd9fdafa66ab0ad216fd123e4a4aadb25f9e004c5a8e794abced52cd6e208d8d2657d03f5179b6353133a9eaf151cf5d47298058ad85ada4e42d29b9c61524ac20264d3b6551efef60789c6a91cf1ce4e069e3ad123fba8d515d705c0e2f3aec108ab317116d593100a4db4affd32b4091a1368cfe3c91b422ff53c75ade0ed61dd562e55cd674e5a0c5ca5952c2204c401d9c61c25b565c83c38cc09eaa6e25c42d234875f2a353813b8db46f031f0392f73a9cc8a88925347e04cfb477f3a7c5578edaa447927c048aa631b4f3dafcb608fafef2e125a549e029f2598cae566f93febee55ab1175359913732673a44fd2a8e28f71e281917c7d0fc04816a5774a3411d1db8f290fe442e3d3141ab366633f07e46528d44528a422c9225129b0ae4e7f3b76c9c8fb25b9c7722be9b5862ce866980003752c178e7a94224a2b0b84fa1e2635b5e2207ade589baa96372d4453868b3e01e9ac2d1d80bc0c7e06a22c205f85e578d55e3283e9b2defb70f46a6084ea40e92b25f35550e7f3fc3c9e764dd4eea7b85df10175a0799e6a46cae937ddb1fbc43795a88850870cdd0f5bfb4506f227b1420f6e8d39b7ccbdc888a7df7066cfcd8da6ba6bceb2aa80bf14fd5d096f604af41ae92b9f4e3c6a7f29208e8b3fb70cf81fd56ff17ee56adf35cb18b39a067f67f9fc3f94e8c30d44760da2322a77679bd6ca0da0e47f206ef7b644448e7330bf19455ca777418896f0e07e551ae4995c7ac5026a7b84c0cd07baf9569135ae4cc3893339ac886ab704b37d46aa3edcfeaa6c4c8aec3ec8bf8ffb51a591ff02f04367314c267a3152070069950d384b99235e5398547d5084c4afbe86ce6a6a60964b8cd89b20c690e8de12606f64ea559a36431dc98d6189a3f15d439dcafd831a9dab64bf8dd5722c91d59fab00fad5b27a89d5f4237cb20ca55648a8256e8a0ba18394608840336d15a84451821619a2363eb51c5ee83e1722476106248962ccb5f7bd843e7dd14620a2b40107d972fa6b1e4ed7bd6b13b659b012870b64906229c93c2625b02da587d2239e09d64bec81f40c50398eb666ff8912711c2377165cbb0ad16999c0b09bc36f354cd495d63fc6cd7b89e4063ff1b63b319afa4a7a64da69aa41ba9af3131df5cb384ce68ab74898d699ea09730b53d3d862521927b0cde151844a9eb11aef8e0e25a3fb607d57c2a9f83eaf01f1c387e09be5fc5ac0eb091359ca26c8fb36cea3ccae8a63e57dcbc60ee66975bfee8964cfdfc840b114dc6ed5eac0babd26f99ec251c45ec602a44fcc6965c0d7c065c186569be6426658b8a5b32fc2dee315a0457e963635795a0d699db9587eeacf8f893699410a87b6390d8c8736552e325f5ccec1b6d4b45002b678649b234301610ecd605f9e56c5dc45d81cccf73e442b872369cbbd3a03eaba79c0af36e23ed97cf73808dc5ed0f5fcd751772b57e2666c5a70b1a5af61a0dd461e8a383e8426369ee6a4e03cf256a6bc3570997c3c06879946ffdd2924aa329e006759c0d6b564c89198c0c09f88c2c9d0c2cd4e0163e8f98145456cc859f55aa8d11a9969ae253d93d56d9fc6a386ae7c6ee884f483913bd739124f69b4bd5238fdc33d860a9cd4670de5dafe4a24fe0001a1470107fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000000000000001312d00af8cea7a0a1be6687852d7f2249be3b445ae0cddc097a05ea468ef797567856d00061b100003e5507553c137054ae33b5d7cdaf142fbdbfae51246ea3db4441251aa49dac2e8cac063650741353682da85f0df1b1db72997177b74940bcabcb961f364c1861a493066a9cf2533aca5f866b280419fe04f9e9807d3049b759f78d7ce322554ed7891184e73d934c62f88ca33f7cd75d56617664cc674d74f553b8674093e60f014a89e1ab4ea7adde5d54f28a9976d2a3ddb20854b3a1f3ecac8490a9243234c7ce9c5df1b0e45d862c847fd93b99ed1f4d5fd5b06ca416ed048433782d4bd867d36196a7ebe1106f687d0868cd2f2cc1fd94f999de8e59e9a9b333aac6304ac7c01cb91d20c09d4e4f38397ccc1a0cbb46865b7292b84d4f11a783abb0d5da2e31fdc7c2885c6c90053f716264d0b8ba1ce53dc72e8f04e702e319029ca234b8bf72f5321382723010153eaf113e0c823914bf6ea319a1083efe2f0736e1f9b36125c4b77b9e4a581e6fecec959be70f2e21b8c9bd568641d45feb3ffdd39491abc973f310cb1f9c46a874471e5edc3ea7d4063274a9e10e2d21a9e836e99d386d062ac31bf19a14ad6d39c19752eb749c4dacc64aa4591228c039a1f3c0dfd919a2576edf70cbe018075d90c94bebbb91290487cb9cc613eef312a87025891ea6d8b02435b26abf72fea504aad333100e4f14ee916c678c6c9a42b3421bb8f7389b642f89b5abaf99f0df25e5859bee216c3845bbc9fdb032acb88cc144306cc17fe754280bccfecd86670d6994833c4847c71913caed6d904525d22d28aadf3d4e867a617f20a9cf61e95152eb1cab90bae7885da01effee11b7780bd2398d37f04096e7d14252e2026f329422c077762996f45a7c4d546d7ad0422996e659c98f1feebc5c73eeecab837c025a44d94cb5ee39d5a1bbd95973e8e5cdd97cabb7b8f4f86a5bb228c0d12be6b9b2032c54795f4f7180828e94377bf6e0f56545dfcd5252da0936d79aef70caa8715e8416579ca1a928c2aecfa1e03b6cd795528a967fb3dbe524996be92b6168222112063077fb39811ebf9dbb97abafe1b315488c8812224faef56c1eb86837ffcf61bc22a60de7e4f6e0df67c332389556df7da838200dcceeafcb6e8a94aaad013c30934ec0ada06fa17afb5414de4812deb6fa96517df57d503e1e521562ed62a65073252dd08aafba343c029e04faf58b4c451061f156669be5ef68a2adff506cba6351e407d3c6077b71807f21e93d0ca69843f2d5fd6b7c7f415d740fac1f1f8c8bd8f95297ee1ffafbe1b41c548b69dd6d211352c556f14bb4e5af76244e7656f882452201e2bfd90a87413904081a78c2d6849a6a07ad77920465141da19034b70c3edac53a560e7b5c07923c9bc6e17b8091257fe7431e7cb18d770ba1457e62714a47beb8e503874f24405c49121d2cb024edee3458b31ebe1bf40a13f564b8ff95dc537ec926311f59150544fcc57efcef53d9f43c41b0dc32f93b91d2663e9f6b09f9483b4a788b0642d667c722d10ddf09a70886153fc8c943a9d6f3eecec3ab7144669c10b3c5864a174c7343a74e4b58da29f0ae480d5be8adb5d5b55e9cc3c11da1a935c2b1cb966d85612b6c4bbd42aaaeeebada9f062175f502c2cde7daa345239f99201be98ae989cd8a3cd0610779fd413ef1c6c48cd6e8bcb454b847815016e214f1e6162df1b388503b06bad1e5e35ca424fa1ac4407757504e311f4ee0f4d8e5dce45759a96031460842697d577205255b9ba2bbbe4eb08528a9224087a40129906655170405ad4491052ba71d6dde4000b7d7154634db60db540a16cc430b5998dc1465bc57b5a4125c967e4fc451d66026b650b9a70c26998f559793809df549ca1d5e730db0759ed40d3a030c8a1c6bccb28b5fd9230c55ef83d23b369938200099274978bcc017a96bd1b506c6d28b46788f9a483f3e0b234fe0001a1470107fffd05b1d174a99366c416559a37c33a3fc696a1abb769ce6d9e977c7c67f272f4483d2300000000000000010000000000e4e1c0415149b9751963f87c1583f34e4fb1bc5599cb48dc36e0ab744c37fb0630f93e00061b1000033fabe908e0bb47028814d9361e2518822ec95a72c8e611b1562fb595ad452db3272b17a6f9280115fa6e17434cfc1639b3b1795ff6cfe4aaa80b3393100ba4a2308b9e42dbbb3cba55bec53bbf574b9307e2023f3e13e6d0424ce7b4376280ea0d3a187c8585212fe9effee9aa102873a48f1b71a2ae81cc97fdf6075b4ee17716bbcbcd930a23d483f04907dcf89d9d55d62ac6935ce45afe8549de48a363c595876e32e8ba33806671cb54a92a81409f9bc8964983552f709335efb962d17c2bcc348fcf9af666ae7cff45833b4b0449481a6cc869a6cc2d928ccba9499e6415a52c455ebce14cd94a36fa0bf127682e890be81a1dcc77dd2b94f886103d793ffce97430edf04f4b8a0aaf9622b34bfceaef96bd6848717430d2212f2df30105354306fe8d5d452c32e1b0f1872cb7c953295d2794805fcad0c50cebd26b14cc903fd995d4eb397d85d3bbcc92a4b640ec4448a16eaae0941f2f074ea2f292b55c8cd8051f5b43c4f0d14ae04038d06216cf01b344fb3b8b42ae2680d7998c5f24c16813dcfc521525b5bda78bf604edab7edcac3934cbb9e5e1baae044ea136613ac003e86e3caa9f0e93c38c5997a17ab472004726730f79248be824fbe894422e2b07f951d31ccc32c7f513a61c7e0a4f3790faf2dca5fc4afca1a0f14b0a7a07178402f1875430c905250a5d4d8d70521418cca1c82a2b3721831087e342d74bab5395c5099fe8710c667fb463e50f59da0b67d7f8b2a4b47896c84954a82d604a570f41fb1a5a9906592ee050b0cad8fdc652a411f8dd0d4aa5c46540479b202f7752985311671be2fca007d87b2f27f16be6a590357aeb87f9f75d9f2c798599f3e8bc2fe5b1e7b5d4e3a779761d983f52e5d4844c7eabbd4e543436954a9e361f2f099bcb603a94f2cfc1f1409aa50aee8644c5a1222e2df52fd8a1f884c0a1a800c92be47b7bed41ceff4ee44700c88e527b55c0abdd04f86ce1dae36f0c2802bc4cc74e0698092fed0e8ab7a95e1bfdf0b6fd160a7c80b0f1bba8dde89740e4caa6681f1d4c926a49ee2088282d18e3646e4ade293e1b2cb5a0155d3ee662d5c468cb5d2ac7557e7666396336e3608d84c97fb1f48dd8ca7f8528b218eeac9a825b161147849d1c2a1ee461150cf8984f492d02221ea43d994821790f5e7357e615ce721817e89495bc3422b98a7e6d6288d75c3b727d1669742b682560660079d8860ed65022d279ca414cd56e169e1147d27ad808290f7daeb665a949b793ae503c1dc9d3f1ae0e57b02d373357408d881e2354337143f7d0b99dd2adf48e6c4f4983bc1de85345892e246c6edbd9aee3747fcc2efa956e3e89e9f5a9be0401933b51d625e25c8296be8ef13720d15411d029a471387fa2c28a168f72e0802d84b3085937f9a83353e002b39596e1567d712e729aa55e648f87043be90b359d3e1fd439091d3ded3b72c0c0bdac8e3033a2b2d6e4e074094b7ff6eb631e2b62ec36e35ea434154e0189b1b1e8b9b60da771cf0077ff267e7e861045d24d0c3fa57b9408d3e2116bf75ba02b4d8cd78701ca9836074440c6c0c0ccdb2cd490ee616bc6d3f8d10ddaf60dfe77003ea326673193d21032997f8fb77c2393ce7af5f1d3d3abef5550e80c5a4e3b7ae75ae3f79950a2f8e5065e8d8fecd2f195aac557d7a4b4a5d1e0c5f1e60c0005d73df261401196c93384fcc271b0ebf0a9da2adee213319ed1fb5bec02202b4a0778bdd48420e701105a135b8bf2d4f50b6939ca0890174d7ce9852473456059b9b7a85db8e4664fa67dbc2d729080df312f3d8970e28e7304510b4828624279cf789d9deebf08cce0c1cf84ffb2abc52784230a5b488fe167632882be37fd1564c22b0dc89cf86ea5ffe6d7880b7199eecb9957b28ba3035316b763a4c0a0691bfe0001a1470107000027100000000045bcc8800000000027a318401078a5654313845429e9ff89f5848fb34caef8f89701d89c1f989a2169a7d1ea029ced6a1eb5263d34d0d6877178f5b9027cbd9014780197c22fb6fe6849d8c96600") + val decoded = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] + assert(decoded.spliceStatus.isInstanceOf[SpliceStatus.SpliceWaitingForSigs]) + val spliceStatus = decoded.spliceStatus.asInstanceOf[SpliceStatus.SpliceWaitingForSigs] + assert(spliceStatus.signingSession.localCommit.isLeft) + val unsignedLocalCommit = spliceStatus.signingSession.localCommit.left.toOption.get + assert(unsignedLocalCommit.index == 2) + assert(unsignedLocalCommit.txId == TxId.fromValidHex("88d4950aff3bb218308d09ca650ac4648f4cbbadbd1d0f333c0e6fd0a483cf07")) + assert(unsignedLocalCommit.input == InputInfo(OutPoint(TxId.fromValidHex("e76ca5648d43b2cb0d53c569ef09e4cca22c644a8212dafdaef344e672c6f641"), 1), TxOut(1_900_000 sat, hex"002029fedca3b62676f613aaaea53ff97b497f8799f40a5dc586e67f023c5b1a5875"), ByteVector.empty)) + } + + test("decode local commit with commit and htlc transactions") { + // Before https://github.com/ACINQ/eclair/pull/3099, we stored the unsigned commit tx and HTLC txs in our local commit. + val bin = ByteVector.fromValidHex("001801fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac5901010340100002aaaa00ce2f18a967dc4f25f414e671ba6585f8ef0b8c5fb812c21064f55a2eaa000999383d0b615906c5db6e5faf402f8773d6d1f0725eb0efe6f704b78f0325c95680000001000000000000044c000000001dcd65000000000000002710000000000000000000900064c0000000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000180802aa698202bbbb671d15145722fb8c28d732cddb249bcc6652ed2b297ff1f77a18371b1e6300000000000003e8000000003b9aca000000000000004e2000000000000003e80090001e03d634673ed2604fc4bec7b755da75f47baf62f271710775c1f283b33f7ec9816e02c37e84f926a720ce810decfa0f080ca43aecfa9bb52464656532ece22361b8920395356cfcfdf4473db386008e6cacbc489d0cade2d59bc502d1fefa0e67652371036a529fc054ac492739b0a286456849a0bb2fe129bf858bcf603247c1f0ead492000000140800000000000000000000000000100802aa6982000000000000000000000000000000000000000000060000000000000000000300fd05b1fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac5900000000000000030000000000e4e1c036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e96800061b10000332c98f379daa236da56ac028f19b025e0b1fff57b4051ddecfb9aed7f4c676a7302ce27f88b74e6d77ee96dfc20b29b73d239bf369230407406dd3d23f3d55f9d6f16b0cb112acda00db9c8d6da811add226c8c673476eebc7b91b1995653afd9ab7693419809bbd44f32be98144911079cd5e0ce023519671fb12fea4b77ef14ec40dea3e1a13c0df50ec2133234fc55c0473105a0fa2ab10b9ea7a3db42f02a9270a9ae032b393ad7d192b96877a5770c3803a4a3eea32e61e9a2854affdf4754f0667bb95bf1c07bd63decd294a167fb7fdcd70e527b27aeaeb48689be4942f0ae3d9b49ae78adfac66d726c021ef0e33af84c4840281915c8ea4cd77ca0441e8810a27c517e9a62385c28590c221dc38f3eb05b0aa371d3250d4ba373e2b79adccf6751f8ae1eb3e363c2b01f9592b0ec35951580ab88eb5069a1e708461e37766af7c3d86c8ffb3ad1981c68d7ffa74f9d4379eb977eda42146871b367cec89afd8d9c0f434e1c132f28de409880f2fa540f16685326fa09e4e4ff48adcf7ec29150ab5b2f581841000401ab4412c1ab3dea8c904c7c5e24421359de09bce12a2a42830a699756530f75ba3febfd16cffc73dc06a4faabb01d7d8914f4a17a0e463ed824e52cd10fa6ddf4bb9b3f7dc57137b42d7fa2acc782414a65813af01b27217e80e69ec3e4b6c74e2dbc87f1cfbcc88e9f450361d40ee5dc4d0311618b01d3030d47a96a569b416a028f6f356f17348276cda2a68816f2a3ae09b5495b1f4d6d3eefcc4ef3572cff82e4ecc6a32adde0142b665123831db84511f3e878bb1e9c06ebfa73b19ced2dd905a51a7eabb4ef3c13c2a874dfc200e2408dddd8536b78d9a60929d47beed0c0dcce1c52ff1aa29a5e4446acd66aa887b36d63967b23ceaca0468f7ac5f616018264b2f66fde267cb9d5f3f9982297bb56e16b93d6f5f028bff9e9f76475cdce9a702747b24ffc91620282a1ddd6dc721225bbd203bff7e3851aa22d6f7dfdab44247728d5cd6ea6f5d80b9928cd28c40187b5753454f96ebd014981fcf72eab4fa1937ff9a958e50b40c4527913e5b9b30dabb8b2d695c19fdb1704540c70aec09f551df64ead2dbabf1917a038e263f0c0166a903f949da0b9a9b285a8ad2c3cfa7f5ad57f07a63b1383a55ed294fecb2225f9640901c7dc3102603c1adc574bbf43730f93d64cf36dc9ddbef0ae5a7939e84987be7ca88081567ef7ef89da7618658465cc05a115b62a25a2e14584e1fe83d5c9cc2b30f8675f7e51f256139f836d8a70a88a7fdec726ffdc6d67b4bb5dc186801ed4fea79efb93a03cd3f17f2411d69c8ae6f6aaefe777d44ec7199c267f14c6a60eb7071cf67f62d453eedb7d54e048dda18466a1c9b47e8920699473d3559c5e223a80e09f4a891fd68ab2cbc12ee0524052b773fd5d8a872754e1336883edea17990de729f7a119bd4f2ccecfed6ea22ae5de41193c6f51ed4c31ead31e5dd661f8dede9d03372645f5a3b5632075ef844f773a52559e46fc2714b5cb2600df2e74275fe1d95e8f3f44dc91840123a885edd2e2d972ca1a0917c54859670ed3091638a0dbc9580f5b25acb5b8688f6d19e00782fb7f2e965c60dfcc8e53b15a1ab399fd09e1c6bcccf6d35df8f69ed2854039041764e101b4e60fde246734971cd98493c45db7fa12b92969ba79ccfd35409f47c84b38f3b2b11e6614a278dc8717955b0500f3d9a88179be50ade3626472a821b15e413c69ea6367f8b56638ee34070adfb6edb8dbd03b595ba089317f9b7b93b317699284d7a562821f74ec92b9e55100e804232c2231d6400b70e2cc6463f17f778c7e98dbdb6da0646a23c6e90e07f80c42a61e23314464986b3cedc9d6a9255b002d209dd2e3576658692b2f3bf6036a263663f9b99074cd514fe0001a147010700fd05b1fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac5900000000000000040000000000e4e1c036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e96800061b1000034086ba1a46c484ed86af94562b643be22879d2dade698f04ec40dab91997e719e19f921b0bf7f276d42672953db62c9b8e4569ef8df2eaf0e4c9349e3b2691f38b6459430b08d288ec8a3d8dccd2e420cf892c8c2ef053d45c21e03dfb438f78041aaf0df9a11c2569476f8f3a525c412f37819f1784fa8f47a53f110f1432a4c55b684a6ad5920f74e378f07802907fd5f4dc6a6aa00bf93d88a3d087642ba67fdc0d8da3fc7b4463b760267d1e55d5ea5158c9b0633f704d57d9f72b847dc3efe35ef861066a9d8d3a555db9344e94fc428ecec960570cf208f3c690ebadc443a8464a74c4fcb32e7e53994d44426ef26402bf9317840b5ef6c41edaf43e9b70da123a06bc31ced8499cd08796cdbdc9b80e6a523b483928afc032439f8e0b47af48384c2004cbc9decb82ea36e62fd3ca07f20f7729684dfa4793ae24a4d1977a5a37f0ffb9f8e5860261372e5107ee58029ba6058141a39a3d4390a86f8a38554d87bfcbbaec495cab0939cff761a316956f6db23fcd3d552b2a6f620d3a2b01868f712784d5a8cd3c64d394f1f8abcf6184862419da136db11f53292df1863eed4e2a04a0ef59a573b800913e27e11fab79260f1a79c35e36026bd77ec852618ced1b5b24909197964331da3f687daa82a34d2ea5f73e2bcc6bc55dfa9e07187a146101c224755b1436e292b4df8597418ac9dc0f46b39a7b2630cdd73383c03f0905a25d3a52ab2b46b49e407fccb7e2fd943603710808b122195d22b2f2d022aea4b0dce9bef1b40e37600d3d652a28a144cf39a088f09d19948fae90142384066cd56541d2003b38ecb5050beb6f618dd3c619973ee64cb76a2565a2ed1ba6c6838dac3698385fc33ca4951f6ac6ae7dfa2c4bc027e3fefb520be24b5d26d8fc929ea1b14cd9427997b7634893928bcaa32600ea7b65a5e96fcd345962f5f4fc68905f9286db7bea219757399d11774491c5159663e9295bf5911eec5a1dc45505bd425dce8ae5e09fbef489276ced951e20d2154d7bfee8a125eb565e0b980abf53f1fd031412233bc0868568a7314945af9c276e52e09add4be6d0ed8ce30d8da4c62cbaac98f26a72bc15e0b5ef185431362698e6a1715a6768070f6216f1380dd1a643ee772c6b93b900afa8f05dccefabb46895402988e39b66839b5c4b0c53857d3ba67ecc279a29351b84677f98e23fca658413c0368227ce74ed9a4fc4918495312126514922b0e361df5c9cb8c4e44b418b885cb812d27958635864dcdaef25c82dcdec31b200a14e56e54de368738e83e8bf94dfe930b23d8544bc72f1a6ef0a74d2e8e84c786fbf01f48f30263d7a080a33fd7d9eeb432316127fed2bedc060ff55494ecc281a92675bde4f42ec73db25c1023c8fb616a78144518a240cf1873f4d7c61200e70e21d165c2bca5c50a0ab60bf4ccf4738f506e7110feb6c6e9782a5036a800e41c128c7c75aa59207d92e1e4ef0b83e3177c12fbe056c332b9db9b5903257c9b113812a485622a6e0fb7add7be65539d195af090288400df1ea795dd08222f2954e811742d3d8d11ad70196a695871ca39ec9e42c1c96bda5c4a03098afb7d46fc90bd9d0e62b2ea1b631dca3dcb9e47f6bbe17e26c04f15fc855120d843763d13972a78988445924a59b361591f249e4c95f1ecc6b994d107a7f47eed6aa4d88bc9bf259da17a7cef6c6aa4331211c56d4252f6ee1e7c7451ae30914c3e78aab657f4194226c79ba19a9569d9ad2eb88ce9d3f19384ae58619d655aefddcf44a1532ce9697368a42aa78b2c13361b323718e19636001ef8d83a78e5f956b9044828db98ef283524ea4e2f4f1564eff7e7ee9eec80d80372d2bae41da1910055e8a0b50b53c1474c188cface6edb9092f7caf99cf354260f95fc93151342113084192acaffe0001a147010700fd05b1fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac5900000000000000050000000001312d0036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e96800061b0f0002c711f0fdd00dc0d59cc9744be7844b230c3e6d896640c686f7d1c93fd005fc90d43ca381ddcc524d7c924ded0fddff49e674ff3fe851d356b17c73e361d7326917281c6ada5dee7b967504f0b3e030963bf12ff9449a0fdbd53a9248b58339507e29e19f5c2591970d6afb7abd49ca61222862d902a0de2417f471bc497842848a897cc7d3bb19c03816c0575b26d5b638fce6f15f0b546614748ec5d0e14bfbb2f5e4e2d65d17f98481b972066a3b7b97bd5e587ccebfc6e7e3d6885ebc9c6c0e09d3d4ddb58b9043fbccf297bd2a9783717a31ed0b39be3c14297a4970fe0a7f1154781cf315d9e4fff61edd2d47c32f1eed011860a4cc2c0e057f35afa6c92529be18b195fb0fd7577042e3f31d115571579959fd329c92fe0d68df2db844ab8296e95f5393810b2744fe0324c118e70c26cd912f4296d7bc2199e93ba20907f5713088d779e5177f16043baf8d59f4208229817acae5512f6eec8e9680204593c67508bb8246f99da80e53cf74147a911ef6891b1d61fcc43135e9a3b7d875faacbd051d277b275000f284cec9499a87a3ae5c62327b707e50cab7d7587b6c9aab7b556df9d3ab865e544615ad397d67063636c9d675a092ddf6ebf00110ec728ca8fa686882cd18fd6abc46f57978cda0d95fcf075c1b35cf08b151f6779b85f9a0ab7da7eae71ed0f8245372b7feb81b4d243a153b7330f90cddc9f18763373f4727780b2563899bdc5b8f79903c5a00cb1862e91981360c776ab503e46749344e5cda444c9a6bcce40aa2b4f0f5ea066a20171f9cfea53cf58fe426246acab1f5326b06e8baff1f2d76976e8a4d6c9001b7a2e8f739df41d80f54e965b5cfb80d811c31bc32b0d91041bf28f45aa5a6ab15abe6b44e9f27dc980e119f2865731e9c5dac0161bc3a320b30467fdc6cda7d105fce503dcf6b62d8e8dd61457b401b849990049926d3715b2131eed48652ce60ce3dd4eaebc7d19b0a35d4c561c2a488d1ec0d480ebaad47ff63987331b882842b36712e6ca9581532251e0ffd7647d62295b33bea95818d04ee37f5b7695b85dd28700c8d5dad87aa2786006590796c354804810f277404c2437c2b70de2d2b10ed91ec4305e80a2f18666c791039c0c3308884012a8a2c92ab76f69c0404ae784a9f748faae201b2a58db8598938978a159e14dfd05c2d10db3d6072274f5f92b7f8cb1d30fe73842272ab6d2bf3901b81e85ef6b1e0a4f0821cd25be5e6021f6702ee804f2ae51382110173a9b206f9340b891985c36eb64af00eaec166507524bc7a2b32ce1e8de40ec127cf8c3f38dbdb4e483e1869faf6513a131853c11e9bf1717f45d3af8be50eee21c8768adeadd9d79df49c862a0bcd1a2938ab43909ac155febd48468285266148c8dada96d85065251f18830299b8383fe694b9561d32a59e3a5f4a95324cb6ef2760abc947351d19882fc671d7ba36510139a186308b8d7ca5ce5c430860e21cb6c218452740e1a30f79541899bca6cd13414e003e847778171bf8ddae4ed9ad9dbc0ed6dacbb603d8f4255745ce5247508222c0d8f46a4a517e6ee30b02a7e690b7bc45e5d260bd4e7d0d27131f088351f116d85eb9e472d01d78d3440a3b6dc1d3da2a3c6ee57e78dd2f2138979c880df9cd94574929895e361a4d43fec2f5f4890eefad4bac1ef4d244d64641e600e4102c20921901b903d0ba382995aeb027906c0f1283315ad454f986734932fead46a7e06edc5ef3817f7b94a831fff268ab255aa251ad08df630790b100b153b809d31b68e6080586d4d9b271b36c67c86a68f4a9034658b7fe8cb7ba94af8c0503ba3df909a9ee8e394bde5e701013f92e3f45c2e67f27e8fbc206721430ba9aa3657ecb9dbf0691bfbefbcf3ee20981d06d8977a7c401f1bd1187d3abbe3c3e9ffe0001a147010700010000000000000000000000000322298336489c06b5e7fc7da4d6dc4f1ffd2a59b06bcc5a6e81feff828cb841d00a5e020000000101010101010101010101010101010101010101010101010101010101010101012a00000000ffffffff0140420f0000000000220020520f04b40b1ec2c985ede593cfff212cac1088275f72e9158a4d05e7f95f0c8800000000061a8000002a000000000200000000000000070003000000000000000003000000000000000004000000000000000005000009c40000000008f0d180000000002faf080024fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac59000000002b40420f0000000000220020520f04b40b1ec2c985ede593cfff212cac1088275f72e9158a4d05e7f95f0c8800fd01600200000001fbbf4f2af338d22194a95c045dd36c1eaf9c090f6668f85e9976a8cb2418ac590000000000da4aaf80074a01000000000000220020f76d112d2b74c50fe65bb3f151b3c98b737e72f02125dd3b0826baa5b596263c4a01000000000000220020ffdacc7e0491932fe22a1411c331e3588ead7849eb1fb2e11318656f276cb93e983a0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf983a0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf204e0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf58370200000000002200207ce427f9be2c35185ab7151f477327acb20657856b69cc652c1a802b6254ac3600350c0000000000220020c76babcbc3844154035e3c5b4138a4f20c06dff53dd04810a1d916f800d96599b2fa742006e35027e3a48a8c39f686e51c10c648d15d6c0543fa6328f78c7453bc09d8504e8883c2fc83471f799ce1f57b9591b9b4a95dc0cf9cb1ed878e0d67de7bf40200031324dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c6020000002b983a0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf005e0200000001dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c602000000000100000001983a0000000000002200207ce427f9be2c35185ab7151f477327acb20657856b69cc652c1a802b6254ac36101b060036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e968000000000000000300061b1065cffa80c72f9e291456fabc3771865f4993108f8a90deddf041a9bcd523d3ec2d5361430705447f87312b2b858327f5f0c4999b7bdd7da84dd270189304def11324dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c6030000002b983a0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf005e0200000001dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c603000000000100000001983a0000000000002200207ce427f9be2c35185ab7151f477327acb20657856b69cc652c1a802b6254ac36101b060036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e968000000000000000400061b10c5930b24a9e50aa0680daf5ab04d82e21fe1edfaf2455e07e08484660cf4c12d384c22a3aa8b32fad23ba7f451ea718c718439b0b256b120f22eeadc5b995f491324dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c6040000002b204e0000000000002200200af572135c9b725f663119cbeedbb7726c42878179f5f52d798d790ec9034abf005e0200000001dc5602643922fb60fb2c94970db12260850742e9c0f9f026bf28119fcf67e9c604000000000100000001204e0000000000002200207ce427f9be2c35185ab7151f477327acb20657856b69cc652c1a802b6254ac360f1b060036e6f4bd1403580346f7ad969d0b45f035220481207ddf9e43cbf0b49014e968000000000000000500061b0f03821c3180261d81126c2157282ea17be5131081c2d27c34f55a4000d06fc12b1a00bc7750d6e8d6765d1d53aae75cef20be8117cf7fc5c91c0ba017d3e214b100000000000000070003ff0000000000000003ff0000000000000004ff0000000000000005000009c4000000002faf08000000000008f0d1807a4b393c2c3f0677b66dec80d334b84644151a4373bba21436e873eeeebe9a61022ecd4622a2074bdf0ea45fc77632342eba413ae5a99afa1e57884176212716cb000000ff02557c4e960b3d9d9e0f73945a59a7cbad0f6f1b0ef9702d4e1b363a333c0362770003003e0000fffffffffffc0083c14c2553d85d117442efb0e845fdce17d61c8ab7215214f19b8857f57fa44a7800fc0003ffffffffffe80100830bc65499bcd444ee2554dc8a2e077dddf4caddea225a3383042e7edad3d11002000007ffffffffffc801022bb1088890b62d59d04fdd3907aa1f4a566019c687c4fd5c6eddde7b1bb746dc0003ffffffffffe40003000000000000000300038ee783097f5141649678366bf8596ed7000000000000000400032db7f6ee8f34459f84965d1a0bec8b8d000000000000000500035c4ac1741ba34f978d316fa4b05d974b00000101460f3d693d6660ff01e5e5b0cbb3ce11008877f608d74589ff7ee0e6cc675de4deeb42cba8e0a1fdd4ced5c0c51a98536631639717aa11043a699d13b2e98960ccc523109349f8ef0f9b0d9c74d84fa51d0806226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f01460f3d693d666068414d270300009000000000000003e8000858b800000014000000001dcd650000000001") + val decoded = channelDataCodec.decode(bin.bits).require.value.asInstanceOf[DATA_NORMAL] + val localCommit = decoded.commitments.latest.localCommit + assert(localCommit.index == 7) + assert(localCommit.txId == TxId.fromValidHex("c6e967cf9f1128bf26f0f9c0e94207856022b10d97942cfb60fb2239640256dc")) + assert(localCommit.input == InputInfo(OutPoint(TxId.fromValidHex("59ac1824cba876995ef868660f099caf1e6cd35d045ca99421d238f32a4fbffb"), 0), TxOut(1_000_000 sat, hex"0020520f04b40b1ec2c985ede593cfff212cac1088275f72e9158a4d05e7f95f0c88"), ByteVector.empty)) + val htlcRemoteSigs = List( + ByteVector64(hex"65cffa80c72f9e291456fabc3771865f4993108f8a90deddf041a9bcd523d3ec2d5361430705447f87312b2b858327f5f0c4999b7bdd7da84dd270189304def1"), + ByteVector64(hex"c5930b24a9e50aa0680daf5ab04d82e21fe1edfaf2455e07e08484660cf4c12d384c22a3aa8b32fad23ba7f451ea718c718439b0b256b120f22eeadc5b995f49"), + ByteVector64(hex"03821c3180261d81126c2157282ea17be5131081c2d27c34f55a4000d06fc12b1a00bc7750d6e8d6765d1d53aae75cef20be8117cf7fc5c91c0ba017d3e214b1"), + ) + assert(localCommit.htlcRemoteSigs == htlcRemoteSigs) + } + test("decode local params pay commit tx fees field") { // The data in this test was encoded using eclair v0.10.0, where a single is_initiator boolean was encoded instead // of two separate booleans (is_channel_opener and pay_commit_tx_fees). From 3c3b94a143642bab773ad3cc9f108f1a45519e75 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 6 Jun 2025 16:05:28 +0200 Subject: [PATCH 3/5] move InputInfo from LocalCommit to Commitment We actually only add `localFundingPubKey` and a few other fields, and compute the `commitInput`. This saves 33 bytes because `remoteFundingPubKey` is now only persisted once. --- .../fr/acinq/eclair/channel/Commitments.scala | 38 ++++++++++++------- .../fr/acinq/eclair/channel/fsm/Channel.scala | 2 +- .../channel/fsm/ChannelOpenDualFunded.scala | 4 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 14 +++++-- .../channel/fund/InteractiveTxBuilder.scala | 19 ++++++---- .../channel/version0/ChannelTypes0.scala | 8 +++- .../channel/version3/ChannelTypes3.scala | 17 +++++++-- .../channel/version4/ChannelCodecs4.scala | 1 - 8 files changed, 68 insertions(+), 35 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 4be79bd720..2764686281 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.crypto.musig2.IndividualNonce import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, OutPoint, Satoshi, SatoshiLong, Transaction, TxId} import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw, FeeratesPerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} @@ -186,7 +186,7 @@ object ChannelSpendSignature { * The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. * The [[htlcRemoteSigs]] are stored in the order in which HTLC outputs appear in the commitment transaction. */ -case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64]) +case class LocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, remoteSig: ChannelSpendSignature, htlcRemoteSigs: List[ByteVector64]) object LocalCommit { def fromCommitSig(params: ChannelParams, commitKeys: LocalCommitmentKeys, fundingTxId: TxId, @@ -215,7 +215,7 @@ object LocalCommit { } remoteSig } - Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, localCommitTx.input, commitTxRemoteSig, htlcRemoteSigs)) + Right(LocalCommit(localCommitIndex, spec, localCommitTx.tx.txid, commitTxRemoteSig, htlcRemoteSigs)) } } @@ -265,13 +265,19 @@ case class CommitTxIds(localCommitTxId: TxId, remoteCommitTxId: TxId, nextRemote */ case class Commitment(fundingTxIndex: Long, firstRemoteCommitIndex: Long, + localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, + fundingTxOutpoint: OutPoint, + fundingAmount: Satoshi, localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, + format: CommitmentFormat, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { - val commitInput: InputInfo = localCommit.input - val fundingTxId: TxId = commitInput.outPoint.txid + + lazy val commitInput: InputInfo = Helpers.Funding.makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, fundingAmount, localFundingPubKey, remoteFundingPubKey, format) + val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) - val capacity: Satoshi = commitInput.txOut.amount + val fundingTxId: TxId = fundingTxOutpoint.txid + val capacity: Satoshi = fundingAmount /** Once the funding transaction is confirmed, short_channel_id matching this transaction. */ val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match { case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId) @@ -691,7 +697,7 @@ case class Commitment(fundingTxIndex: Long, def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = localKeys(params, channelKeys) - val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec) + val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput, localCommit.spec) localCommit.remoteSig match { case remoteSig: ChannelSpendSignature.IndividualSignature => val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey) @@ -709,7 +715,7 @@ case class Commitment(fundingTxIndex: Long, /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ def htlcTxs(params: ChannelParams, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = { - val (_, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, localCommit.input, localCommit.spec) + val (_, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput, localCommit.spec) htlcTxs.sortBy(_.input.outPoint.index).zip(localCommit.htlcRemoteSigs) } @@ -754,8 +760,12 @@ case class AnnouncedCommitment(commitment: Commitment, announcement: ChannelAnno case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, fundingTxIndex: Long, firstRemoteCommitIndex: Long, + localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, + fundingTxOutpoint: OutPoint, + fundingAmount: Satoshi, localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, + format: CommitmentFormat, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { val channelId: ByteVector32 = params.channelId val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match { @@ -764,11 +774,11 @@ case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, } val localParams: LocalParams = params.localParams val remoteParams: RemoteParams = params.remoteParams - val commitInput: InputInfo = localCommit.input - val fundingTxId: TxId = commitInput.outPoint.txid + val fundingTxId: TxId = fundingTxOutpoint.txid val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) - val capacity: Satoshi = commitInput.txOut.amount - val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localFundingStatus, remoteFundingStatus, localCommit, remoteCommit, nextRemoteCommit_opt) + val capacity: Satoshi = fundingAmount + val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, localFundingPubKey, remoteFundingPubKey, fundingTxOutpoint, fundingAmount, localFundingStatus, remoteFundingStatus, format, localCommit, remoteCommit, nextRemoteCommit_opt) + lazy val commitInput: InputInfo = commitment.commitInput def localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = commitment.localKeys(params, channelKeys) @@ -844,7 +854,7 @@ case class Commitments(params: ChannelParams, val all: Seq[Commitment] = active ++ inactive // We always use the last commitment that was created, to make sure we never go back in time. - val latest: FullCommitment = FullCommitment(params, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.remoteFundingPubKey, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt) + val latest: FullCommitment = FullCommitment(params, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.localFundingPubKey, active.head.remoteFundingPubKey, active.head.fundingTxOutpoint, active.head.fundingAmount, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.format, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt) val lastLocalLocked_opt: Option[Commitment] = active.filter(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).sortBy(_.fundingTxIndex).lastOption val lastRemoteLocked_opt: Option[Commitment] = active.filter(c => c.remoteFundingStatus == RemoteFundingStatus.Locked).sortBy(_.fundingTxIndex).lastOption @@ -1319,7 +1329,7 @@ case class Commitments(params: ChannelParams, * @param spendingTx A transaction that may spend a current or former funding tx */ def resolveCommitment(spendingTx: Transaction): Option[Commitment] = { - all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.commitInput.outPoint)) + all.find(c => spendingTx.txIn.map(_.outPoint).contains(c.fundingTxOutpoint)) } /** Find the corresponding commitment based on its short_channel_id (once funding transaction is confirmed). */ 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 7694baba61..b427046971 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 @@ -1369,7 +1369,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.spliceStatus match { case SpliceStatus.SpliceWaitingForSigs(signingSession) => // we have not yet sent our tx_signatures - signingSession.receiveTxSigs(channelKeys, msg, nodeParams.currentBlockHeight) match { + signingSession.receiveTxSigs(d.commitments.params, channelKeys, msg, nodeParams.currentBlockHeight) match { case Left(f) => rollbackFundingAttempt(signingSession.fundingTx.tx, previousTxs = Seq.empty) // no splice rbf yet stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, f.getMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index 96d87085b1..a6beb2b0c8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -400,7 +400,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case Event(msg: InteractiveTxMessage, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) => msg match { case txSigs: TxSignatures => - d.signingSession.receiveTxSigs(channelKeys, txSigs, nodeParams.currentBlockHeight) match { + d.signingSession.receiveTxSigs(d.channelParams, channelKeys, txSigs, nodeParams.currentBlockHeight) match { case Left(f) => rollbackFundingAttempt(d.signingSession.fundingTx.tx, Nil) goto(CLOSED) sending Error(d.channelId, f.getMessage) @@ -468,7 +468,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { case _: FullySignedSharedTransaction => d.status match { case DualFundingStatus.RbfWaitingForSigs(signingSession) => - signingSession.receiveTxSigs(channelKeys, txSigs, nodeParams.currentBlockHeight) match { + signingSession.receiveTxSigs(d.channelParams, channelKeys, txSigs, nodeParams.currentBlockHeight) match { case Left(f) => rollbackRbfAttempt(signingSession, d) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, f.getMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 09ef1cbee6..a1bf236c18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe -import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} +import fr.acinq.bitcoin.scalacompat.{OutPoint, SatoshiLong, Script} import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.Funding @@ -284,10 +284,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, + localFundingPubKey = fundingKey.publicKey, remoteFundingPubKey = remoteFundingPubKey, + fundingTxOutpoint = localCommitTx.input.outPoint, + fundingAmount = localCommitTx.input.txOut.amount, localFundingStatus = SingleFundedUnconfirmedFundingTx(None), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), + format = params.commitmentFormat, + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), remoteCommit = RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), nextRemoteCommit_opt = None) val commitments = Commitments( @@ -329,10 +333,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, + localFundingPubKey = fundingPubkey, remoteFundingPubKey = remoteFundingPubKey, + fundingTxOutpoint = localCommitTx.input.outPoint, + fundingAmount = localCommitTx.input.txOut.amount, localFundingStatus = SingleFundedUnconfirmedFundingTx(Some(fundingTx)), remoteFundingStatus = RemoteFundingStatus.NotLocked, - localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, localCommitTx.input, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), + format = params.commitmentFormat, + localCommit = LocalCommit(0, localSpec, localCommitTx.tx.txid, ChannelSpendSignature.IndividualSignature(remoteSig), htlcRemoteSigs = Nil), remoteCommit = remoteCommit, nextRemoteCommit_opt = None ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index dd6aec7db6..ed489af77e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -856,16 +856,16 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon val localSigOfRemoteTx = remoteCommitTx.sign(localFundingKey, fundingParams.remoteFundingPubKey).sig val htlcSignatures = sortedHtlcTxs.map(_.sign(remoteCommitmentKeys, channelParams.commitmentFormat)).toList val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) - val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid, localCommitTx.input) + val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx.tx.txid) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) - signFundingTx(completeTx, localCommitSig, localCommit, remoteCommit) + signFundingTx(completeTx, localCommitTx.input, localCommitSig, localCommit, remoteCommit) case _: SimpleTaprootChannelCommitmentFormat => ??? } } } - private def signFundingTx(completeTx: SharedTransaction, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { + private def signFundingTx(completeTx: SharedTransaction, commitInput: InputInfo, commitSig: CommitSig, localCommit: UnsignedLocalCommit, remoteCommit: RemoteCommit): Behavior[Command] = { signTx(completeTx) Behaviors.receiveMessagePartial { case SignTransactionResult(signedTx) => @@ -897,6 +897,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon fundingParams, purpose.fundingTxIndex, signedTx, + commitInput, Left(localCommit), remoteCommit, liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator)) @@ -1020,7 +1021,7 @@ object InteractiveTxSigningSession { // +-------+ +-------+ /** A local commitment for which we haven't received our peer's signatures. */ - case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId, input: InputInfo) + case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, txId: TxId) private def shouldSignFirst(isInitiator: Boolean, channelParams: ChannelParams, tx: SharedTransaction): Boolean = { val sharedAmountIn = tx.sharedInput_opt.map(_.txOut.amount).getOrElse(0 sat) @@ -1092,10 +1093,11 @@ object InteractiveTxSigningSession { case class WaitingForSigs(fundingParams: InteractiveTxParams, fundingTxIndex: Long, fundingTx: PartiallySignedSharedTransaction, + commitInput: InputInfo, localCommit: Either[UnsignedLocalCommit, LocalCommit], remoteCommit: RemoteCommit, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession { - val commitInput: InputInfo = localCommit.fold(_.input, _.input) + val fundingTxOutpoint: OutPoint = OutPoint(fundingTx.txId, fundingTx.tx.buildUnsignedTx().txOut.indexWhere(_.publicKeyScript == fundingTx.tx.sharedOutput.pubkeyScript)) val localCommitIndex: Long = localCommit.fold(_.index, _.index) // This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not. val nextLocalCommitmentNumber: Long = localCommit match { @@ -1111,7 +1113,7 @@ object InteractiveTxSigningSession { LocalCommit.fromCommitSig(channelParams, commitKeys, fundingTx.txId, fundingKey, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec).map { signedLocalCommit => if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) SendingSigs(fundingStatus, commitment, fundingTx.localSigs) } else { this.copy(localCommit = Right(signedLocalCommit)) @@ -1123,7 +1125,7 @@ object InteractiveTxSigningSession { } } - def receiveTxSigs(channelKeys: ChannelKeys, remoteTxSigs: TxSignatures, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, SendingSigs] = { + def receiveTxSigs(channelParams: ChannelParams, channelKeys: ChannelKeys, remoteTxSigs: TxSignatures, currentBlockHeight: BlockHeight)(implicit log: LoggingAdapter): Either[ChannelException, SendingSigs] = { localCommit match { case Left(_) => log.info("received tx_signatures before commit_sig") @@ -1135,8 +1137,9 @@ object InteractiveTxSigningSession { Left(f) case Right(fullySignedTx) => log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length) + val localFundingPubKey = channelKeys.fundingKey(fundingTxIndex).publicKey val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, localFundingPubKey, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs)) } } 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 5fe64865bd..d706608336 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.{CltvExpiryDelta, Features, InitFeature, MilliSatoshi, UInt64, channel} +import fr.acinq.eclair.{CltvExpiryDelta, Features, InitFeature, MilliSatoshi, UInt64, channel, randomKey} import scodec.bits.{BitVector, ByteVector} private[channel] object ChannelTypes0 { @@ -108,7 +108,7 @@ private[channel] object ChannelTypes0 { val remoteSig = extractRemoteSig(publishableTxs.commitTx, remoteFundingPubKey) val unsignedCommitTx = publishableTxs.commitTx.copy(tx = removeWitnesses(publishableTxs.commitTx.tx)) val htlcRemoteSigs = publishableTxs.htlcTxsAndSigs.map(_.remoteSig) - channel.LocalCommit(index, spec, unsignedCommitTx.tx.txid, unsignedCommitTx.input, remoteSig, htlcRemoteSigs) + channel.LocalCommit(index, spec, unsignedCommitTx.tx.txid, remoteSig, htlcRemoteSigs) } private def extractRemoteSig(commitTx: CommitTx, remoteFundingPubKey: PublicKey): ChannelSpendSignature.IndividualSignature = { @@ -220,11 +220,15 @@ private[channel] object ChannelTypes0 { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, + localFundingPubKey = randomKey().publicKey, remoteFundingPubKey = remoteParams.fundingPubKey, + fundingTxOutpoint = localCommit.publishableTxs.commitTx.input.outPoint, + fundingAmount = localCommit.publishableTxs.commitTx.input.txOut.amount, // We set an empty funding tx, even if it may be confirmed already (and the channel fully operational). We could // have set a specific Unknown status, but it would have forced us to keep it forever. We will retrieve the // funding tx when the channel is instantiated, and update the status (possibly immediately if it was confirmed). LocalFundingStatus.SingleFundedUnconfirmedFundingTx(None), RemoteFundingStatus.Locked, + format = channelFeatures.commitmentFormat, localCommit.migrate(remoteParams.fundingPubKey), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)) ) channel.Commitments( 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 53367cfeaf..00871f58aa 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} -import fr.acinq.eclair.channel +import fr.acinq.eclair.{channel, randomKey} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession import fr.acinq.eclair.crypto.ShaChain @@ -38,11 +38,11 @@ private[channel] object ChannelTypes3 { // Before version 4, we stored the unsigned commit tx and htlc txs in our local commit. // We changed that to only store the remote signatures and re-compute transactions on-the-fly when force-closing. case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig]) { - def migrate(): channel.LocalCommit = channel.LocalCommit(index, spec, commitTxAndRemoteSig.commitTx.tx.txid, commitTxAndRemoteSig.commitTx.input, commitTxAndRemoteSig.remoteSig, htlcTxsAndRemoteSigs.map(_.remoteSig)) + def migrate(): channel.LocalCommit = channel.LocalCommit(index, spec, commitTxAndRemoteSig.commitTx.tx.txid, commitTxAndRemoteSig.remoteSig, htlcTxsAndRemoteSigs.map(_.remoteSig)) } case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx]) { - def migrate(): InteractiveTxSigningSession.UnsignedLocalCommit = InteractiveTxSigningSession.UnsignedLocalCommit(index, spec, commitTx.tx.txid, commitTx.input) + def migrate(): InteractiveTxSigningSession.UnsignedLocalCommit = InteractiveTxSigningSession.UnsignedLocalCommit(index, spec, commitTx.tx.txid) } // Before version4, we didn't support multiple active commitments, which were later introduced by dual funding and splicing. @@ -62,7 +62,16 @@ private[channel] object ChannelTypes3 { def migrate(): channel.Commitments = channel.Commitments( ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams.migrate(), channelFlags), CommitmentChanges(localChanges, remoteChanges, localNextHtlcId, remoteNextHtlcId), - Seq(Commitment(fundingTxIndex = 0, firstRemoteCommitIndex = 0, remoteFundingPubKey = remoteParams.fundingPubKey, localFundingStatus, remoteFundingStatus, localCommit.migrate(), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), + Seq(Commitment( + fundingTxIndex = 0, + firstRemoteCommitIndex = 0, + localFundingPubKey = randomKey().publicKey, + remoteFundingPubKey = remoteParams.fundingPubKey, + fundingTxOutpoint = localCommit.commitTxAndRemoteSig.commitTx.input.outPoint, + fundingAmount = localCommit.commitTxAndRemoteSig.commitTx.input.txOut.amount, + localFundingStatus, remoteFundingStatus, + format = channelFeatures.commitmentFormat, + localCommit.migrate(), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), inactive = Nil, remoteNextCommitInfo.fold(w => Left(WaitForRev(w.sentAfterLocalCommitIndex)), remotePerCommitmentPoint => Right(remotePerCommitmentPoint)), remotePerCommitmentSecrets, 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 4e3fed1ac5..7898c84632 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 @@ -489,7 +489,6 @@ private[channel] object ChannelCodecs4 { ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("txId" | txId) :: - ("input" | inputInfoCodec) :: ("remoteSig" | channelSpendSignatureCodec) :: ("htlcRemoteSigs" | listOfN(uint16, bytes64))).as[LocalCommit] From 414ab1ff38d617d8d7f578b384fbc2c5522d4d6c Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 6 Jun 2025 16:25:28 +0200 Subject: [PATCH 4/5] simplified FullCommitment --- .../fr/acinq/eclair/channel/Commitments.scala | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 2764686281..3ce76fd2bc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -757,28 +757,24 @@ case class AnnouncedCommitment(commitment: Commitment, announcement: ChannelAnno } /** Subset of Commitments when we want to work with a single, specific commitment. */ -case class FullCommitment(params: ChannelParams, changes: CommitmentChanges, - fundingTxIndex: Long, - firstRemoteCommitIndex: Long, - localFundingPubKey: PublicKey, - remoteFundingPubKey: PublicKey, - fundingTxOutpoint: OutPoint, - fundingAmount: Satoshi, - localFundingStatus: LocalFundingStatus, remoteFundingStatus: RemoteFundingStatus, - format: CommitmentFormat, - localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { +case class FullCommitment(params: ChannelParams, + changes: CommitmentChanges, + commitment: Commitment) { val channelId: ByteVector32 = params.channelId - val shortChannelId_opt: Option[RealShortChannelId] = localFundingStatus match { - case f: LocalFundingStatus.ConfirmedFundingTx => Some(f.shortChannelId) - case _ => None - } + val shortChannelId_opt: Option[RealShortChannelId] = commitment.shortChannelId_opt val localParams: LocalParams = params.localParams val remoteParams: RemoteParams = params.remoteParams - val fundingTxId: TxId = fundingTxOutpoint.txid - val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) - val capacity: Satoshi = fundingAmount - val commitment: Commitment = Commitment(fundingTxIndex, firstRemoteCommitIndex, localFundingPubKey, remoteFundingPubKey, fundingTxOutpoint, fundingAmount, localFundingStatus, remoteFundingStatus, format, localCommit, remoteCommit, nextRemoteCommit_opt) - lazy val commitInput: InputInfo = commitment.commitInput + val fundingTxId: TxId = commitment.fundingTxOutpoint.txid + val commitTxIds: CommitTxIds = commitment.commitTxIds + val fundingTxIndex: Long = commitment.fundingTxIndex + val capacity: Satoshi = commitment.capacity + val remoteFundingPubKey: PublicKey = commitment.remoteFundingPubKey + val commitInput: InputInfo = commitment.commitInput + val localCommit: LocalCommit = commitment.localCommit + val remoteCommit: RemoteCommit = commitment.remoteCommit + val nextRemoteCommit_opt: Option[NextRemoteCommit] = commitment.nextRemoteCommit_opt + val localFundingStatus: LocalFundingStatus = commitment.localFundingStatus + val remoteFundingStatus: RemoteFundingStatus = commitment.remoteFundingStatus def localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = commitment.localKeys(params, channelKeys) @@ -854,7 +850,7 @@ case class Commitments(params: ChannelParams, val all: Seq[Commitment] = active ++ inactive // We always use the last commitment that was created, to make sure we never go back in time. - val latest: FullCommitment = FullCommitment(params, changes, active.head.fundingTxIndex, active.head.firstRemoteCommitIndex, active.head.localFundingPubKey, active.head.remoteFundingPubKey, active.head.fundingTxOutpoint, active.head.fundingAmount, active.head.localFundingStatus, active.head.remoteFundingStatus, active.head.format, active.head.localCommit, active.head.remoteCommit, active.head.nextRemoteCommit_opt) + val latest: FullCommitment = FullCommitment(params, changes, active.head) val lastLocalLocked_opt: Option[Commitment] = active.filter(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).sortBy(_.fundingTxIndex).lastOption val lastRemoteLocked_opt: Option[Commitment] = active.filter(c => c.remoteFundingStatus == RemoteFundingStatus.Locked).sortBy(_.fundingTxIndex).lastOption From 173cc10a635151af63db140376db907ebf067971 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 6 Jun 2025 17:48:34 +0200 Subject: [PATCH 5/5] remove localFundingPubKey from Commitment It is now computed from `fundingTxIndex` + `ChannelKeys`. --- .../fr/acinq/eclair/channel/Commitments.scala | 34 +++++--- .../fr/acinq/eclair/channel/Helpers.scala | 31 ++++--- .../fr/acinq/eclair/channel/fsm/Channel.scala | 24 +++--- .../channel/fsm/ChannelOpenDualFunded.scala | 4 +- .../channel/fsm/ChannelOpenSingleFunded.scala | 2 - .../channel/fsm/CommonFundingHandlers.scala | 4 +- .../eclair/channel/fsm/ErrorHandlers.scala | 18 ++-- .../channel/fund/InteractiveTxBuilder.scala | 9 +- .../channel/publish/ReplaceableTx.scala | 2 +- .../acinq/eclair/json/JsonSerializers.scala | 4 +- .../channel/version0/ChannelTypes0.scala | 1 - .../channel/version3/ChannelTypes3.scala | 17 ++-- .../channel/version4/ChannelCodecs4.scala | 84 +++++++++++++------ 13 files changed, 137 insertions(+), 97 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 3ce76fd2bc..48d18e3b97 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -265,7 +265,6 @@ case class CommitTxIds(localCommitTxId: TxId, remoteCommitTxId: TxId, nextRemote */ case class Commitment(fundingTxIndex: Long, firstRemoteCommitIndex: Long, - localFundingPubKey: PublicKey, remoteFundingPubKey: PublicKey, fundingTxOutpoint: OutPoint, fundingAmount: Satoshi, @@ -273,8 +272,6 @@ case class Commitment(fundingTxIndex: Long, format: CommitmentFormat, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[NextRemoteCommit]) { - lazy val commitInput: InputInfo = Helpers.Funding.makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, fundingAmount, localFundingPubKey, remoteFundingPubKey, format) - val commitTxIds: CommitTxIds = CommitTxIds(localCommit.txId, remoteCommit.txId, nextRemoteCommit_opt.map(_.commit.txId)) val fundingTxId: TxId = fundingTxOutpoint.txid val capacity: Satoshi = fundingAmount @@ -284,6 +281,11 @@ case class Commitment(fundingTxIndex: Long, case _ => None } + def commitInput(channelKeys: ChannelKeys): InputInfo = + Helpers.Funding.makeFundingInputInfo(fundingTxOutpoint.txid, fundingTxOutpoint.index.toInt, fundingAmount, localFundingKey(channelKeys).publicKey, remoteFundingPubKey, format) + + def localFundingKey(channelKeys: ChannelKeys): PrivateKey = channelKeys.fundingKey(fundingTxIndex) + def localKeys(params: ChannelParams, channelKeys: ChannelKeys): LocalCommitmentKeys = LocalCommitmentKeys(params, channelKeys, localCommit.index) def remoteKeys(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = RemoteCommitmentKeys(params, channelKeys, remotePerCommitmentPoint) @@ -655,7 +657,7 @@ case class Commitment(fundingTxIndex: Long, // remote commitment will include all local proposed changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val fundingKey = channelKeys.fundingKey(fundingTxIndex) - val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput, spec) + val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(params, commitKeys, remoteCommit.index + 1, fundingKey, remoteFundingPubKey, commitInput(channelKeys), spec) val htlcSigs = htlcTxs.sortBy(_.input.outPoint.index).map(_.sign(commitKeys, params.commitmentFormat)) // NB: IN/OUT htlcs are inverted because this is the remote commit @@ -687,7 +689,7 @@ case class Commitment(fundingTxIndex: Long, val localCommitIndex = localCommit.index + 1 val fundingKey = channelKeys.fundingKey(fundingTxIndex) val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) - LocalCommit.fromCommitSig(params, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec).map { localCommit1 => + LocalCommit.fromCommitSig(params, commitKeys, fundingTxId, fundingKey, remoteFundingPubKey, commitInput(channelKeys), commit, localCommitIndex, spec).map { localCommit1 => log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.txId} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(",")) copy(localCommit = localCommit1) } @@ -697,7 +699,7 @@ case class Commitment(fundingTxIndex: Long, def fullySignedLocalCommitTx(params: ChannelParams, channelKeys: ChannelKeys): Transaction = { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = localKeys(params, channelKeys) - val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput, localCommit.spec) + val (unsignedCommitTx, _) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(channelKeys), localCommit.spec) localCommit.remoteSig match { case remoteSig: ChannelSpendSignature.IndividualSignature => val localSig = unsignedCommitTx.sign(fundingKey, remoteFundingPubKey) @@ -708,14 +710,14 @@ case class Commitment(fundingTxIndex: Long, /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ def htlcTxs(params: ChannelParams, channelKeys: ChannelKeys): Seq[(HtlcTx, ByteVector64)] = { - val fundingKey = channelKeys.fundingKey(fundingTxIndex) val commitKeys = localKeys(params, channelKeys) - htlcTxs(params, fundingKey, commitKeys) + htlcTxs(params, channelKeys, commitKeys) } /** Return the HTLC transactions for our local commit and the corresponding remote signatures. */ - def htlcTxs(params: ChannelParams, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = { - val (_, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput, localCommit.spec) + def htlcTxs(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val (_, htlcTxs) = Commitment.makeLocalTxs(params, commitKeys, localCommit.index, fundingKey, remoteFundingPubKey, commitInput(channelKeys), localCommit.spec) htlcTxs.sortBy(_.input.outPoint.index).zip(localCommit.htlcRemoteSigs) } @@ -764,18 +766,22 @@ case class FullCommitment(params: ChannelParams, val shortChannelId_opt: Option[RealShortChannelId] = commitment.shortChannelId_opt val localParams: LocalParams = params.localParams val remoteParams: RemoteParams = params.remoteParams + val fundingTxOutpoint: OutPoint = commitment.fundingTxOutpoint val fundingTxId: TxId = commitment.fundingTxOutpoint.txid val commitTxIds: CommitTxIds = commitment.commitTxIds val fundingTxIndex: Long = commitment.fundingTxIndex val capacity: Satoshi = commitment.capacity val remoteFundingPubKey: PublicKey = commitment.remoteFundingPubKey - val commitInput: InputInfo = commitment.commitInput val localCommit: LocalCommit = commitment.localCommit val remoteCommit: RemoteCommit = commitment.remoteCommit val nextRemoteCommit_opt: Option[NextRemoteCommit] = commitment.nextRemoteCommit_opt val localFundingStatus: LocalFundingStatus = commitment.localFundingStatus val remoteFundingStatus: RemoteFundingStatus = commitment.remoteFundingStatus + def commitInput(channelKeys: ChannelKeys): InputInfo = commitment.commitInput(channelKeys) + + def localFundingKey(channelKeys: ChannelKeys): PrivateKey = commitment.localFundingKey(channelKeys) + def localKeys(channelKeys: ChannelKeys): LocalCommitmentKeys = commitment.localKeys(params, channelKeys) def remoteKeys(channelKeys: ChannelKeys, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys = commitment.remoteKeys(params, channelKeys, remotePerCommitmentPoint) @@ -788,7 +794,7 @@ case class FullCommitment(params: ChannelParams, def htlcTxs(channelKeys: ChannelKeys): Seq[(HtlcTx, ByteVector64)] = commitment.htlcTxs(params, channelKeys) - def htlcTxs(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = commitment.htlcTxs(params, fundingKey, commitKeys) + def htlcTxs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys): Seq[(HtlcTx, ByteVector64)] = commitment.htlcTxs(params, channelKeys, commitKeys) def specs2String: String = { s"""specs: @@ -1184,10 +1190,12 @@ case class Commitments(params: ChannelParams, def validateSeed(channelKeys: ChannelKeys): Boolean = { active.forall { commitment => + // TODO: the following is circular, need another method val localFundingKey = channelKeys.fundingKey(commitment.fundingTxIndex).publicKey val remoteFundingKey = commitment.remoteFundingPubKey val redeemInfo = Helpers.Funding.makeFundingScript(localFundingKey, remoteFundingKey, params.commitmentFormat) - commitment.commitInput.txOut.publicKeyScript == redeemInfo.pubkeyScript + val commitInput = commitment.commitInput(channelKeys) + commitInput.txOut.publicKeyScript == redeemInfo.pubkeyScript } } 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 33df041cf1..8b3f91bf04 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 @@ -662,9 +662,9 @@ object Helpers { } } - def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { + def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { // this is just to estimate the weight, it depends on size of the pubkey scripts - val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, Satoshi(0), Satoshi(0), commitment.localCommit.spec) + val dummyClosingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, Satoshi(0), Satoshi(0), commitment.localCommit.spec) val dummyPubkey = commitment.remoteFundingPubKey val dummySig = ChannelSpendSignature.IndividualSignature(Transactions.PlaceHolderSig) val closingWeight = dummyClosingTx.aggregateSigs(dummyPubkey, dummyPubkey, dummySig, dummySig).weight() @@ -672,7 +672,7 @@ object Helpers { feerates.computeFees(closingWeight) } - def firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = { + def firstClosingFee(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf)(implicit log: LoggingAdapter): ClosingFees = { val requestedFeerate = onChainFeeConf.getClosingFeerate(feerates) val preferredFeerate = commitment.params.commitmentFormat match { case DefaultCommitmentFormat => @@ -683,15 +683,15 @@ object Helpers { // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can // always use CPFP to speed up confirmation if necessary. val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(ConfirmationPriority.Slow.getFeerate(feerates)), preferredFeerate * 2) - firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) + firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) } def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 def makeFirstClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: FeeratesPerKw, onChainFeeConf: OnChainFeeConf, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { val closingFees = closingFeerates_opt match { - case Some(closingFeerates) => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) - case None => firstClosingFee(commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf) + case Some(closingFeerates) => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFeerates) + case None => firstClosingFee(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, feerates, onChainFeeConf) } makeClosingTx(channelKeys, commitment, localScriptPubkey, remoteScriptPubkey, closingFees) } @@ -699,7 +699,7 @@ object Helpers { def makeClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, commitment.specs2String) val dustLimit = commitment.localParams.dustLimit.max(commitment.remoteParams.dustLimit) - val closingTx = ClosingTx.createUnsignedTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) + val closingTx = ClosingTx.createUnsignedTx(commitment.commitInput(channelKeys), localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) val localClosingSig = closingTx.sign(channelKeys.fundingKey(commitment.fundingTxIndex), commitment.remoteFundingPubKey).sig val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) log.debug(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") @@ -726,7 +726,7 @@ object Helpers { def makeSimpleClosingTx(currentBlockHeight: BlockHeight, channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerate: FeeratePerKw): Either[ChannelException, (ClosingTxs, ClosingComplete)] = { // We must convert the feerate to a fee: we must build dummy transactions to compute their weight. val closingFee = { - val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) + val dummyClosingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, SimpleClosingTxFee.PaidByUs(0 sat), currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) dummyClosingTxs.preferred_opt match { case Some(dummyTx) => val dummyPubkey = commitment.remoteFundingPubKey @@ -737,7 +737,7 @@ object Helpers { } } // Now that we know the fee we're ready to pay, we can create our closing transactions. - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, currentBlockHeight.toLong, localScriptPubkey, remoteScriptPubkey) closingTxs.preferred_opt match { case Some(closingTx) if closingTx.fee > 0.sat => () case _ => return Left(CannotGenerateClosingTx(commitment.channelId)) @@ -759,7 +759,7 @@ object Helpers { */ def signSimpleClosingTx(channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingComplete: ClosingComplete): Either[ChannelException, (ClosingTx, ClosingSig)] = { val closingFee = SimpleClosingTxFee.PaidByThem(closingComplete.fees) - val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput, commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) + val closingTxs = Transactions.makeSimpleClosingTxs(commitment.commitInput(channelKeys), commitment.localCommit.spec, closingFee, closingComplete.lockTime, localScriptPubkey, remoteScriptPubkey) // If our output isn't dust, they must provide a signature for a transaction that includes it. // Note that we're the closee, so we look for signatures including the closee output. (closingTxs.localAndRemote_opt, closingTxs.localOnly_opt) match { @@ -890,7 +890,7 @@ 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 htlcs = claimHtlcOutputs(fundingKey, commitmentKeys, commitment) + val htlcs = claimHtlcOutputs(channelKeys, commitmentKeys, commitment) val anchorOutput_opt = ClaimAnchorOutputTx.findInput(commitTx, fundingKey.publicKey, commitmentKeys, commitment.params.commitmentFormat).toOption val spendAnchor = htlcs.nonEmpty || onChainFeeConf.spendAnchorWithoutHtlcs val anchorTx_opt = if (spendAnchor) { @@ -926,7 +926,7 @@ 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. */ - private def claimHtlcOutputs(fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, Option[HtlcTx])] = { + private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, Option[HtlcTx])] = { // 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 @@ -939,7 +939,7 @@ 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.htlcTxs(fundingKey, commitKeys).collect { + commitment.htlcTxs(channelKeys, commitKeys).collect { case (txInfo: HtlcSuccessTx, remoteSig) => if (preimages.contains(txInfo.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. @@ -976,9 +976,8 @@ object Helpers { /** Claim the outputs of incoming HTLCs for the payment_hash matching the preimage provided. */ def claimHtlcsWithPreimage(channelKeys: ChannelKeys, localCommitPublished: LocalCommitPublished, commitment: FullCommitment, preimage: ByteVector32)(implicit log: LoggingAdapter): Seq[TxPublisher.PublishTx] = { - val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitKeys = commitment.localKeys(channelKeys) - commitment.htlcTxs(fundingKey, commitKeys).collect { + commitment.htlcTxs(channelKeys, commitKeys).collect { case (txInfo: HtlcSuccessTx, remoteSig) if txInfo.paymentHash == Crypto.sha256(preimage) => withTxGenerationLog("htlc-success") { val localSig = txInfo.sign(commitKeys, commitment.params.commitmentFormat, Map.empty) @@ -1108,7 +1107,7 @@ object Helpers { */ private def claimHtlcOutputs(channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, finalScriptPubKey: ByteVector)(implicit log: LoggingAdapter): Map[OutPoint, (DirectedHtlcId, 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) + val remoteCommitTx = makeCommitTx(commitment.commitInput(channelKeys), 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") // 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)) 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 b427046971..afc0ff25fd 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 @@ -1072,7 +1072,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall isInitiator = false, localContribution = spliceAck.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(Multisig2of2Input(channelKeys, parentCommitment)), remoteFundingPubKey = msg.fundingPubKey, localOutputs = Nil, lockTime = msg.lockTime, @@ -1116,7 +1116,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall isInitiator = true, localContribution = spliceInit.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(parentCommitment)), + sharedInput_opt = Some(Multisig2of2Input(channelKeys, parentCommitment)), remoteFundingPubKey = msg.fundingPubKey, localOutputs = cmd.spliceOutputs, lockTime = spliceInit.lockTime, @@ -1176,7 +1176,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks) stay() using d.copy(spliceStatus = SpliceStatus.SpliceAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, rbf.latestFundingTx.createdAt, rbf.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) case Right(rbf) => - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = false, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, feeCreditUsed_opt = None) match { case Left(t) => log.warning("rejecting rbf request with invalid liquidity ads: {}", t.getMessage) @@ -1192,7 +1192,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall isInitiator = false, localContribution = fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(Multisig2of2Input(channelKeys, rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, lockTime = msg.lockTime, @@ -1233,7 +1233,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case SpliceStatus.RbfRequested(cmd, txInitRbf) => getSpliceRbfContext(Some(cmd), d) match { case Right(rbf) => - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, txInitRbf.feerate, isChannelCreation = false, msg.willFund_opt) match { case Left(t) => log.info("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage) @@ -1246,7 +1246,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall isInitiator = true, localContribution = txInitRbf.fundingContribution, remoteContribution = msg.fundingContribution, - sharedInput_opt = Some(Multisig2of2Input(rbf.parentCommitment)), + sharedInput_opt = Some(Multisig2of2Input(channelKeys, rbf.parentCommitment)), remoteFundingPubKey = rbf.latestFundingTx.fundingParams.remoteFundingPubKey, localOutputs = rbf.latestFundingTx.fundingParams.localOutputs, lockTime = txInitRbf.lockTime, @@ -1741,7 +1741,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.params.localParams.paysClosingFees => // if we are not paying the closing fees and they proposed a fee range, we pick a value in that range and they should accept it without further negotiation // we don't care much about the closing fee since they're paying it (not us) and we can use CPFP if we want to speed up confirmation - val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) + val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) if (maxFee < localClosingFees.min) { log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min) stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}") @@ -1768,7 +1768,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) val (closingTx, closingSigned) = { // if we are not the channel initiator and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val localClosingFees = MutualClose.firstClosingFee(d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) + val localClosingFees = MutualClose.firstClosingFee(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.currentFeeratesForFundingClosing, nodeParams.onChainFeeConf) val nextPreferredFee = MutualClose.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) MutualClose.makeClosingTx(channelKeys, d.commitments.latest, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) } @@ -2415,7 +2415,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures // and our commit_sig if they haven't received it already. if (channelReestablish.nextLocalCommitmentNumber == 0) { - val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys)) goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs) } else { goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs @@ -2443,7 +2443,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall d.commitments.latest.localFundingStatus.localSigs_opt match { case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 => log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys)) goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady) case Some(txSigs) => log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) @@ -2515,7 +2515,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall // tx_signatures and our commit_sig if they haven't received it already. if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) { log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) - val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput) + val commitSig = d.commitments.latest.remoteCommit.sign(d.commitments.params, channelKeys, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput(channelKeys)) sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs } else { log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId) @@ -3288,7 +3288,7 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall val targetFeerate = nodeParams.onChainFeeConf.getFundingFeerate(nodeParams.currentFeeratesForFundingClosing) val fundingContribution = InteractiveTxFunder.computeSpliceContribution( isInitiator = true, - sharedInput = Multisig2of2Input(parentCommitment), + sharedInput = Multisig2of2Input(channelKeys, parentCommitment), spliceInAmount = cmd.additionalLocalFunding, spliceOut = cmd.spliceOutputs, targetFeerate = targetFeerate) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala index a6beb2b0c8..36a4814d00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenDualFunded.scala @@ -548,7 +548,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { log.info("rejecting rbf attempt: last attempt was less than {} blocks ago", nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks) stay() using d.copy(status = DualFundingStatus.RbfAborted) sending TxAbort(d.channelId, InvalidRbfAttemptTooSoon(d.channelId, d.latestFundingTx.createdAt, d.latestFundingTx.createdAt + nodeParams.channelConf.remoteRbfLimits.attemptDeltaBlocks).getMessage) } else { - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRequest(nodeParams.privateKey, d.channelId, fundingScript, msg.feerate, isChannelCreation = true, msg.requestFunding_opt, nodeParams.liquidityAdsConfig.rates_opt, None) match { case Left(t) => log.warning("rejecting rbf attempt: invalid liquidity ads request ({})", t.getMessage) @@ -606,7 +606,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers { lockTime = cmd.lockTime, targetFeerate = cmd.targetFeerate, ) - val fundingScript = d.commitments.latest.commitInput.txOut.publicKeyScript + val fundingScript = d.commitments.latest.commitInput(channelKeys).txOut.publicKeyScript LiquidityAds.validateRemoteFunding(cmd.requestFunding_opt, remoteNodeId, d.channelId, fundingScript, msg.fundingContribution, cmd.targetFeerate, isChannelCreation = true, msg.willFund_opt) match { case Left(t) => log.warning("rejecting rbf attempt: invalid liquidity ads response ({})", t.getMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index a1bf236c18..c2c6d138b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -284,7 +284,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, - localFundingPubKey = fundingKey.publicKey, remoteFundingPubKey = remoteFundingPubKey, fundingTxOutpoint = localCommitTx.input.outPoint, fundingAmount = localCommitTx.input.txOut.amount, @@ -333,7 +332,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, - localFundingPubKey = fundingPubkey, remoteFundingPubKey = remoteFundingPubKey, fundingTxOutpoint = localCommitTx.input.outPoint, fundingAmount = localCommitTx.input.txOut.amount, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala index ed96642c0c..ed5ee7a4fb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala @@ -45,7 +45,7 @@ trait CommonFundingHandlers extends CommonHandlers { */ def watchFundingSpent(commitment: Commitment, additionalKnownSpendingTxs: Set[TxId], delay_opt: Option[FiniteDuration]): Unit = { val knownSpendingTxs = commitment.commitTxIds.txIds ++ additionalKnownSpendingTxs - val watch = WatchFundingSpent(self, commitment.commitInput.outPoint.txid, commitment.commitInput.outPoint.index.toInt, knownSpendingTxs) + val watch = WatchFundingSpent(self, commitment.fundingTxOutpoint.txid, commitment.fundingTxOutpoint.index.toInt, knownSpendingTxs) delay_opt match { case Some(delay) => context.system.scheduler.scheduleOnce(delay, blockchain.toClassic, watch) case None => blockchain ! watch @@ -85,7 +85,7 @@ trait CommonFundingHandlers extends CommonHandlers { context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx)) d.commitments.all.find(_.fundingTxId == w.tx.txid) match { case Some(c) => - val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.commitInput.outPoint.index.toInt) + val scid = RealShortChannelId(w.blockHeight, w.txIndex, c.fundingTxOutpoint.index.toInt) val fundingStatus = ConfirmedFundingTx(w.tx, scid, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid)) // When a splice transaction confirms, it double-spends all the commitment transactions that only applied to the // previous funding transaction. Our peer cannot publish the corresponding revoked commitments anymore, so we can 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 5ab67bed94..4202928a6c 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 @@ -29,7 +29,7 @@ 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.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys} import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions.{IncomingHtlc, OutgoingHtlc, Transactions} import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelReestablish, Error, OpenChannel, UpdateAddHtlc, UpdateFulfillHtlc, Warning} @@ -232,7 +232,7 @@ trait ErrorHandlers extends CommonHandlers { def doPublish(lcp: LocalCommitPublished, txs: Closing.LocalClose.SecondStageTransactions, commitment: FullCommitment): Unit = { val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) val commitKeys = commitment.localKeys(channelKeys) - val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.commitInput.outPoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput, lcp.commitTx, commitment.localParams.paysCommitTxFees), None) + val publishCommitTx = PublishFinalTx(lcp.commitTx, commitment.fundingTxOutpoint, commitment.capacity, "commit-tx", Closing.commitTxFee(commitment.commitInput(channelKeys), lcp.commitTx, commitment.localParams.paysCommitTxFees), None) val publishAnchorTx_opt = txs.anchorTx_opt match { case Some(anchorTx) if !lcp.isConfirmed => val confirmationTarget = Closing.confirmationTarget(commitment.localCommit, commitment.localParams.dustLimit, commitment.params.commitmentFormat, nodeParams.onChainFeeConf) @@ -240,7 +240,7 @@ trait ErrorHandlers extends CommonHandlers { case _ => None } val publishMainDelayedTx_opt = txs.mainDelayedTx_opt.map(tx => PublishFinalTx(tx, tx.fee, None)) - val publishHtlcTxs = redeemableHtlcTxs(lcp.commitTx, fundingKey, commitKeys, commitment) + val publishHtlcTxs = redeemableHtlcTxs(lcp.commitTx, channelKeys, commitKeys, commitment) val publishQueue = Seq(publishCommitTx) ++ publishAnchorTx_opt ++ publishMainDelayedTx_opt ++ publishHtlcTxs publishIfNeeded(publishQueue, lcp.irrevocablySpent) @@ -265,11 +265,11 @@ trait ErrorHandlers extends CommonHandlers { txs.htlcDelayedTxs.foreach(tx => watchSpentIfNeeded(tx.input, lcp.irrevocablySpent)) } - private def redeemableHtlcTxs(commitTx: Transaction, fundingKey: PrivateKey, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Iterable[PublishTx] = { + private def redeemableHtlcTxs(commitTx: Transaction, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, commitment: FullCommitment): Iterable[PublishTx] = { val preimages = (commitment.changes.localChanges.all ++ commitment.changes.remoteChanges.all).collect { case fulfill: UpdateFulfillHtlc => Crypto.sha256(fulfill.paymentPreimage) -> fulfill.paymentPreimage }.toMap - commitment.htlcTxs(fundingKey, commitKeys).flatMap { + commitment.htlcTxs(channelKeys, commitKeys).flatMap { case (htlcTx, remoteSig) => val preimage_opt = preimages.get(htlcTx.paymentHash) commitment.params.commitmentFormat match { @@ -298,7 +298,7 @@ trait ErrorHandlers extends CommonHandlers { log.warning(s"they published their current commit in txid=${commitTx.txid}") require(commitTx.txid == commitments.remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "remote-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitments.commitInput(channelKeys), commitTx, d.commitments.params.localParams.paysCommitTxFees), "remote-commit")) val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitments, commitments.remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(remoteCommitPublished = Some(remoteCommitPublished)) @@ -317,7 +317,7 @@ trait ErrorHandlers extends CommonHandlers { require(commitTx.txid == remoteCommit.txId, "txid mismatch") val finalScriptPubKey = getOrGenerateFinalScriptPubKey(d) - context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput, commitTx, d.commitments.params.localParams.paysCommitTxFees), "next-remote-commit")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, commitTx, Closing.commitTxFee(commitment.commitInput(channelKeys), commitTx, d.commitments.params.localParams.paysCommitTxFees), "next-remote-commit")) val (remoteCommitPublished, closingTxs) = Closing.RemoteClose.claimCommitTxOutputs(channelKeys, commitment, remoteCommit, commitTx, nodeParams.currentBitcoinCoreFeerates, nodeParams.onChainFeeConf, finalScriptPubKey) val nextData = d match { case closing: DATA_CLOSING => closing.copy(nextRemoteCommitPublished = Some(remoteCommitPublished)) @@ -392,7 +392,7 @@ trait ErrorHandlers extends CommonHandlers { 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("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")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(commitment.commitInput(channelKeys), tx, d.commitments.params.localParams.paysCommitTxFees), "revoked-commit")) val exc = FundingTxSpent(d.channelId, tx.txid) val error = Error(d.channelId, exc.getMessage) val nextData = d match { @@ -406,7 +406,7 @@ trait ErrorHandlers extends CommonHandlers { case None => d match { case d: DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT => 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")) + context.system.eventStream.publish(TransactionPublished(d.channelId, remoteNodeId, tx, Closing.commitTxFee(d.commitments.latest.commitInput(channelKeys), 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) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index ed489af77e..d3e0897f17 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -118,8 +118,8 @@ object InteractiveTxBuilder { } object Multisig2of2Input { - def apply(commitment: Commitment): Multisig2of2Input = Multisig2of2Input( - info = commitment.commitInput, + def apply(channelKeys: ChannelKeys, commitment: Commitment): Multisig2of2Input = Multisig2of2Input( + info = commitment.commitInput(channelKeys), fundingTxIndex = commitment.fundingTxIndex, remoteFundingPubkey = commitment.remoteFundingPubKey ) @@ -1113,7 +1113,7 @@ object InteractiveTxSigningSession { LocalCommit.fromCommitSig(channelParams, commitKeys, fundingTx.txId, fundingKey, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec).map { signedLocalCommit => if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) { val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingKey.publicKey, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) SendingSigs(fundingStatus, commitment, fundingTx.localSigs) } else { this.copy(localCommit = Right(signedLocalCommit)) @@ -1137,9 +1137,8 @@ object InteractiveTxSigningSession { Left(f) case Right(fullySignedTx) => log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length) - val localFundingPubKey = channelKeys.fundingKey(fundingTxIndex).publicKey val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, currentBlockHeight, fundingParams, liquidityPurchase_opt) - val commitment = Commitment(fundingTxIndex, remoteCommit.index, localFundingPubKey, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) + val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingTxOutpoint, fundingTx.tx.sharedOutput.amount, fundingStatus, RemoteFundingStatus.NotLocked, channelParams.commitmentFormat, signedLocalCommit, remoteCommit, None) Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala index ff7ea3da80..69f10bfaba 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTx.scala @@ -39,7 +39,7 @@ sealed trait ReplaceableTx { def commitment: FullCommitment def commitmentFormat: CommitmentFormat = commitment.params.commitmentFormat def dustLimit: Satoshi = commitment.localParams.dustLimit - def commitFee: Satoshi = commitment.commitInput.txOut.amount - commitTx.txOut.map(_.amount).sum + def commitFee: Satoshi = commitment.capacity - commitTx.txOut.map(_.amount).sum def concurrentCommitTxs: Set[TxId] = commitment.commitTxIds.txIds - commitTx.txid // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index ddb27aab18..ac82d4b4f2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -555,8 +555,8 @@ object OriginSerializer extends MinimalSerializer({ }) // @formatter:off -case class CommitmentJson(fundingTxIndex: Long, fundingTx: InputInfo, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit]) -object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.commitInput, c.localFundingStatus, c.remoteFundingStatus, c.localCommit, c.remoteCommit, c.nextRemoteCommit_opt.map(_.commit))) +case class CommitmentJson(fundingTxIndex: Long, fundingTxOutpoint: OutPoint, fundingAmount: Satoshi, localFunding: LocalFundingStatus, remoteFunding: RemoteFundingStatus, localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit: Option[RemoteCommit]) +object CommitmentSerializer extends ConvertClassSerializer[Commitment](c => CommitmentJson(c.fundingTxIndex, c.fundingTxOutpoint, c.fundingAmount, c.localFundingStatus, c.remoteFundingStatus, c.localCommit, c.remoteCommit, c.nextRemoteCommit_opt.map(_.commit))) // @formatter:on // @formatter:off 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 d706608336..4b558ef66f 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 @@ -220,7 +220,6 @@ private[channel] object ChannelTypes0 { val commitment = Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, - localFundingPubKey = randomKey().publicKey, remoteFundingPubKey = remoteParams.fundingPubKey, fundingTxOutpoint = localCommit.publishableTxs.commitTx.input.outPoint, fundingAmount = localCommit.publishableTxs.commitTx.input.txOut.amount, 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 00871f58aa..3a4b5014d0 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 @@ -18,12 +18,12 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64} -import fr.acinq.eclair.{channel, randomKey} +import fr.acinq.eclair.channel import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.transactions.CommitmentSpec -import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx} +import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo} import fr.acinq.eclair.wire.internal.channel.version0.ChannelTypes0 import fr.acinq.eclair.wire.protocol.CommitSig @@ -38,11 +38,17 @@ private[channel] object ChannelTypes3 { // Before version 4, we stored the unsigned commit tx and htlc txs in our local commit. // We changed that to only store the remote signatures and re-compute transactions on-the-fly when force-closing. case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig]) { - def migrate(): channel.LocalCommit = channel.LocalCommit(index, spec, commitTxAndRemoteSig.commitTx.tx.txid, commitTxAndRemoteSig.remoteSig, htlcTxsAndRemoteSigs.map(_.remoteSig)) + def migrate(): (InputInfo, channel.LocalCommit) = ( + commitTxAndRemoteSig.commitTx.input, + channel.LocalCommit(index, spec, commitTxAndRemoteSig.commitTx.tx.txid, commitTxAndRemoteSig.remoteSig, htlcTxsAndRemoteSigs.map(_.remoteSig)) + ) } case class UnsignedLocalCommit(index: Long, spec: CommitmentSpec, commitTx: CommitTx, htlcTxs: List[HtlcTx]) { - def migrate(): InteractiveTxSigningSession.UnsignedLocalCommit = InteractiveTxSigningSession.UnsignedLocalCommit(index, spec, commitTx.tx.txid) + def migrate(): (InputInfo, InteractiveTxSigningSession.UnsignedLocalCommit) = ( + commitTx.input, + InteractiveTxSigningSession.UnsignedLocalCommit(index, spec, commitTx.tx.txid) + ) } // Before version4, we didn't support multiple active commitments, which were later introduced by dual funding and splicing. @@ -65,13 +71,12 @@ private[channel] object ChannelTypes3 { Seq(Commitment( fundingTxIndex = 0, firstRemoteCommitIndex = 0, - localFundingPubKey = randomKey().publicKey, remoteFundingPubKey = remoteParams.fundingPubKey, fundingTxOutpoint = localCommit.commitTxAndRemoteSig.commitTx.input.outPoint, fundingAmount = localCommit.commitTxAndRemoteSig.commitTx.input.txOut.amount, localFundingStatus, remoteFundingStatus, format = channelFeatures.commitmentFormat, - localCommit.migrate(), remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), + localCommit.migrate()._2, remoteCommit, remoteNextCommitInfo.left.toOption.map(w => NextRemoteCommit(w.sent, w.nextRemoteCommit)))), inactive = Nil, remoteNextCommitInfo.fold(w => Left(WaitForRev(w.sentAfterLocalCommitIndex)), remotePerCommitmentPoint => Right(remotePerCommitmentPoint)), remotePerCommitmentSecrets, 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 7898c84632..ed52b71f1e 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 @@ -462,6 +462,13 @@ private[channel] object ChannelCodecs4 { .typecase(0x01, provide(RemoteFundingStatus.NotLocked)) .typecase(0x02, provide(RemoteFundingStatus.Locked)) + val commitmentFormatCodec: Codec[CommitmentFormat] = discriminated[CommitmentFormat].by(uint8) + .typecase(0x00, provide(DefaultCommitmentFormat)) + .typecase(0x01, provide(UnsafeLegacyAnchorOutputsCommitmentFormat)) + .typecase(0x02, provide(ZeroFeeHtlcTxAnchorOutputsCommitmentFormat)) + .typecase(0x03, provide(LegacySimpleTaprootChannelCommitmentFormat)) + .typecase(0x04, provide(ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat)) + val paramsCodec: Codec[ChannelParams] = ( ("channelId" | bytes32) :: ("channelConfig" | channelConfigCodec) :: @@ -479,11 +486,11 @@ private[channel] object ChannelCodecs4 { ("localNextHtlcId" | uint64overflow) :: ("remoteNextHtlcId" | uint64overflow)).as[CommitmentChanges] - private def localCommitWithTxsCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( + private def localCommitWithTxsCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[(InputInfo, LocalCommit)] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTxAndRemoteSig" | commitTxAndRemoteSigCodec) :: - ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[ChannelTypes3.LocalCommit].decodeOnly.map[LocalCommit](_.migrate()).decodeOnly + ("htlcTxsAndRemoteSigs" | listOfN(uint16, htlcTxsAndRemoteSigsCodec))).as[ChannelTypes3.LocalCommit].map(_.migrate()).decodeOnly private def localCommitCodec(commitmentSpecCodec: Codec[CommitmentSpec]): Codec[LocalCommit] = ( ("index" | uint64overflow) :: @@ -502,32 +509,41 @@ private[channel] object ChannelCodecs4 { ("sig" | lengthDelimited(commitSigCodec)) :: ("commit" | remoteCommitCodec(commitmentSpecCodec))).as[NextRemoteCommit] - private def commitmentCodecWithoutFirstRemoteCommitIndex(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + private def commitmentCodecWithoutFirstRemoteCommitIndex(commitmentFormat: CommitmentFormat, htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( ("fundingTxIndex" | uint32) :: ("firstRemoteCommitIndex" | provide(0L)) :: - ("fundingPubKey" | publicKey) :: - ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingPubKey" | publicKey) :: + ("localFundingTxStatus" | fundingTxStatusCodec) :: ("remoteFundingStatus" | remoteFundingStatusCodec) :: ("localCommit" | localCommitWithTxsCodec(minimalCommitmentSpecCodec(htlcs))) :: ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: - ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).map { + case fundingTxIndex :: firstRemoteCommitIndex :: remoteFundingPubKey :: localFundingTxStatus :: remoteFundingStatus :: localCommit :: remoteCommit :: nextRemoteCommit_opt :: HNil => + Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localCommit._1.outPoint, localCommit._1.txOut.amount, localFundingTxStatus, remoteFundingStatus, commitmentFormat, localCommit._2, remoteCommit, nextRemoteCommit_opt) + }.decodeOnly - private def commitmentCodecWithLocalTxs(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( + private def commitmentCodecWithLocalTxs(commitmentFormat: CommitmentFormat, htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( ("fundingTxIndex" | uint32) :: ("firstRemoteCommitIndex" | uint64overflow) :: - ("fundingPubKey" | publicKey) :: - ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingPubKey" | publicKey) :: + ("localFundingTxStatus" | fundingTxStatusCodec) :: ("remoteFundingStatus" | remoteFundingStatusCodec) :: ("localCommit" | localCommitWithTxsCodec(minimalCommitmentSpecCodec(htlcs))) :: ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: - ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] + ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).map { + case fundingTxIndex :: firstRemoteCommitIndex :: remoteFundingPubKey :: localFundingTxStatus :: remoteFundingStatus :: localCommit :: remoteCommit :: nextRemoteCommit_opt :: HNil => + Commitment(fundingTxIndex, firstRemoteCommitIndex, remoteFundingPubKey, localCommit._1.outPoint, localCommit._1.txOut.amount, localFundingTxStatus, remoteFundingStatus, commitmentFormat, localCommit._2, remoteCommit, nextRemoteCommit_opt) + }.decodeOnly private def commitmentCodec(htlcs: Set[DirectedHtlc]): Codec[Commitment] = ( ("fundingTxIndex" | uint32) :: ("firstRemoteCommitIndex" | uint64overflow) :: - ("fundingPubKey" | publicKey) :: - ("fundingTxStatus" | fundingTxStatusCodec) :: + ("remoteFundingPubKey" | publicKey) :: + ("fundingTxOutpoint" | outPointCodec) :: + ("fundingAmount" | satoshi) :: + ("localFundingTxStatus" | fundingTxStatusCodec) :: ("remoteFundingStatus" | remoteFundingStatusCodec) :: + ("format" | commitmentFormatCodec) :: ("localCommit" | localCommitCodec(minimalCommitmentSpecCodec(htlcs))) :: ("remoteCommit" | remoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))) :: ("nextRemoteCommit_opt" | optional(bool8, nextRemoteCommitCodec(minimalCommitmentSpecCodec(htlcs.map(_.opposite)))))).as[Commitment] @@ -586,31 +602,31 @@ private[channel] object ChannelCodecs4 { } val commitmentsCodecWithoutFirstRemoteCommitIndex: Codec[Commitments] = ( - ("params" | paramsCodec) :: + ("params" | paramsCodec) >>:~ { params => ("changes" | changesCodec) :: (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => - ("active" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: - ("inactive" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(htlcs))) :: + ("active" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(params.commitmentFormat, htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodecWithoutFirstRemoteCommitIndex(params.commitmentFormat, htlcs))) :: ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: ("originChannels" | originsMapCodec) :: ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) - })).as[EncodedCommitments].xmap( + })}).as[EncodedCommitments].xmap( encoded => encoded.toCommitments, commitments => EncodedCommitments(commitments) ) val commitmentsCodecWithLocalTxs: Codec[Commitments] = ( - ("params" | paramsCodec) :: + ("params" | paramsCodec) >>:~ { params => ("changes" | changesCodec) :: (("htlcs" | setCodec(htlcCodec)) >>:~ { htlcs => - ("active" | listOfN(uint16, commitmentCodecWithLocalTxs(htlcs))) :: - ("inactive" | listOfN(uint16, commitmentCodecWithLocalTxs(htlcs))) :: + ("active" | listOfN(uint16, commitmentCodecWithLocalTxs(params.commitmentFormat, htlcs))) :: + ("inactive" | listOfN(uint16, commitmentCodecWithLocalTxs(params.commitmentFormat, htlcs))) :: ("remoteNextCommitInfo" | either(bool8, waitForRevCodec, publicKey)) :: ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) :: ("originChannels" | originsMapCodec) :: ("remoteChannelData_opt" | optional(bool8, varsizebinarydata)) - })).as[EncodedCommitments].xmap( + })}).as[EncodedCommitments].xmap( encoded => encoded.toCommitments, commitments => EncodedCommitments(commitments) ) @@ -716,17 +732,16 @@ private[channel] object ChannelCodecs4 { // We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel // cannot be used for payments. private val (interactiveTxWaitingForSigsWithoutLiquidityPurchaseCodec, interactiveTxWaitingForSigsWithTxsCodec, interactiveTxWaitingForSigsCodec): (Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs], Codec[InteractiveTxSigningSession.WaitingForSigs]) = { - val unsignedLocalCommitWithTxsCodec: Codec[UnsignedLocalCommit] = ( + val unsignedLocalCommitWithTxsCodec: Codec[(InputInfo, UnsignedLocalCommit)] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: ("commitTx" | commitTxCodec) :: - ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[ChannelTypes3.UnsignedLocalCommit].decodeOnly.map[UnsignedLocalCommit](_.migrate()).decodeOnly + ("htlcTxs" | listOfN(uint16, htlcTxCodec))).as[ChannelTypes3.UnsignedLocalCommit].map(_.migrate()).decodeOnly val unsignedLocalCommitCodec: Codec[UnsignedLocalCommit] = ( ("index" | uint64overflow) :: ("spec" | commitmentSpecCodec) :: - ("txId" | txId) :: - ("input" | inputInfoCodec)).as[UnsignedLocalCommit] + ("txId" | txId)).as[UnsignedLocalCommit] val waitingForSigsWithoutLiquidityPurchaseCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: @@ -734,7 +749,15 @@ private[channel] object ChannelCodecs4 { ("fundingTx" | partiallySignedSharedTransactionCodec) :: ("localCommit" | either(bool8, unsignedLocalCommitWithTxsCodec, localCommitWithTxsCodec(commitmentSpecCodec))) :: ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: - ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))).as[InteractiveTxSigningSession.WaitingForSigs] + ("liquidityPurchase" | provide(Option.empty[LiquidityAds.PurchaseBasicInfo]))) + .map { + case fundingParams :: fundingTxIndex :: fundingTx :: localCommit :: remoteCommit :: liquidityPurchase :: HNil => + val (inputInfo, localCommit1) = localCommit match { + case Left((inputInfo, unsignedLocalCommit)) => (inputInfo, Left(unsignedLocalCommit)) + case Right((inputInfo, localCommit)) => (inputInfo, Right(localCommit)) + } + InteractiveTxSigningSession.WaitingForSigs(fundingParams, fundingTxIndex, fundingTx, inputInfo, localCommit1, remoteCommit, liquidityPurchase) + }.decodeOnly val waitingForSigsWithTxsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: @@ -742,12 +765,21 @@ private[channel] object ChannelCodecs4 { ("fundingTx" | partiallySignedSharedTransactionCodec) :: ("localCommit" | either(bool8, unsignedLocalCommitWithTxsCodec, localCommitWithTxsCodec(commitmentSpecCodec))) :: ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: - ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs] + ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))) + .map { + case fundingParams :: fundingTxIndex :: fundingTx :: localCommit :: remoteCommit :: liquidityPurchase :: HNil => + val (inputInfo, localCommit1) = localCommit match { + case Left((inputInfo, unsignedLocalCommit)) => (inputInfo, Left(unsignedLocalCommit)) + case Right((inputInfo, localCommit)) => (inputInfo, Right(localCommit)) + } + InteractiveTxSigningSession.WaitingForSigs(fundingParams, fundingTxIndex, fundingTx, inputInfo, localCommit1, remoteCommit, liquidityPurchase) + }.decodeOnly val waitingForSigsCodec: Codec[InteractiveTxSigningSession.WaitingForSigs] = ( ("fundingParams" | fundingParamsCodec) :: ("fundingTxIndex" | uint32) :: ("fundingTx" | partiallySignedSharedTransactionCodec) :: + ("commitInput" | inputInfoCodec) :: ("localCommit" | either(bool8, unsignedLocalCommitCodec, localCommitCodec(commitmentSpecCodec))) :: ("remoteCommit" | remoteCommitCodec(commitmentSpecCodec)) :: ("liquidityPurchase" | optional(bool8, liquidityPurchaseCodec))).as[InteractiveTxSigningSession.WaitingForSigs]