From bff0b86d1a2a20548fe449b86c87a554b95d417b Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 14 Apr 2021 14:47:36 +0200 Subject: [PATCH] Add channel logic to handle new mutual close flow As described in https://github.com/lightningnetwork/lightning-rfc/pull/847 We also refactor the negotiating state, add many tests and fix #1742. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 6 +- .../fr/acinq/eclair/channel/Channel.scala | 108 +++-- .../acinq/eclair/channel/ChannelTypes.scala | 15 +- .../fr/acinq/eclair/channel/Helpers.scala | 41 +- .../channel/version0/ChannelCodecs0.scala | 9 +- .../channel/version1/ChannelCodecs1.scala | 6 +- .../channel/version2/ChannelCodecs2.scala | 42 +- .../fr/acinq/eclair/channel/FuzzySpec.scala | 4 +- .../states/StateTestsHelperMethods.scala | 2 +- .../a/WaitForAcceptChannelStateSpec.scala | 2 +- .../a/WaitForOpenChannelStateSpec.scala | 2 +- ...itForFundingCreatedInternalStateSpec.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 4 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../channel/states/e/NormalStateSpec.scala | 36 +- .../channel/states/e/OfflineStateSpec.scala | 4 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../states/g/NegotiatingStateSpec.scala | 442 +++++++++++++----- .../channel/states/h/ClosingStateSpec.scala | 49 +- .../integration/ChannelIntegrationSpec.scala | 2 +- .../internal/channel/ChannelCodecsSpec.scala | 4 +- .../channel/version2/ChannelCodecs2Spec.scala | 25 + 24 files changed, 553 insertions(+), 262 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index a55977084e..ef2d17fe3d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -87,7 +87,7 @@ trait Eclair { def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], initialRelayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] - def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] + def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] @@ -183,8 +183,8 @@ class EclairImpl(appKit: Kit) extends Eclair { timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] } - override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { - sendToChannels[CommandResponse[CMD_CLOSE]](channels, CMD_CLOSE(ActorRef.noSender, scriptPubKey_opt)) + override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = { + sendToChannels[CommandResponse[CMD_CLOSE]](channels, CMD_CLOSE(ActorRef.noSender, scriptPubKey_opt, closingFeerates_opt)) } override def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index f5a8bb7662..fa5312376d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -651,7 +651,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, feeBase, feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.schedule(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, interval = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) - goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() + goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None, None) storing() case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel => log.debug("received remote announcement signatures, delaying") @@ -845,7 +845,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val localShutdown = Shutdown(d.channelId, commitments1.localParams.defaultFinalScriptPubKey) // note: it means that we had pending htlcs to sign, therefore we go to SHUTDOWN, not to NEGOTIATING require(commitments1.remoteCommit.spec.htlcs.nonEmpty, "we must have just signed new htlcs, otherwise we would have sent our Shutdown earlier") - goto(SHUTDOWN) using DATA_SHUTDOWN(commitments1, localShutdown, d.remoteShutdown.get) storing() sending localShutdown + goto(SHUTDOWN) using DATA_SHUTDOWN(commitments1, localShutdown, d.remoteShutdown.get, d.closingFeerates) storing() sending localShutdown } else { stay using d.copy(commitments = commitments1) storing() } @@ -867,7 +867,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleCommandError(InvalidFinalScript(d.channelId), c) } else { val shutdown = Shutdown(d.channelId, localScriptPubKey) - handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown + handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closingFeerates = c.feerates)) storing() sending shutdown } case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d: DATA_NORMAL) => @@ -921,7 +921,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // there are no pending signed changes, let's go directly to NEGOTIATING if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, d.closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned } else { // we are fundee, will wait for their closing_signed @@ -929,7 +929,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } else { // there are some pending signed changes, we need to wait for them to be settled (fail/fulfill htlcs and sign fee updates) - goto(SHUTDOWN) using DATA_SHUTDOWN(d.commitments, localShutdown, remoteShutdown) storing() sending sendList + goto(SHUTDOWN) using DATA_SHUTDOWN(d.commitments, localShutdown, remoteShutdown, d.closingFeerates) storing() sending sendList } } @@ -1149,7 +1149,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId stay using d.copy(commitments = d.commitments.copy(remoteNextCommitInfo = Left(waitForRevocation.copy(reSignAsap = true)))) } - case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown)) => + case Event(commit: CommitSig, d@DATA_SHUTDOWN(_, localShutdown, remoteShutdown, closingFeerates)) => Commitments.receiveCommit(d.commitments, commit, keyManager) match { case Right((commitments1, revocation)) => // we always reply with a revocation @@ -1158,7 +1158,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (commitments1.hasNoPendingHtlcsOrFeeUpdate) { if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending revocation :: closingSigned :: Nil } else { // we are fundee, will wait for their closing_signed @@ -1174,7 +1174,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Left(cause) => handleLocalError(cause, d, Some(commit)) } - case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(commitments, localShutdown, remoteShutdown)) => + case Event(revocation: RevokeAndAck, d@DATA_SHUTDOWN(commitments, localShutdown, remoteShutdown, closingFeerates)) => // we received a revocation because we sent a signature // => all our changes have been acked including the shutdown message Commitments.receiveRevocation(commitments, revocation) match { @@ -1194,7 +1194,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.debug("switching to NEGOTIATING spec:\n{}", Commitments.specs2String(commitments1)) if (d.commitments.localParams.isFunder) { // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, commitments1, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, closingFeerates) goto(NEGOTIATING) using DATA_NEGOTIATING(commitments1, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending closingSigned } else { // we are fundee, will wait for their closing_signed @@ -1229,38 +1229,68 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(NEGOTIATING)(handleExceptions { case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig, _), d: DATA_NEGOTIATING) => - log.info("received closingFeeSatoshis={}", remoteClosingFee) + log.info("received closing fees={}", remoteClosingFee) Closing.checkClosingSignature(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, remoteClosingFee, remoteSig) match { - case Right(signedClosingTx) if d.closingTxProposed.last.lastOption.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee) || d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS => - // we close when we converge or when there were too many iterations - handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) - case Right(signedClosingTx) => - // if we are fundee and we were waiting for them to send their first closing_signed, we don't have a lastLocalClosingFee, so we compute a firstClosingFee - val lastLocalClosingFee = d.closingTxProposed.last.lastOption.map(_.localClosingSigned.feeSatoshis) - val nextClosingFee = if (d.commitments.localCommit.spec.toLocal == 0.msat) { - // if we have nothing at stake there is no need to negotiate and we accept their fee right away - remoteClosingFee - } else { - Closing.nextClosingFee( - localClosingFee = lastLocalClosingFee.getOrElse(Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets)), - remoteClosingFee = remoteClosingFee) - } - val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nextClosingFee) - if (lastLocalClosingFee.contains(nextClosingFee)) { - // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now + case Right((signedClosingTx, closingSignedRemoteFees)) => + val lastLocalClosingSigned_opt = d.closingTxProposed.last.lastOption + if (lastLocalClosingSigned_opt.exists(_.localClosingSigned.feeSatoshis == remoteClosingFee)) { + // they accepted the last fee we sent them, so we close without sending a closing_signed handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) - } else if (nextClosingFee == remoteClosingFee) { - // we have converged! - val closingTxProposed1 = d.closingTxProposed match { - case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) - } - handleMutualClose(signedClosingTx, Left(d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSigned + } else if (d.closingTxProposed.flatten.size >= MAX_NEGOTIATION_ITERATIONS) { + // there were too many iterations, we stop negotiating and accept their fee + log.warning("could not agree on closing fees after {} iterations, accepting their closing fees ({})", MAX_NEGOTIATION_ITERATIONS, remoteClosingFee) + handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees + } else if (lastLocalClosingSigned_opt.flatMap(_.localClosingSigned.feeRange_opt).exists(r => r.min <= remoteClosingFee && remoteClosingFee <= r.max)) { + // they chose a fee inside our proposed fee range, so we close and send a closing_signed for that fee + handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees + } else if (d.commitments.localCommit.spec.toLocal == 0.msat) { + // we have nothing at stake so there is no need to negotiate, we accept their fee right away + handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees } else { - log.info("proposing closingFeeSatoshis={}", closingSigned.feeSatoshis) - val closingTxProposed1 = d.closingTxProposed match { - case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) + c.feeRange_opt match { + case Some(ClosingSignedTlv.FeeRange(minFee, maxFee)) if !d.commitments.localParams.isFunder => + // if we are fundee 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 closingFee = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) match { + case ClosingFees(preferred, _, _) if preferred > maxFee => maxFee + // if we underestimate the fee, then we're happy with whatever they propose (it will confirm more quickly and we're not paying it) + case ClosingFees(preferred, _, _) if preferred < remoteClosingFee => remoteClosingFee + case ClosingFees(preferred, _, _) => preferred + } + if (closingFee == remoteClosingFee) { + log.info("accepting their closing fees={}", remoteClosingFee) + handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSignedRemoteFees + } else { + val (closingTx, closingSigned) = Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, ClosingFees(closingFee, minFee, maxFee)) + log.info("proposing closing fees={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee) + val closingTxProposed1 = d.closingTxProposed match { + case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) + } + stay using d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)) storing() sending closingSigned + } + case _ => + val lastLocalClosingFee_opt = lastLocalClosingSigned_opt.map(_.localClosingSigned.feeSatoshis) + val (closingTx, closingSigned) = { + // if we are fundee 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 = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val nextPreferredFee = Closing.nextClosingFee(lastLocalClosingFee_opt.getOrElse(localClosingFees.preferred), remoteClosingFee) + Closing.makeClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, localClosingFees.copy(preferred = nextPreferredFee)) + } + val closingTxProposed1 = d.closingTxProposed match { + case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) + } + if (lastLocalClosingFee_opt.contains(closingSigned.feeSatoshis)) { + // next computed fee is the same than the one we previously sent (probably because of rounding), let's close now + handleMutualClose(signedClosingTx, Left(d.copy(bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) + } else if (closingSigned.feeSatoshis == remoteClosingFee) { + // we have converged! + log.info("accepting their closing fees={}", remoteClosingFee) + handleMutualClose(signedClosingTx, Left(d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSigned + } else { + log.info("proposing closing fees={}", closingSigned.feeSatoshis) + stay using d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)) storing() sending closingSigned + } } - stay using d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)) storing() sending closingSigned } case Left(cause) => handleLocalError(cause, d, Some(c)) } @@ -1672,7 +1702,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them if (d.commitments.localParams.isFunder) { // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, None) val closingTxProposed1 = d.closingTxProposed :+ List(ClosingTxProposed(closingTx, closingSigned)) goto(NEGOTIATING) using d.copy(closingTxProposed = closingTxProposed1) storing() sending d.localShutdown :: closingSigned :: Nil } else { @@ -2538,5 +2568,3 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId initialize() } - - diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index ac6e12d537..bb02a60680 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -178,9 +178,16 @@ final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessa final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HtlcSettlementCommand final case class CMD_UPDATE_FEE(feeratePerKw: FeeratePerKw, commit: Boolean = false, replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand final case class CMD_SIGN(replyTo_opt: Option[ActorRef] = None) extends HasOptionalReplyToCommand + +final case class ClosingFees(preferred: Satoshi, min: Satoshi, max: Satoshi) +final case class ClosingFeerates(preferred: FeeratePerKw, min: FeeratePerKw, max: FeeratePerKw) { + def computeFees(closingTxWeight: Int): ClosingFees = ClosingFees(weight2fee(preferred, closingTxWeight), weight2fee(min, closingTxWeight), weight2fee(max, closingTxWeight)) +} + sealed trait CloseCommand extends HasReplyToCommand -final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector]) extends CloseCommand +final case class CMD_CLOSE(replyTo: ActorRef, scriptPubKey: Option[ByteVector], feerates: Option[ClosingFeerates]) extends CloseCommand final case class CMD_FORCECLOSE(replyTo: ActorRef) extends CloseCommand + final case class CMD_UPDATE_RELAY_FEE(replyTo: ActorRef, feeBase: MilliSatoshi, feeProportionalMillionths: Long) extends HasReplyToCommand final case class CMD_GETSTATE(replyTo: ActorRef) extends HasReplyToCommand final case class CMD_GETSTATEDATA(replyTo: ActorRef) extends HasReplyToCommand @@ -430,9 +437,9 @@ final case class DATA_NORMAL(commitments: Commitments, channelAnnouncement: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], - remoteShutdown: Option[Shutdown]) extends Data with HasCommitments -final case class DATA_SHUTDOWN(commitments: Commitments, - localShutdown: Shutdown, remoteShutdown: Shutdown) extends Data with HasCommitments + remoteShutdown: Option[Shutdown], + closingFeerates: Option[ClosingFeerates]) extends Data with HasCommitments +final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends Data with HasCommitments final case class DATA_NEGOTIATING(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingTxProposed: List[List[ClosingTxProposed]], // one list for every negotiation (there can be several in case of disconnection) 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 10876e3014..9a2cddfe0b 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 @@ -420,58 +420,61 @@ object Helpers { } } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeratePerKw: FeeratePerKw)(implicit log: LoggingAdapter): Satoshi = { + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feerates: ClosingFeerates)(implicit log: LoggingAdapter): ClosingFees = { import commitments._ // this is just to estimate the weight, it depends on size of the pubkey scripts val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx) - log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx") - Transactions.weight2fee(feeratePerKw, closingWeight) + log.info(s"using feerates=$feerates for initial closing tx") + feerates.computeFees(closingWeight) } - def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): Satoshi = { + def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): ClosingFees = { val requestedFeerate = feeEstimator.getFeeratePerKw(feeTargets.mutualCloseBlockTarget) - val feeratePerKw = if (commitments.channelVersion.hasAnchorOutputs) { + val preferredFeerate = if (commitments.channelVersion.hasAnchorOutputs) { requestedFeerate } else { // we "MUST set fee_satoshis less than or equal to the base fee of the final commitment transaction" requestedFeerate.min(commitments.localCommit.spec.feeratePerKw) } - firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeratePerKw) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, ClosingFeerates(preferredFeerate, preferredFeerate / 2, preferredFeerate * 2)) } def nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2 - def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { - val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) - makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee) + def makeFirstClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeEstimator: FeeEstimator, feeTargets: FeeTargets, closingFeerates_opt: Option[ClosingFeerates])(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + val closingFees = closingFeerates_opt match { + case Some(closingFeerates) => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) + case None => firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, feeEstimator, feeTargets) + } + makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees) } - def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { + def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFees: ClosingFees)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = { import commitments._ require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey") require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey") - log.debug("making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments)) - val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit) - val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) + log.debug("making closing tx with closing fees={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) + val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) + val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimit, closingFees.preferred, localCommit.spec) val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat) - val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig) - log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}") + val closingSigned = ClosingSigned(channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) + log.info(s"signed closing txid=${closingTx.tx.txid} with closing fees=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") (closingTx, closingSigned) } - def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, ClosingTx] = { + def checkClosingSignature(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, remoteClosingFee: Satoshi, remoteClosingSig: ByteVector64)(implicit log: LoggingAdapter): Either[ChannelException, (ClosingTx, ClosingSigned)] = { import commitments._ val lastCommitFeeSatoshi = commitments.commitInput.txOut.amount - commitments.localCommit.publishableTxs.commitTx.tx.txOut.map(_.amount).sum if (remoteClosingFee > lastCommitFeeSatoshi && !commitments.channelVersion.hasAnchorOutputs) { - log.error(s"remote proposed a commit fee higher than the last commitment fee: remoteClosingFeeSatoshi=${remoteClosingFee.toLong} lastCommitFeeSatoshi=$lastCommitFeeSatoshi") + log.error(s"remote proposed a commit fee higher than the last commitment fee: remote closing fees=${remoteClosingFee.toLong} last commit fees=$lastCommitFeeSatoshi") Left(InvalidCloseFee(commitments.channelId, remoteClosingFee)) } else { - val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee) + val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee, remoteClosingFee, remoteClosingFee)) val signedClosingTx = Transactions.addSigs(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath).publicKey, remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig) Transactions.checkSpendable(signedClosingTx) match { - case Success(_) => Right(signedClosingTx) + case Success(_) => Right(signedClosingTx, closingSigned) case _ => Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index 0af1bc5698..6134f90ab3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -311,7 +311,8 @@ private[channel] object ChannelCodecs0 { ("channelAnnouncement" | optional(bool, variableSizeBytes(noUnknownFieldsChannelAnnouncementSizeCodec, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(noUnknownFieldsChannelUpdateSizeCodec, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: - ("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL].decodeOnly + ("remoteShutdown" | optional(bool, shutdownCodec)) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: @@ -320,12 +321,14 @@ private[channel] object ChannelCodecs0 { ("channelAnnouncement" | optional(bool, variableSizeBytes(uint16, channelAnnouncementCodec))) :: ("channelUpdate" | variableSizeBytes(uint16, channelUpdateCodec)) :: ("localShutdown" | optional(bool, shutdownCodec)) :: - ("remoteShutdown" | optional(bool, shutdownCodec))).as[DATA_NORMAL].decodeOnly + ("remoteShutdown" | optional(bool, shutdownCodec)) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL].decodeOnly val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | shutdownCodec) :: - ("remoteShutdown" | shutdownCodec)).as[DATA_SHUTDOWN].decodeOnly + ("remoteShutdown" | shutdownCodec) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN].decodeOnly val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index 6132f0efc7..92e79f6279 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -247,12 +247,14 @@ private[channel] object ChannelCodecs1 { ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec)))).as[DATA_NORMAL] + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec))).as[DATA_SHUTDOWN] + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: 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 597b526ec7..fbaecec3ed 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 @@ -233,6 +233,11 @@ private[channel] object ChannelCodecs2 { ("channelId" | bytes32) }).as[Commitments] + val closingFeeratesCodec: Codec[ClosingFeerates] = ( + ("preferred" | feeratePerKw) :: + ("min" | feeratePerKw) :: + ("max" | feeratePerKw)).as[ClosingFeerates] + val closingTxProposedCodec: Codec[ClosingTxProposed] = ( ("unsignedTx" | closingTxCodec) :: ("localClosingSigned" | lengthDelimited(closingSignedCodec))).as[ClosingTxProposed] @@ -274,6 +279,16 @@ private[channel] object ChannelCodecs2 { ("lastSent" | lengthDelimited(fundingLockedCodec)) :: ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + val DATA_NORMAL_COMPAT_02_Codec: Codec[DATA_NORMAL] = ( + ("commitments" | commitmentsCodec) :: + ("shortChannelId" | shortchannelid) :: + ("buried" | bool8) :: + ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: + ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: + ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_NORMAL] + val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: @@ -281,12 +296,20 @@ private[channel] object ChannelCodecs2 { ("channelAnnouncement" | optional(bool8, lengthDelimited(channelAnnouncementCodec))) :: ("channelUpdate" | lengthDelimited(channelUpdateCodec)) :: ("localShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: - ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec)))).as[DATA_NORMAL] + ("remoteShutdown" | optional(bool8, lengthDelimited(shutdownCodec))) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_NORMAL] + + val DATA_SHUTDOWN_COMPAT_03_Codec: Codec[DATA_SHUTDOWN] = ( + ("commitments" | commitmentsCodec) :: + ("localShutdown" | lengthDelimited(shutdownCodec)) :: + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingFeerates" | provide(Option.empty[ClosingFeerates]))).as[DATA_SHUTDOWN] val DATA_SHUTDOWN_Codec: Codec[DATA_SHUTDOWN] = ( ("commitments" | commitmentsCodec) :: ("localShutdown" | lengthDelimited(shutdownCodec)) :: - ("remoteShutdown" | lengthDelimited(shutdownCodec))).as[DATA_SHUTDOWN] + ("remoteShutdown" | lengthDelimited(shutdownCodec)) :: + ("closingFeerates" | optional(bool8, closingFeeratesCodec))).as[DATA_SHUTDOWN] val DATA_NEGOTIATING_Codec: Codec[DATA_NEGOTIATING] = ( ("commitments" | commitmentsCodec) :: @@ -312,13 +335,16 @@ private[channel] object ChannelCodecs2 { ("remoteChannelReestablish" | channelReestablishCodec)).as[DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT] } + // Order matters! val stateDataCodec: Codec[HasCommitments] = discriminated[HasCommitments].by(uint16) - .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) - .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) - .typecase(0x02, Codecs.DATA_NORMAL_Codec) - .typecase(0x03, Codecs.DATA_SHUTDOWN_Codec) - .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) - .typecase(0x05, Codecs.DATA_CLOSING_Codec) + .typecase(0x08, Codecs.DATA_SHUTDOWN_Codec) + .typecase(0x07, Codecs.DATA_NORMAL_Codec) .typecase(0x06, Codecs.DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT_Codec) + .typecase(0x05, Codecs.DATA_CLOSING_Codec) + .typecase(0x04, Codecs.DATA_NEGOTIATING_Codec) + .typecase(0x03, Codecs.DATA_SHUTDOWN_COMPAT_03_Codec) + .typecase(0x02, Codecs.DATA_NORMAL_COMPAT_02_Codec) + .typecase(0x01, Codecs.DATA_WAIT_FOR_FUNDING_LOCKED_Codec) + .typecase(0x00, Codecs.DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 93d41800d6..aace48b01e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -185,7 +185,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT awaitCond(latch.getCount == 0, interval = 1 second, max = 2 minutes) val sender = TestProbe() awaitCond({ - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsgType[CommandResponse[CMD_CLOSE]].isInstanceOf[RES_SUCCESS[CMD_CLOSE]] }, interval = 1 second, max = 30 seconds) @@ -203,7 +203,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT awaitCond(latch.getCount == 0, interval = 1 second, max = 2 minutes) val sender = TestProbe() awaitCond({ - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c val resa = sender.expectMsgType[CommandResponse[CMD_CLOSE]] sender.send(bob, c) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index c04ef45143..0f0c3c4e37 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -245,7 +245,7 @@ trait StateTestsHelperMethods extends TestKitBase { def mutualClose(s: TestFSMRef[State, Data, Channel], r: TestFSMRef[State, Data, Channel], s2r: TestProbe, r2s: TestProbe, s2blockchain: TestProbe, r2blockchain: TestProbe): Unit = { val sender = TestProbe() // s initiates a closing - s ! CMD_CLOSE(sender.ref, None) + s ! CMD_CLOSE(sender.ref, None, None) s2r.expectMsgType[Shutdown] s2r.forward(r) r2s.expectMsgType[Shutdown] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 03adaece48..ed9e89669d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -186,7 +186,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(alice.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 6f865d27b2..523349154a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -224,7 +224,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) bob ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(bob.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index 75887ff558..0ad9e12696 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -70,7 +70,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(alice.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 84b05cbd4a..42d8a1a634 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -120,7 +120,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) bob ! c sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(bob.stateName == CLOSED) @@ -131,7 +131,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val sender = TestProbe() // this makes sure that our backward-compatibility hack for the ask pattern (which uses context.sender as reply-to) // works before we fully transition to akka typed - val c = CMD_CLOSE(ActorRef.noSender, None) + val c = CMD_CLOSE(ActorRef.noSender, None, None) sender.send(bob, c) sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) awaitCond(bob.stateName == CLOSED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index 8b1029f144..913818478b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -106,7 +106,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_SUCCESS(c, alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED].channelId)) awaitCond(alice.stateName == CLOSED) 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 a4feca32f5..428dce5a8c 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 @@ -191,7 +191,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_CONFIRMED))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index bca805655f..36f82675a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -122,7 +122,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_FAILURE(c, CommandUnavailableInThisState(channelId(alice), "close", WAIT_FOR_FUNDING_LOCKED))) } 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 c69c8e9df9..d5437e00c7 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 @@ -386,7 +386,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) @@ -408,7 +408,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! add1 sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]] // at the same time bob initiates a closing - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] // this command will be received by alice right after having received the shutdown val add2 = CMD_ADD_HTLC(sender.ref, 100000000 msat, randomBytes32, CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) @@ -1740,7 +1740,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == NORMAL) @@ -1761,7 +1761,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) // this makes sure that our backward-compatibility hack for the ask pattern (which uses context.sender as reply-to) // works before we fully transition to akka typed - val c = CMD_CLOSE(ActorRef.noSender, None) + val c = CMD_CLOSE(ActorRef.noSender, None, None) sender.send(alice, c) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] @@ -1773,7 +1773,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, CannotCloseWithUnsignedOutgoingHtlcs]] } @@ -1781,7 +1781,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] bob2alice.expectMsgType[Shutdown] } @@ -1789,7 +1789,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_CLOSE (with invalid final script)") { f => import f._ val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, Some(hex"00112233445566778899")) + alice ! CMD_CLOSE(sender.ref, Some(hex"00112233445566778899"), None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } @@ -1798,7 +1798,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == NORMAL) @@ -1809,12 +1809,12 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty) - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == NORMAL) awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] } @@ -1825,7 +1825,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] // actual test begins - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == NORMAL) @@ -1836,13 +1836,13 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() alice ! CMD_UPDATE_FEE(FeeratePerKw(20000 sat), commit = false) alice2bob.expectMsgType[UpdateFee] - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, CannotCloseWithUnsignedOutgoingUpdateFee]] alice2bob.expectNoMsg(100 millis) // once alice signs, the channel can be closed alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] alice2bob.expectMsgType[Shutdown] awaitCond(alice.stateName == NORMAL) @@ -1870,7 +1870,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) bob2alice.expectMsgType[Shutdown] // actual test begins bob2alice.forward(alice) @@ -1907,7 +1907,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with alice2bob.forward(bob) val sig = alice2bob.expectMsgType[CommitSig] // Bob initiates a close before receiving the signature. - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) alice2bob.forward(bob, sig) @@ -1942,7 +1942,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val sender = TestProbe() addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) bob2alice.expectMsgType[Shutdown] // actual test begins bob ! Shutdown(ByteVector32.Zeroes, hex"00112233445566778899") @@ -1978,7 +1978,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) alice ! CMD_SIGN() alice2bob.expectMsgType[CommitSig] - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) bob2alice.expectMsgType[Shutdown] // actual test begins bob2alice.forward(alice) @@ -1990,7 +1990,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() // let's make bob send a Shutdown message - bob ! CMD_CLOSE(sender.ref, None) + bob ! CMD_CLOSE(sender.ref, None, None) bob2alice.expectMsgType[Shutdown] // this is just so we have something to sign addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) 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 8dae3f9f88..e8f5ad8d25 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 @@ -430,7 +430,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] // We initiate a mutual close - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob) bob2alice.expectMsgType[Shutdown] @@ -617,7 +617,7 @@ class OfflineStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // alice initiates a shutdown val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) alice2bob.expectMsgType[Shutdown] testUpdateFeeOnReconnect(f, shouldUpdateFee = false) 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 acb29237bb..162da0015d 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 @@ -89,7 +89,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit relayerB.expectMsgType[RelayForward] relayerB.expectMsgType[RelayForward] // alice initiates a closing - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob) bob2alice.expectMsgType[Shutdown] @@ -838,7 +838,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit test("recv CMD_CLOSE") { f => import f._ val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] } 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 f5704626aa..fc58df982f 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 @@ -16,10 +16,8 @@ package fr.acinq.eclair.channel.states.g -import akka.event.LoggingAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, SatoshiLong} -import fr.acinq.eclair.TestConstants.Bob +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing @@ -27,11 +25,11 @@ import fr.acinq.eclair.channel.TxPublisher.{PublishRawTx, PublishTx} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.{StateTestsBase, StateTestsTags} import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange +import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown, TlvStream} +import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -45,122 +43,347 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike override def withFixture(test: OneArgTest): Outcome = { val setup = init() - import setup._ within(30 seconds) { reachNormal(setup, test.tags) - val sender = TestProbe() - // alice initiates a closing - if (test.tags.contains("fee2")) { - alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(4319 sat))) - bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(4319 sat))) - } - else { - alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - } - bob ! CMD_CLOSE(sender.ref, None) - bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) - alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateName == NEGOTIATING) - // NB: at this point, alice has already computed and sent the first ClosingSigned message - // In order to force a fee negotiation, we will change the current fee before forwarding - // the Shutdown message to alice, so that alice computes a different initial closing fee. - if (test.tags.contains("fee2")) { - alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(4316 sat))) - bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(4316 sat))) - } else { - alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) - bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) - } - alice2bob.forward(bob) - awaitCond(bob.stateName == NEGOTIATING) withFixture(test.toNoArgTest(setup)) } } + implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging + + def aliceClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None): Unit = { + import f._ + val sender = TestProbe() + alice ! CMD_CLOSE(sender.ref, None, feerates) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + awaitCond(alice.stateName == NEGOTIATING) + awaitCond(bob.stateName == NEGOTIATING) + } + + def bobClose(f: FixtureParam, feerates: Option[ClosingFeerates] = None): Unit = { + import f._ + val sender = TestProbe() + bob ! CMD_CLOSE(sender.ref, None, feerates) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice) + alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob) + awaitCond(alice.stateName == NEGOTIATING) + awaitCond(bob.stateName == NEGOTIATING) + } + test("recv CMD_ADD_HTLC") { f => import f._ + aliceClose(f) alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(sender.ref, 5000000000L msat, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = CltvExpiry(300000), onion = TestConstants.emptyOnionPacket, localOrigin(sender.ref)) + val add = CMD_ADD_HTLC(sender.ref, 5000000000L msat, randomBytes32, CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) alice ! add val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(RES_ADD_FAILED(add, error, None)) - alice2bob.expectNoMsg(200 millis) + alice2bob.expectNoMessage(200 millis) } - def testClosingSigned(f: FixtureParam): Unit = { + private def testClosingSignedDifferentFees(f: FixtureParam, bobInitiates: Boolean = false): Unit = { import f._ - // alice initiates the negotiation + + // alice and bob see different on-chain feerates + alice.feeEstimator.setFeerate(FeeratesPerKw(FeeratePerKw(250 sat), FeeratePerKw(10000 sat), FeeratePerKw(5000 sat), FeeratePerKw(2000 sat), FeeratePerKw(2000 sat), FeeratePerKw(2000 sat), FeeratePerKw(2000 sat), FeeratePerKw(2000 sat), FeeratePerKw(2000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw(FeeratePerKw(250 sat), FeeratePerKw(15000 sat), FeeratePerKw(7500 sat), FeeratePerKw(3000 sat), FeeratePerKw(3000 sat), FeeratePerKw(3000 sat), FeeratePerKw(3000 sat), FeeratePerKw(3000 sat), FeeratePerKw(3000 sat))) + assert(alice.feeTargets.mutualCloseBlockTarget == 2) + assert(bob.feeTargets.mutualCloseBlockTarget == 2) + + if (bobInitiates) { + bobClose(f) + } else { + aliceClose(f) + } + + // alice is funder so she initiates the negotiation val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5000 sat/kw + assert(aliceCloseSig1.feeRange_opt.nonEmpty) + assert(aliceCloseSig1.feeRange_opt.get.min < aliceCloseSig1.feeSatoshis) + assert(aliceCloseSig1.feeSatoshis < aliceCloseSig1.feeRange_opt.get.max) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.length === 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) alice2bob.forward(bob) - // bob answers with a counter proposition + // bob answers with a counter proposition in alice's fee range val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis > bobCloseSig1.feeSatoshis) - // actual test starts here - val initialState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] + assert(aliceCloseSig1.feeRange_opt.get.min < bobCloseSig1.feeSatoshis) + assert(bobCloseSig1.feeSatoshis < aliceCloseSig1.feeRange_opt.get.max) + assert(bobCloseSig1.feeRange_opt.nonEmpty) + assert(aliceCloseSig1.feeSatoshis < bobCloseSig1.feeSatoshis) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) bob2alice.forward(alice) + // alice accepts this proposition val aliceCloseSig2 = alice2bob.expectMsgType[ClosingSigned] - // BOLT 2: If the receiver doesn't agree with the fee it SHOULD propose a value strictly between the received fee-satoshis and its previously-sent fee-satoshis - assert(aliceCloseSig2.feeSatoshis < aliceCloseSig1.feeSatoshis && aliceCloseSig2.feeSatoshis > bobCloseSig1.feeSatoshis) - awaitCond(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.map(_.localClosingSigned) == initialState.closingTxProposed.last.map(_.localClosingSigned) :+ aliceCloseSig2) - val Some(closingTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt - assert(closingTx.tx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx - assert(aliceCloseSig2.feeSatoshis > Transactions.weight2fee(TestConstants.anchorOutputsFeeratePerKw, closingTx.tx.weight())) // NB: closing fee is allowed to be higher than commit tx fee when using anchor outputs + assert(aliceCloseSig2.feeSatoshis === bobCloseSig1.feeSatoshis) + alice2bob.forward(bob) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + + val mutualCloseTx = alice2blockchain.expectMsgType[PublishTx].tx + assert(bob2blockchain.expectMsgType[PublishTx].tx === mutualCloseTx) + assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + assert(aliceCloseSig2.feeSatoshis > Transactions.weight2fee(TestConstants.anchorOutputsFeeratePerKw, mutualCloseTx.weight())) // NB: closing fee is allowed to be higher than commit tx fee when using anchor outputs + assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx)) + assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx)) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) === List(mutualCloseTx)) } - test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { - testClosingSigned _ + test("recv ClosingSigned (theirCloseFee != ourCloseFee)") { f => + testClosingSignedDifferentFees(f) } - test("recv ClosingSigned (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { - testClosingSigned _ + test("recv ClosingSigned (theirCloseFee != ourCloseFee, bob starts closing)") { f => + testClosingSignedDifferentFees(f, bobInitiates = true) } - private def testFeeConverge(f: FixtureParam) = { + test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f => + testClosingSignedDifferentFees(f) + } + + test("recv ClosingSigned (theirMinCloseFee > ourCloseFee)") { f => import f._ - var aliceCloseFee, bobCloseFee = 0.sat - do { - aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - alice2bob.forward(bob) - if (!bob2blockchain.msgAvailable) { - bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - bob2alice.forward(alice) - } - } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(2500 sat))) + + aliceClose(f) + val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob) + val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] + assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeSatoshis) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1)") { f => - testFeeConverge(f) + test("recv ClosingSigned (theirMaxCloseFee < ourCloseFee)") { f => + import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(20000 sat))) + + aliceClose(f) + val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob) + val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] + assert(bobCloseSig.feeSatoshis === aliceCloseSig.feeRange_opt.get.max) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 2)", Tag("fee2")) { f => - testFeeConverge(f) + private def testClosingSignedSameFees(f: FixtureParam, bobInitiates: Boolean = false): Unit = { + import f._ + + // alice and bob see the same on-chain feerates + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) + + if (bobInitiates) { + bobClose(f) + } else { + aliceClose(f) + } + + // alice is funder so she initiates the negotiation + val aliceCloseSig1 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceCloseSig1.feeSatoshis === 3370.sat) // matches a feerate of 5 000 sat/kw + assert(aliceCloseSig1.feeRange_opt.nonEmpty) + alice2bob.forward(bob) + // bob agrees with that proposal + val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] + assert(bobCloseSig1.feeSatoshis === aliceCloseSig1.feeSatoshis) + val mutualCloseTx = bob2blockchain.expectMsgType[PublishTx].tx + assert(mutualCloseTx.txOut.length === 2) // NB: in the anchor outputs case, anchors are removed from the closing tx + bob2alice.forward(alice) + assert(alice2blockchain.expectMsgType[PublishTx].tx === mutualCloseTx) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + } + + test("recv ClosingSigned (theirCloseFee == ourCloseFee)") { f => + testClosingSignedSameFees(f) + } + + test("recv ClosingSigned (theirCloseFee == ourCloseFee, bob starts closing)") { f => + testClosingSignedSameFees(f, bobInitiates = true) + } + + test("recv ClosingSigned (theirCloseFee == ourCloseFee, anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f => + testClosingSignedSameFees(f) + } + + test("override on-chain fee estimator (funder)") { f => + import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + aliceClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) + // alice initiates the negotiation with a very low feerate + val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] + assert(aliceCloseSig.feeSatoshis === 1685.sat) + assert(aliceCloseSig.feeRange_opt === Some(FeeRange(1348 sat, 2022 sat))) + alice2bob.forward(bob) + // bob chooses alice's highest fee + val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] + assert(bobCloseSig.feeSatoshis === 2022.sat) + bob2alice.forward(alice) + // alice accepts this proposition + assert(alice2bob.expectMsgType[ClosingSigned].feeSatoshis === 2022.sat) + alice2bob.forward(bob) + val mutualCloseTx = alice2blockchain.expectMsgType[PublishTx].tx + assert(bob2blockchain.expectMsgType[PublishTx].tx === mutualCloseTx) + awaitCond(alice.stateName === CLOSING) + awaitCond(bob.stateName === CLOSING) + } + + test("override on-chain fee estimator (fundee)") { f => + import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bobClose(f, Some(ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(2000 sat), FeeratePerKw(3000 sat)))) + // alice is funder, so bob's override will simply be ignored + val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] + assert(aliceCloseSig.feeSatoshis === 6740.sat) // matches a feerate of 10000 sat/kw + alice2bob.forward(bob) + // bob directly agrees because their fee estimator matches + val bobCloseSig = bob2alice.expectMsgType[ClosingSigned] + assert(aliceCloseSig.feeSatoshis === bobCloseSig.feeSatoshis) + bob2alice.forward(alice) + val mutualCloseTx = alice2blockchain.expectMsgType[PublishTx].tx + assert(bob2blockchain.expectMsgType[PublishTx].tx === mutualCloseTx) + awaitCond(alice.stateName === CLOSING) + awaitCond(bob.stateName === CLOSING) } test("recv ClosingSigned (nothing at stake)", Tag(StateTestsTags.NoPushMsat)) { f => import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(5000 sat))) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bobClose(f) val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis alice2bob.forward(bob) val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis assert(aliceCloseFee === bobCloseFee) - bob2alice.forward(alice) - val mutualCloseTxAlice = alice2blockchain.expectMsgType[PublishTx].tx - val mutualCloseTxBob = bob2blockchain.expectMsgType[PublishTx].tx - assert(mutualCloseTxAlice === mutualCloseTxBob) - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTxAlice)) - assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTxBob)) - assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTxAlice)) - assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.map(_.tx) == List(mutualCloseTxBob)) + bob2blockchain.expectMsgType[PublishTx] + awaitCond(bob.stateName === CLOSING) + } + + private def makeLegacyClosingSigned(f: FixtureParam, closingFee: Satoshi): (ClosingSigned, ClosingSigned) = { + import f._ + val aliceState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] + val aliceKeyManager = alice.underlyingActor.nodeParams.channelKeyManager + val aliceScript = aliceState.localShutdown.scriptPubKey + val bobState = bob.stateData.asInstanceOf[DATA_NEGOTIATING] + val bobKeyManager = bob.underlyingActor.nodeParams.channelKeyManager + val bobScript = bobState.localShutdown.scriptPubKey + val (_, aliceClosingSigned) = Closing.makeClosingTx(aliceKeyManager, aliceState.commitments, aliceScript, bobScript, ClosingFees(closingFee, closingFee, closingFee)) + val (_, bobClosingSigned) = Closing.makeClosingTx(bobKeyManager, bobState.commitments, bobScript, aliceScript, ClosingFees(closingFee, closingFee, closingFee)) + (aliceClosingSigned.copy(tlvStream = TlvStream.empty), bobClosingSigned.copy(tlvStream = TlvStream.empty)) + } + + test("recv ClosingSigned (other side ignores our fee range, funder)") { f => + import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(1000 sat))) + aliceClose(f) + val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] + val Some(FeeRange(_, maxFee)) = aliceClosing1.feeRange_opt + assert(aliceClosing1.feeSatoshis === 674.sat) + assert(maxFee === 1348.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.isEmpty) + // bob makes a proposal outside our fee range + val (_, bobClosing1) = makeLegacyClosingSigned(f, 2500 sat) + bob2alice.send(alice, bobClosing1) + val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing1.feeSatoshis < aliceClosing2.feeSatoshis) + assert(aliceClosing2.feeSatoshis < 1600.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (_, bobClosing2) = makeLegacyClosingSigned(f, 2000 sat) + bob2alice.send(alice, bobClosing2) + val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing2.feeSatoshis < aliceClosing3.feeSatoshis) + assert(aliceClosing3.feeSatoshis < 1800.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (_, bobClosing3) = makeLegacyClosingSigned(f, 1800 sat) + bob2alice.send(alice, bobClosing3) + val aliceClosing4 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing3.feeSatoshis < aliceClosing4.feeSatoshis) + assert(aliceClosing4.feeSatoshis < 1800.sat) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (_, bobClosing4) = makeLegacyClosingSigned(f, aliceClosing4.feeSatoshis) + bob2alice.send(alice, bobClosing4) + awaitCond(alice.stateName === CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) + assert(alice2blockchain.expectMsgType[PublishTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + } + + test("recv ClosingSigned (other side ignores our fee range, fundee)") { f => + import f._ + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) + bobClose(f) + // alice starts with a very low proposal + val (aliceClosing1, _) = makeLegacyClosingSigned(f, 500 sat) + alice2bob.send(bob, aliceClosing1) + val bobClosing1 = bob2alice.expectMsgType[ClosingSigned] + assert(3000.sat < bobClosing1.feeSatoshis) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 1) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (aliceClosing2, _) = makeLegacyClosingSigned(f, 750 sat) + alice2bob.send(bob, aliceClosing2) + val bobClosing2 = bob2alice.expectMsgType[ClosingSigned] + assert(bobClosing2.feeSatoshis < bobClosing1.feeSatoshis) + assert(2000.sat < bobClosing2.feeSatoshis) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 2) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (aliceClosing3, _) = makeLegacyClosingSigned(f, 1000 sat) + alice2bob.send(bob, aliceClosing3) + val bobClosing3 = bob2alice.expectMsgType[ClosingSigned] + assert(bobClosing3.feeSatoshis < bobClosing2.feeSatoshis) + assert(1500.sat < bobClosing3.feeSatoshis) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 3) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (aliceClosing4, _) = makeLegacyClosingSigned(f, 1300 sat) + alice2bob.send(bob, aliceClosing4) + val bobClosing4 = bob2alice.expectMsgType[ClosingSigned] + assert(bobClosing4.feeSatoshis < bobClosing3.feeSatoshis) + assert(1300.sat < bobClosing4.feeSatoshis) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.length === 4) + assert(bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt.nonEmpty) + val (aliceClosing5, _) = makeLegacyClosingSigned(f, bobClosing4.feeSatoshis) + alice2bob.send(bob, aliceClosing5) + awaitCond(bob.stateName === CLOSING) + assert(bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) + assert(bob2blockchain.expectMsgType[PublishTx].tx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + } + + test("recv ClosingSigned (other side ignores our fee range, max iterations reached)") { f => + import f._ + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(1000 sat))) + aliceClose(f) + for (_ <- 1 to Channel.MAX_NEGOTIATION_ITERATIONS) { + val aliceClosing = alice2bob.expectMsgType[ClosingSigned] + val Some(FeeRange(_, aliceMaxFee)) = aliceClosing.feeRange_opt + val bobNextFee = (aliceClosing.feeSatoshis + 500.sat).max(aliceMaxFee + 1.sat) + val (_, bobClosing) = makeLegacyClosingSigned(f, bobNextFee) + bob2alice.send(alice, bobClosing) + } + awaitCond(alice.stateName === CLOSING) + assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.length === 1) + assert(alice2blockchain.expectMsgType[PublishTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) } test("recv ClosingSigned (fee too high)") { f => import f._ + bobClose(f) val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] - val sender = TestProbe() val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx - sender.send(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later + alice2bob.forward(bob, aliceCloseSig.copy(feeSatoshis = 99000 sat)) // sig doesn't matter, it is checked later val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray).startsWith("invalid close fee: fee_satoshis=Satoshi(99000)")) assert(bob2blockchain.expectMsgType[PublishRawTx].tx === tx) @@ -170,6 +393,7 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("recv ClosingSigned (invalid sig)") { f => import f._ + aliceClose(f) val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] val tx = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) @@ -182,65 +406,61 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("recv BITCOIN_FUNDING_SPENT (counterparty's mutual close)") { f => import f._ - var aliceCloseFee, bobCloseFee = 0.sat - do { - aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - alice2bob.forward(bob) - if (!bob2blockchain.msgAvailable) { - bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - if (aliceCloseFee != bobCloseFee) { - bob2alice.forward(alice) - } - } - } while (!alice2blockchain.msgAvailable && !bob2blockchain.msgAvailable) - // at this point alice and bob have converged on closing fees, but alice has not yet received the final signature whereas bob has + aliceClose(f) + val aliceCloseSig = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob, aliceCloseSig) + // at this point alice and bob agree on closing fees, but alice has not yet received the final signature whereas bob has // bob publishes the mutual close and alice is notified that the funding tx has been spent - // actual test starts here - assert(alice.stateName == NEGOTIATING) + assert(alice.stateName === NEGOTIATING) val mutualCloseTx = bob2blockchain.expectMsgType[PublishTx].tx assert(bob2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(mutualCloseTx)) alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx) assert(alice2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx) assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === mutualCloseTx.txid) - alice2blockchain.expectNoMsg(100 millis) - assert(alice.stateName == CLOSING) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateName === CLOSING) } test("recv BITCOIN_FUNDING_SPENT (an older mutual close)") { f => import f._ - val aliceClose1 = alice2bob.expectMsgType[ClosingSigned] - alice2bob.forward(bob) - val bobClose1 = bob2alice.expectMsgType[ClosingSigned] - bob2alice.forward(alice) - val aliceClose2 = alice2bob.expectMsgType[ClosingSigned] - assert(aliceClose2.feeSatoshis != bobClose1.feeSatoshis) - // at this point alice and bob have not yet converged on closing fees, but bob decides to publish a mutual close with one of the previous sigs - val d = bob.stateData.asInstanceOf[DATA_NEGOTIATING] - implicit val log: LoggingAdapter = bob.underlyingActor.implicitLog - val Right(bobClosingTx) = Closing.checkClosingSignature(Bob.channelKeyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, aliceClose1.feeSatoshis, aliceClose1.signature) - - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobClosingTx.tx) - assert(alice2blockchain.expectMsgType[PublishRawTx].tx === bobClosingTx.tx) - assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === bobClosingTx.tx.txid) - alice2blockchain.expectNoMsg(100 millis) - assert(alice.stateName == CLOSING) + alice.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(1000 sat))) + aliceClose(f) + val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob, aliceClosing1) + bob2alice.expectMsgType[ClosingSigned] + val Some(firstMutualCloseTx) = bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + val (_, bobClosing1) = makeLegacyClosingSigned(f, 3000 sat) + assert(bobClosing1.feeSatoshis !== aliceClosing1.feeSatoshis) + bob2alice.send(alice, bobClosing1) + val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing2.feeSatoshis !== bobClosing1.feeSatoshis) + val Some(latestMutualCloseTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + assert(firstMutualCloseTx.tx.txid !== latestMutualCloseTx.tx.txid) + // at this point bob will receive a new signature, but he decides instead to publish the first mutual close + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[PublishRawTx].tx === firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[WatchConfirmed].txId === firstMutualCloseTx.tx.txid) + alice2blockchain.expectNoMessage(100 millis) + assert(alice.stateName === CLOSING) } test("recv CMD_CLOSE") { f => import f._ + bobClose(f) + alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] } test("recv Error") { f => import f._ + bobClose(f) + alice2bob.expectMsgType[ClosingSigned] val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.publishableTxs.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) assert(alice2blockchain.expectMsgType[PublishRawTx].tx === tx) - alice2blockchain.expectMsgType[PublishTx] - assert(alice2blockchain.expectMsgType[WatchConfirmed].event === BITCOIN_TX_CONFIRMED(tx)) } } 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 263038b963..17dec05a17 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 @@ -113,26 +113,6 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with } } - test("start fee negotiation from configured block target") { f => - import f._ - - alice.feeEstimator.setFeerate(FeeratesPerKw(FeeratePerKw(50 sat), FeeratePerKw(100 sat), FeeratePerKw(250 sat), FeeratePerKw(350 sat), FeeratePerKw(450 sat), FeeratePerKw(600 sat), FeeratePerKw(800 sat), FeeratePerKw(900 sat), FeeratePerKw(1000 sat))) - - val sender = TestProbe() - // alice initiates a closing - alice ! CMD_CLOSE(sender.ref, None) - alice2bob.expectMsgType[Shutdown] - alice2bob.forward(bob) - bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) - val closing = alice2bob.expectMsgType[ClosingSigned] - val aliceData = alice.stateData.asInstanceOf[DATA_NEGOTIATING] - val mutualClosingFeeRate = alice.feeEstimator.getFeeratePerKw(alice.feeTargets.mutualCloseBlockTarget) - val expectedFirstProposedFee = Closing.firstClosingFee(aliceData.commitments, aliceData.localShutdown.scriptPubKey, aliceData.remoteShutdown.scriptPubKey, mutualClosingFeeRate)(akka.event.NoLogging) - assert(alice.feeTargets.mutualCloseBlockTarget == 2 && mutualClosingFeeRate == FeeratePerKw(250 sat)) - assert(closing.feeSatoshis == expectedFirstProposedFee) - } - test("recv BITCOIN_FUNDING_PUBLISH_FAILED", Tag("funding_unconfirmed")) { f => import f._ val sender = TestProbe() @@ -287,31 +267,28 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion === channelVersion) - // alice initiates a closing - alice ! CMD_CLOSE(sender.ref, None) + // alice initiates a closing with a low fee + alice ! CMD_CLOSE(sender.ref, None, Some(ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(1000 sat)))) alice2bob.expectMsgType[Shutdown] alice2bob.forward(bob) bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) - // agreeing on a closing fee val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis - bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(100 sat))) alice2bob.forward(bob) val bobCloseFee = bob2alice.expectMsgType[ClosingSigned].feeSatoshis - bob2alice.forward(alice) - // they don't converge yet, but alice has a publishable commit tx now + // they don't converge yet, but bob has a publishable commit tx now assert(aliceCloseFee != bobCloseFee) - val Some(mutualCloseTx) = alice.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt - // let's make alice publish this closing tx - alice ! Error(ByteVector32.Zeroes, "") - awaitCond(alice.stateName == CLOSING) - assert(alice2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx.tx) - assert(mutualCloseTx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) + val Some(mutualCloseTx) = bob.stateData.asInstanceOf[DATA_NEGOTIATING].bestUnpublishedClosingTx_opt + // let's make bob publish this closing tx + bob ! Error(ByteVector32.Zeroes, "") + awaitCond(bob.stateName == CLOSING) + assert(bob2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx.tx) + assert(mutualCloseTx === bob.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.last) // actual test starts here - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx.tx) - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx) - awaitCond(alice.stateName == CLOSED) + bob ! WatchEventSpent(BITCOIN_FUNDING_SPENT, mutualCloseTx.tx) + bob ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(mutualCloseTx.tx), 0, 0, mutualCloseTx.tx) + awaitCond(bob.stateName == CLOSED) } test("recv BITCOIN_FUNDING_SPENT (mutual close before converging)") { f => @@ -1602,7 +1579,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val sender = TestProbe() - val c = CMD_CLOSE(sender.ref, None) + val c = CMD_CLOSE(sender.ref, None, None) alice ! c sender.expectMsg(RES_FAILURE(c, ClosingAlreadyInProgress(channelId(alice)))) } 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 99655f6845..f730425353 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 @@ -545,7 +545,7 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(fundee.register, Register.Forward(sender.ref, channelId, CMD_GETSTATEDATA(ActorRef.noSender))) val finalPubKeyScriptF = sender.expectMsgType[RES_GETSTATEDATA[DATA_NORMAL]].data.commitments.localParams.defaultFinalScriptPubKey - fundee.register ! Register.Forward(sender.ref, channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF))) + fundee.register ! Register.Forward(sender.ref, channelId, CMD_CLOSE(sender.ref, Some(finalPubKeyScriptF), None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] // we then wait for C and F to negotiate the closing fee awaitCond(stateListener.expectMsgType[ChannelStateChanged].currentState == CLOSING, max = 60 seconds) 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 79299a9966..7a1c22e3de 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 @@ -122,7 +122,7 @@ class ChannelCodecsSpec extends AnyFunSuite { // and we encode with new codec val newbin = stateDataCodec.encode(oldnormal).require.bytes // make sure that encoding used the new codec - assert(newbin.startsWith(hex"020002")) + assert(newbin.startsWith(hex"020007")) // make sure that round-trip yields the same data val newnormal = stateDataCodec.decode(newbin.bits).require.value assert(newnormal === oldnormal) @@ -304,7 +304,7 @@ object ChannelCodecsSpec { remotePerCommitmentSecrets = ShaChain.init, channelId = htlcs.headOption.map(_.add.channelId).getOrElse(ByteVector32.Zeroes)) - DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None) + DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None, None) } object JsonSupport { 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 856fd70443..10ffd2a493 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,12 @@ package fr.acinq.eclair.wire.internal.channel.version2 import fr.acinq.bitcoin.{OutPoint, Transaction} +import fr.acinq.eclair.channel.{DATA_NORMAL, DATA_SHUTDOWN} import fr.acinq.eclair.randomBytes32 import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.Codecs._ +import fr.acinq.eclair.wire.internal.channel.version2.ChannelCodecs2.stateDataCodec import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.HexStringSyntax class ChannelCodecs2Spec extends AnyFunSuite { @@ -17,4 +20,26 @@ class ChannelCodecs2Spec extends AnyFunSuite { assert(spentMapCodec.decodeValue(spentMapCodec.encode(map).require).require === map) } + test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { + val oldBin = hex"00020000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000094a9267a1f2b86e492dacf939afd1561a0e42ed248d1a09f711204c176fef1b0f80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff16001429acc00c77e6894e41e417ae712e557d7bba1c460000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f021503c2a3cb942c336afbe484a00452f5b027378e2124c7ea452f05c26ab89591c18f03019eec506c69765552b397d706dd276f0718c82e0a49224fbaccfad75f81b2f502d3d560591f03da622f338b0988bbf0f612fafa4f1b22ed51398cf7a07c5f4192025d67e71808e128eb21c82881cef04a510067a50d0f61f2f455bcfb7e168ae7ad00000003028a820000000000000000010007fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000" ++ hex"000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e00fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000002c2322200000000008af34a0245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aefd01d9020000000001015d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000e136df8005401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b450c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e750c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d924390200000000001600141fa232bbb376f103dd015631dc8d6f4f9d306122241c0b00000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc040046304302204b7524262f7aa8fbf4e9fadf476ba42b4202617b16e4d1e1ac7c9db1adb8a126021f59622119b96d12a66c6f18f12b5c04764c576f9ccb4216049ddd6e719cef9201473044022003f76c8bbfc91e5323ddc1d7a95e8a3c7707de898e6a50fd5e6562b8412e32fb022010b2bb7edcf519a9451b3883832a39a0f9008903b52fb2db90e6e371bf20307b014752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52aec34a862000030224489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000002b401f00000000000022002019cccc41d4120d54831ad41b091b1e52b8902711365f1cbd9a44a7b5a3dc22b48576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914d3260c3a0710948e34d708a99ff708f9c257ac5288ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac000000000000000000015a050000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b0600000000000000000040ac4bb46e1b736a6fc76a4d86b24df1e9f9f91013025209ba64daf333863a2bde59a51a7af931bf0b07d6154daa30ed3a9c8c33d173c75765b46674a4e249bb734061aa4d418884ef272ea2bd65d745c42b5bd5f6781b7f876402b10eb505c60d086ddf3e108528e65f4712503114c24a20c8fab7867761677401defa780c4574f50224489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000002b50c3000000000000220020653fa8ccacaea967b6bbfe9411c3f812cb44c4a18d4f6806fd81512009f706e78576a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c820120876475527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae67a914caf9c0d315f1ca821720d6911e090095585cee2788ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac010000000000000000016aa90000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc101b0600000000000000000040dc5ef2218b256e0db9981ab591cf830819b62ae2068b6c4d19a85ba03153ff39316c14c1570a8fdbc2765b38a16c74e76ec7190f47aa3754e08c770d8c7e918640678fd252d77d34b3ecefff1fa369e92dad0b4a2632efe13974e538e13e14780c4c6c21838a7ead4878c9836405e45333651de6cbc012af4d6c1bda4a0039d5aa0124489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac020000002b50c3000000000000220020c9c8b5e68eea1a6f32394124873d68c66b1cab8ffe657c852696e1a1597783d98b76a91423ca5b93a48d00f645081be0479fa799c70731a28763ac672102897afb6799af1c1af49fe4c96800ee1cea77553c0b5ae9b8cca5347238cf5be77c8201208763a9149bd34d5a8ea2d91b8ac4d1fb4ce79bcfd481c1f088527c21035a26304d46e27993c218a0c644a613ca87630c8f0e1db5f112a01c3af26cdb8f52ae677503101b06b175ac68685e0200000001489133c313ebc91202d43a19b2034b1b2df6ccc257712bb0c38cd1d3141e42ac02000000000000000001daa70000000000002200207121fb3dfd089e1a7b7cf0934e4cc42e99678c7db90927a358985a6471d0cffc00000000ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f0000000000000000401e7b146982a595ed1a95e1225312dce24f45237477efca22c759c9cb16c4a6bc1c9e8834a18eb0c6c389a14ea03fb7062f788003c5c58e738d78e28e004014a64013f1f51471ecdb32897a5f604a254334f7f5fa26de5fc63b96c942db2196ae9e43b34f00054e0c58a061d5ae5f4938b5d449b8f9371edd76efdfcce758a102c500000000000000010004fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e0fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d086fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a260270400002710000000000bebc200000000002c232220b8e78d93ef82c5351ede6c6aceaaafb806ef6ff23deb81fd78e7a9bebfcdeb5302f6bed70479b4d98f5c93f32b382524233983ebebd14b2b50fe35142766300efe000000000000000000000003fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e5770" ++ hex"4fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bddfd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fd05ac00805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200e0000000000000004000000000000000300040000000000000000000319c0569a7b4f4021827771a963002b8b00000000000000010003d56d81344fe44d1f8f1a95850431563c00000000000000020003d33fd4c8d4ad4ce3880bf79a4856ca2100000000000000030003efcb10168b144c8bb4f694a3b98a129f000000000000000002000700fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000003d0900559bb949364ed92a64f449cd6ee3eaba2e607595d3cecb36b06b145baaa69bc000061b100002860fd058656f185036e81a64fe19034d29223d3620bdf4bf2ff3c1def9c6bdd70c2cda7f660e1b202672741bc3258b04fb755fbbb1350261e7992bce930e256bd5d8d4fde61365b7b24904b788ef2040fdd6eb87f97c2d2f29eab4291d9a28b5da306f28b98d01d93517b203a028199d423a3545aa17522c63247f73af7b63335b0b48e4b875c69b42f4cb1573bb3e5fd68837f90c50b0161f067a9eafd9a0790e53ed5053ce60ffff410e4b16a4b7bc5a52c57e78ef266100c9f79753f81878c08e5dbd4d80c6e46a339578d8ac8c572df77ef614800cedc460c06878c0da97908067729ed35e3afe919071724f89ab736f5791a9c9b5d422136332213434c836e2ceb9fce0e2e96a9a6d7befe8c132867d5fafea1a7809ddd6b3a89c8ef6ea83028d3e2cca00f1bc6e12ea8b67e91a98acaa2edebaf6dd3a18c655b6b1fbbff5c641f8002780758d05f1f39c9470a124a5add314abd2262142733120747cda2f1d9eb90d68ecb9c7fbab23d73a35f2a20a2a365de6cd678d53bd5bd9bd518333d04e8e678b5d08f028982dad08c80be7d8fbb0638dd814232224c687f8321baf96ed8b39a1e9ab52dfd69d8eed79ac3f5a2c480a585bff038c92b367743317b937d969cdd533ae1d797a789ff7994f86a0d6cae470b64ebddbc478573af347a110dd1feaaeb4779441ec439cfdbafaba870105efd86b9d85a4df7ddb9b09f5b6b4144cd1fad5932df37ebf19a62648659fc1969142310a5cc9b4d0c48ba6bb0f863ed53a0b75fe1ee6515a46993f95be2e34166408b54a43e55c4802b37ac902fb4c8367ce38990d07ed3104d0728d327d3b9de6452b520f9af534505885788109ec78c1176ca0864d28422e826cc83f821b7eaf028d6a7e350b3037d0fe58d1d4e18113c8f61913932e71c0f334402534d8663f15445f900fb9dc6b3a93223868167be26fcbd70c0459eee37f81fd539c319eb0b04bd478b94b5f4cd23b4d496c2bdd6e8a154fd76c4ecbdf7647fe9e7be88c6a3a8e7696e2e596dfc25ba798db6ca331d135e9ce7c0aab9721d3f70ca53354f96ecd028236259b9b0d9e0bbf73c8e841b1d4276214f7be8feb525c91d39910b0e091997a2b89e945806e93cd325cb51463b0729f1a519334038cba09653799ef533a49e812e86b81af7e5099a02ca11c2b17dfc8b9e51a57a20546f2c92826676ebcb4f64fe7cc77424388dfec7199179cb125bb4613c8bf05edc4173987d7d5ae0fcbfa08a1e5ea2d6b01406d740b49c5b1a68da585549590c3ec13479efa3136c5ade68057fe173ace55593ceca8440372b03f332969866d1bfbce3fe9dd907d27593b8b2ad25eb4b12afd0f3abe7931ba7789c84a3ed65a03df06d998a9956043a4de786c359bdd58f0b9e5cfc32bc709b626ce8e63f3997a0e9f784f6b94e342b4710553e805cc8399191254189058ec75a15556467b2456d9b38a7e4d15cf59727ad2c32f0daaef1748be04311ff484479eb31f7eff9e32b816fa40f40430e801c15e931294b39ec4d6c93e8130fb4a1b53ca6471886a33b10b46d6973d02bbe3b39345e121473ea4ba41f3cfced07c3a613393e38b8ca01c353ccab96128093bb5fd000978b11ae6f7c1fedf48c3682119d1b44629d02ea3800d2e247ef1e78e527e5d574b4dd144e1fe1b0c6551442d419baba300258657792947443747b29b6fb9417d3c57536de6aa379c02f3addbd2066554189ca817c7f57331c972c30e3ed06fc7e521b8e2bb57c023dff9816f260a153f1ffd55df2a2fae568f3dc734d62ab47a77c54772b5e4cf2758c912cb473ec372173d9eda0c58b103ab12d11dcb976a40816ecfa8b9c9384f6415017555933652e77ae20e14ea9ac25b5811fc03364c4883bb78c82e29f8a8892415ff652b782642bf6036b3e2200efffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee0000000000000003000000000007a120bf5899359b258acdec0ea500e437267aa6ba18e8509e15df74518f6ba7aa348f00061b1000036d768d1d63f1c9f09c252b5b48cf4d2db3f4935150c551391b37727723168982d7e58ce2a769313e461a0f3791a8c0208a769f5bdf4d57fbd0a9de104a0b1bbc1ab25ebbf87d1bc86dbbd42fd0dc0ff18a7ebfdd692c7dc3b95d095052540ce7801f3e25cbbcfd9dab857bc39624f59facbfacef5ab1e1e1f889b85f3b1f2a580cd660b73aa662fb15487722bd3c3f93d272725289136f2ee0fae4efe1afea25e6d4fc8334a47ad62d22be06605a15538dbd2a81311c4489b91d1cb143743e4570a6345c4f035c060aaf287ef66e7ebbe7b9037c10b66e087827478fdf76a02d25fe90e0f8228c1edfcb12eee3dd4e505a6c5a7bf2f5954ebb5560cd8c7f8b8f3f4ddca41a48a0d6c0e1092dcadc853752f459486bed349213cc15044585255842717ab70a3de3f0eedeecca10112c85a8bc248dc66c883a62288b49588d9fcc048c51081e94d65bd4731e7d71fba13693b82d2831d3bea370918aa5ddf1cc4f0d5015bb8dfb951ab13fbb9d26d5cb83dc980fc36f1712d616ce24d2c530253320f4f322f093a605ea426c577544f2983efc80be56791f443652c2233039a68f966c2f0b6351068616755dd2036b6226244d394a2b5b59160217603149901e8abe19a2bf404f384c2ed7fb0c5e470ee5ee8561f58f66bda729c2c8816853ad2357a009e537efcb4a28e845ca616be917b15aa6b8eb280bfcb321ea62fff21168b8ed54d58ccfdeee0e7752bfc0f02549d76615c85dd1e152a85ee931b34f436439e2233740328ba504c49f9764e1dca645ebf6a1377310ab53b68b4d0a6e6c952068249b86c29061725035db8d294ab9c56901485814735aa2a8d6987b1a19ced65a332f97751c4cd8a27093851f7775e5314078c04d254754d976bed2dbd2e6ecda62e9a0c7fd95299b4b13a54c9498d384210fb42d3b6bc5d8f0d42e42879f86c21eb7c5c6d1bffdd598b8f3cfcb75df159f1125a65f960637c62c7c5632d73b7b4b0544082008ede22d87e79e20eb08be0817650fefcd111de48ba2be02a7b080275c991a0ee4445dab89312644c7cf4101895e2dbcaad7d87e8e3b13e62751861b204a7e6f5a476eab0817c294d59aa0247903077d4cbe4a98e7984d2b04623d2b2ef4c650b43db15541ede229c12c045529b5c77993eb6acbdc28d812a486b5957fb996731980555bdd59ad824a882ebe1a77cbe6b9035f1c69dd01b2a27a47be5febfa65c721354e70071b07db4ebc2f01d143587c1b32a5337dd010d2a76a7773f4a7c665b4cfe4a61b103b1c319d85e007eb99b52400cd8776697e1d6118197655bf7bd0a5e7f4594bf36a2706128d5f5c3ee166b586c4d515611f4597a4c1088c1853a5959f73830cb973ea922d6211ee7d9b1d67b1025486f8f3c72a517d0d48d9a57d64c0f48e513c3b09e14ce91b515a87f3035ab55d241ccb12108dd299a362af26af96ada920202dfe26d456065717a85e6bbec540637059d82480f6c917a11434a9be5fee5ba33a8552b7b0e59f123991525e1dc14bfdfd109625b2df477bce565045375dacf6ffe99081914fc9f64df7ef8eb26801ce01be083555fd2f8a338a33c07f484b01a310ee420f5c932cfc5a8f6d32a20c3915a188833ac5775500c5d2b73e4ea595512e869f91f83de8e048c804ec8cd6de45b7ad264bcfb3c235325d3c09673a2a94e60736ceada065cefb1e355bd35bd4b56ae513343e85a3d840c2211da2d1941112318f97f825e1714f74fc2d33430196cca2423f43641b0d75b4b1a1a1ef0f07af31fda06220a2628aaab9f303cb4fe6d73fa9b39e8f6083af3bbca1becdd646afbd888c560bbf7cf4fcf5c02f712726eb23b03fe5c290df63a85ed1026cee07ab0d2bb868aa4dd6594252cb75ad5d11d9c53c5238047ea3d311c1443d478dd283a2602704fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000100000000007a1200ffb841fb291ecf09ae7b5dcec1feeb45ba196ca02d6b9e50ae2b3dcd9ca5d72500061b100003acd0a3acb9b4f9540678dc9324480bb3d4f54e5e007739c9c1d600bff75dfbbd0191e75c0a2d810a6ec5b03d02cffbf1a66123c87790e66eee8d416caf82e7ea7090a62fe14276fa88af32ab9793f7a100de5525eedf71967c13c8d361246d98b78cc74383e714f88899f34407644c14874046b3b722186015c07b8db042db955c91cb14abbf3cfa646aea81ad15bf67108a763539c64c5a8e8115d46e063e956671e8ea8d8fd638a6414d71e9b475ebe070da7faf75a898f29048ab5a2b6c7e3a72a178b8e470e8375f539ebf6284d15486c5a8774d46ea164ba2b62181f47623fd987ff5958550c962a193638679b79fcd477fdf2a09c0fa879bb22cb493fbaed27518f5cb265741535b4ab14246077ca18f11ece7aaa0e01ec5bf02c3c3b541ea08bf254df123079c1538e266dcd3161bf1b9ed41d873f1491906e1459ba51ac9dd95e783598d3c356e0cc5b98c2a96b148f55f102a9810181eedd46cd00b445d861baeeac46eba469435aa4ddc877bd68b53f4d005aa2566d356cc344aafcdc86abd774ea28cc838d2fdc541c4b6da494a96e128b8c2abab4b21b3ae2646cdcc3528ef6fd8587b3a0636ead67a62309fb003afdc14177d329b062622313e9dee912847763c68678df663a39b89c69efdb6d916d5754534bdca9030955cbcbae6fb7ff1df6282175cd37a30a904418b976af05809f0e0e7e4b4e2ec018f1e9c6bcbe7a7822c8699669946f5e684671d63e68cc7c9cca2963945dc21c52232e6f83b1875b2bed7c80c37371a480a2e5255d49d390c3b2adfc695036ed91371cda7d79bdfbae464581f0b32942f03826aca17ab9da6ade4a778d310ec3da17fc3af426d21b347aee7c2db7b5e188e35714dc514e3a1c100e8595c9e0e4399ad796021976f077e5733ea535cc6daec2e371853dcb715fc366ea7d6b9a5b3509dccf5c2e1225e3a51de9f5bb9b6586b282a0b27a9ae7ae8f2be14ec677670241e384b462eecfde68957839b1327c9e5c622c0f67cdaf3845ddbe6f754401d720d6b6d5c061dc906bfa70fb76e1168c6ac1a25cabe8873c3c1e540ae44ae631a2638accd7951f368442dba7b38d0662ccb0140d1e4ca23f51de731a6f5adcf816c3235359afd607e58948da29a5f06c96b4312aee7d35ed4c2c811a58c5a196ac2f377d653d51cfccb5213c928955ac880b5fb1b91e88a52d5c217cbf78e071275fe626c230fded548b0f1667af1309149ff74c5542119d4e269fdc1b241d9f53e02e38e015b7c5c2d2ee623bcb4167e37edafddc7fadd642c20f81b454db1a3b578d527f124dbd1f3d99fdd1590256ae4e47c2e8b3bfe8708a0d5506d6ce8b130ce6b70028161454a5065e9925d75c0095dc24ba789489fa1e9236e25330ca1a45e61224ee027664f6589028a240961aff09187fb719ff3477b56427189b7b3c790b4031f6539c5e3a8a5d7fd99c2534ed1646920a43e7315bba98d59c51b337ba7a1b038006bb574df46830f96a5685e07ee8a0a41e712810faacfb231c67e69d0fc24b98d782c70e15524d5d2dcf4e64b4e26772dfa7067f6dc7ffc8b06e6ef3ecb13f927d466e0cd3a7ea09aaed90b7810bbfdcd8a1274bbe78a453ffff11ccee62059ec25955b34a1a1cdf8a3e506d99dcbadf16032646117556ad71cd93ceeec42be0350a6c9f194fea783558c42a56d034dcaf6fb1b28037362c7c6e2446bcda71d0a88adda3144589447ef13cb85a4d2cd16ef444097bc03e32c3b1a055e952f7ac87078b04deb900375a16dbd382ea4375ddc0a9645deca620590337e803ea8b41337f3f4e4030119a2337424dbea3d21214063ce853843dd4e6df94e3dc3bbb36d89d9eb8e15e52d0699bc6ad1de9d12afc95c8785d63756576d357126e13b25502d542774f6e5d2fdb559d52698d08600fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000010000000002faf080ab009f4d0b317a141e5b3f865599676f895834400b66dd09d33ad27163ca176f00061b1000028fe56ab181f454372c939ca0516f5782b26cec2010885c70e55c41a1e43de3af6635d2ed0bed90837cefa9f1805f4808e8092a4d44efe5fb616ff7678487a460b367134fcc728ba273fb1d22718bc6a95e47a120a6d952c7c2cb8f38e59c2a4efa63977cced7e4b8f46e4d47d29098a73beee807c3337c4acddcbb32b78eddbd124b2f33ec6cd8bdd364aa4ad2c10eb69dab808fe5f5f0aec19750e51ac65a8746f345c05d4b8823ffbeadd6200ad01449c39a008fbe117a8ee904445488811336d0c439419fa4f285f9f62a34f10b076c99c0092968e3cc9fa656016b6da049bd56b910d7a9356e76d24e746b280f0275ec9e9bace82d852bf0a137ea02d4cbd3b68450bfb593564d8c20953bb758890a55a8c381a4b3303b61ec26a56111361bf7262b3f6f2503aad06758130d86ea607cdbba53415aaf253430d92fdd81c685ab39233e94654e6508eec1347747e2df2862169382aef6f99dd78b50629c5d98b1fcc73e865679d862b42f8e9d54ef6288ed2c3f2713f0fa4db538cd3e70ec1a30cd65dbf873f581b30892acedacd39b5f0aa774d1f3f77d8fd11ed628bcd02ac33f89123595aa455ec54a07e93e26f94338fedd8bb84094a0add52f912ed5f9019e3a28d90d251cc6ed7ffd35254dcadd9f1e9b28eb0e06fd4fe961d60cb690a7757f475c08aef07c2e54668121540a42a9c779623709a2124629e8c4bd4021763979647f625b360a4559dfd3f57798dfe5d36e9d902904af3ed67d8f4b0894538c7718f5160d211cec27375a7e6a2ec42f2c8fcd1c953b7b8379d42439a2c6b921a66d5102ceb6bd6bc20b17098e69a0a4f708b42520e4792474c3d115a12c83ef60ac6e69d8842c5981e9a6d178efa352e73e4a34bed4fb590dbeecb259617668e6ffb9f955297f26e3a6a3b95d9617529a61f08666ca1069d2ee1876337d3e786244c5bb45a8236577184584cf3018118d7e4e78973ee510b6773bd922797e580cd240dea3ca31892d23c1e6e4fa92f1a01da8ea40044f5613a9429ebe7906f79b32636204d025115810b376d4c6436da136b96c7c10649e3290caecd6ca14d995a817e3725fee7e621c5366f80c752e50aeffee1af3361924f31cbb1cb44731d19963ff30127ca2363ce15e50948be14c43400737ee8910ed06027599da74b06e77eb82ac523cc031c57c02dd82dbc0d53629d072615c92034cf829e7a5d4437b1f58e2bd4b16993e1e1b05c26ed8d695351db11d21df36a7f5811ef5fe001ab1e1c6ce9d2b69b6ac3af8087e6666317f75b645e3b1caefac0eb65327fcb9fa62be341c99f191cc869e48dbc8fee3e42d4393cbc6505c880dd6739a69be4f7ef3de306480a7a51f413d310926f252ea96a0c772d8b8e94e7d6cedbfbdb21fae2ffc379eb17c2680fa2bc56a8726c93e7bf2d446221ce95e49da93d29bec8e53ddcfd262c33d556c2b8921c3de93236408b462d28612d3343fbb9cc538b1e6b33c341c3b91dd41f936931e61f146fd00aee1c5c0de97b47cf7efce889012e1c22dd8faf0fe155f4e9930c27941d8b0907502a835bfffff801f6835de69ad33e95232f773219eec0e2374c421230f323257dbc91629c4ca8a61584f737b827fe8e8f5b69b88a7b64b362f8142b043f08ea82c4a0ab7c4e0b9805533e806f90597095242ff64f314801fb7ad838e98e1859b2c05c9b027ae5a4baf780d15977bc1492dee9b14b1cb0fb3243eb2304919486fcde89a3cf35a64e31b1698e35fbb8528a73526a19189d406272b8becec94379f69372afa99d06bb4f34df72e1c3b49557855ae8ac265160bdf48ab34cde30d2665891cfeda24adcc657d851431f38953f917f1a111f023d2c71845ccf25a562d2450bf8b4986b64ae4fd09e5a8ab9610d11fd68e9d570b3a467780236de974c7fffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000200000000000f4240657c0bd97e795218aa623b27cf9a71764379c4762fcee8993aa0b1ab1e32194a00061b100002c0ec5d388a8c78491bbd870faa2c46e4282e11796123b69c2792e5806748ef068397f0212a33e3f79162ba4ea247ccec410db039dd323af48ac27bd0ba77ecb870c587477d4543d9a29c53fc02bc98d7cd0144c7abf80b999c22b42a28ae8d625478ca304001f9a49782ec970031e673c76e4e27357a321729d6df38d1d88dcbf764c69eda3baac9739bed637010f44638cfe1deecc56b76f6e02d1d0c3f104462b9ffa5f20de4cf092d86a5bb35d5f62b0fb1c983a2c06df17c9ffc809c83e4b4335f5903fab536fbd9719847bb063541ecbe05c12ef8d058b3547faca054e3d662250f1cc1f925dd71297abc25fe37ab33a086759fac76208a64552f84d2e4d84daccdc3aa2bbd2c2f922bf262596742dfe034529d1ead2975dd3d197ab0e2e1c75c8b8f160ca6077638022d4afbd107979949cf342cb399347f3990029f0db6d9ac0c569d61d42539371f9a7ff59e9c83ff97d15bf0eeb254ae58fb7b1f9d8710c546ac8a227930c66ac841bc4f475229e5cadd14ba5a01e6b2da99c55861a08e2100e62c4499d30003fe30ddaa347d7a27c2158d3787d58fe51ae57d797bbef7f900508d1580df3e5233f0887567fba1faa918c246d2ec5c3b7aa022cb8a652d00b4d719e312482f57655eee80a90cdc73151fd7ab9c5367793d60c6088fab98f0547d7f547e10db202a25e027a5cd0abc41bb0e3ef563c0a6d469a702b2a26f0e8b4fddb845a16a5f06b9dee33c3adb31430c94942c5023179d3e4441948a332069a1c3b69dca65f05a43452e42fd28a2f7e6344f98ab9a7e4eece3c1709be1f7bb620f8b6c45989a8bccad39a4bf40e8215183d1449196f1f9fc17de778b616856152e6e6145a1b7a3d7f226becaea5ebe34aa4bd06e60f0fed207bfd21f5663bfadd37dd722437bcd46a26fb4e19d062574a81bcd817eecbc1914a5878809128961acdd73113ae9c51070ff4494e16d81ccec777eeb513da82bf43d4884812b26546b4370dc315793271b069f60f4285f648cf122ed8b22b0c7a27e94ccd59a273eb774c109e19980e146850de95f82cdd8aa0e82022672024c917b281422d284df0ee0bdeb3d4ac56b4ca675ebdb835c17b6a822d79ae7310f4aa41d80ac61c5e45c1c0e1d64542622a31091a9f87c335e86d964dd85a951d7c9bf41c9f2b1a9bb8424d7d1b26413da8034182fa42d2b1cd1f8745482c49d8348d19c72cf5a02bd28e4cba82128af8bf5d9c1215c4f543ef4d185f100f8d803dfa29c300c072e44ad9542b82fb1380d55c15c9a4b4398876e2450b90b49990746f339abca8cc8a462b62329a128758ce0e46b5f998af1bb485a3044bd125424eb5c623afd2a11befe4ec544eafe275ed1ad82b940dad5e9a9710d48562e51b296ba81f2d70593685ba0e3f3b25089a187e61d5675dd481aa99620276cb0a841a3c4df201a929287b1127270c5d25d06fb286dae1a9a5a5cdb60003f0c30d2021074bf252e550685f7b51a087a77b0871e883104e55f898aa5bc4cf8538c293253a737556d7f220e15b90cf0eda7d5f2172372e3c50c12cc588f312da37191b5038e944825044b130bff281ecd47a4252a1411ab7a9305c2b37e9facd435e9c434de37641498f8e4bfa7b42966da29c84200aca87ea1c3b00db54906b340e524a7dc4a15403bb82bc24517cb91026096bdf18f5f7ae5640ed6de1f0c5d184813d6b9d244b32b58e9ff524741a39383eec3a530d60db13deb26e3523a725f0599b671b625c07002704fb600b77318417d2527537359d122e22a1f7581eaebdc19e65ba50bdda18ae08e9a8694fcb0ff1a2cd98d910dbcd52064c15a4282d67b278c72a0fdbf228abf6b519dd28ac21c57d1da4bb7ad5b5ab10da6b83132df1da79ccfc77fb45598bbd91ef5ab96d8a2ee148639a562debafffd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee00000000000000000000000002faf08011b29e60a38883a8d4434f17ca3d92161109f7ddd8799e64e86d6b8509babd1100061b100003b8c1771721551fa88af8fdde92909add6e5b8fa90a4b0484ee065ed3ed7c56e733a7fcd775d4ab92956c1328ffee9c195004c5dee5dd5b9d0f9034589e6769e72579d3d5837ad70785ec420b4a24c04d36668728c0d2534ff1feea9aac2410423fd79c7db9231ee7efd3d585646e378fe53d731d18f38d6356a970f5c3026edc849d49ff34e58dfb7548512461110088ac3aa800e10785029ba3b0e9ab7bde0f056939e4921792dba2f5c005135daf57e32cae06a9ccb1b4d321f3ba015e5b92def1ff1c200e56b3990d82570586bfae26e9398e17dc6c069f92d80e6dfedf6b2f24b1dc3cc9d63e684d861f40fdbf508d4ac34b7f10c57be2a9b0c5921f86869c29ada5394b8780d2488a4fca3cd98ddb0ff8ea4415a07caea436682835744e94d5cff6d3024a9525dbd697e499b7ef23062b18b225bfaa4c5bb07166f34ff7866ec8f0fbbc12f695c609692798364fa20bf7977e321deda3fe5510833494532fba94fc1f0dd14ec74f3e9fe8ee659634621b63d16d46a8958132c24bd82c516bdf9ae9515cebae42778e4de6be7047c31cf86c0df0306f7b6562e1f35be51e5e64cc6d9d4c010849e6ac7ddacaa4b7b6fb1d35aac815964090940e73a1193eece11c1c1d37e373ef58c5e2d690b6ed6338360af9906146da9db8329bd2786bbf92df10445ee093f0b1b2a640cc2daf003fa7141435ba1dd54f9cdbf5417fa7f539b255452852a85d2ce97ce5abed4980e7b409e283f97ccc9c01e104b55155f96ace6789f61c4661962d34fc5d7e6f5f5233180933b2fa7f7a5b074714645489f5221966160946b7bfbf0fe6733e6beb8af4457b9d36cde1200811009ec483a9d730ca980aa28f636942af5e89794a8edbc1b75d555ba134974374d0fe23d31c26566064eb9998d649bb2bf066bf710da50672f4e3ab4df843a0c8942bad0a071c237d4c1759eca37380919e36aec73284db202a32d3d1619f3e5b757b2df8b04bde567783dc8e465d996799782f1a1b8de9331681a35aa04edb427de87264c8ae9c397f29d3e8730db91256425a10b960a9de1a48d0d4186d617d2b69c87e2540f6570faff4ee1f6303d7d281434947abeaad83c86a4d25bef4de2bb3c6104aa0ceed7c8df039f4be6a42851a118adb1b8f98e02f6727b75d98541bab2ff24fb2f20342e86150c678941825409b62a844f44ca1ccdf0d9f7c2cf9b222fbed00bc92be0802fbfbbeefa71c8976cba8fc4aeb031480f434027b1cd593d08cbc14c2a360b736b06b5afb8da35f0be3818fff4275b8c830f5248a8b8edea1327454e1360bd90d4fa08e965f459b0b027e1180290cf762f813a31e8109f472d9657b03af737d1f7bd2e59441541a84ba818f1413c5cd1f8b9882e9188e0def9e44e2f4a7c710c893c7188ba86423f8ae86068d84e1832af548289e87c34d68b186df7e24ca5b051f8f5e4a44e2e7383ba2a09615b4147b34e86486731290ea67f3be24c13a9c5cc37f06555989b3f10c580a9cd2b416d0ee4210855c6833a25996761dfabb036f3893cff7db7e310baa8faa79f46e0ee43bf4dfd732eae7f44bad2e7c032b9c6d14947af6b0e37e5ec98372a622f716ffba0cde04b9d4508392dd154ddc34829412bfa604d4f00e4b10a553587343ef5c0944165e7ee1e34387b09c147ecba943cf36dbc4269efe50ec3a5a3075c43be9651d6db6acb9f657476952b78c990557f05935247a71077373ec436ec586def177448f8859ba096b7a838e5b4ce7a463f9082f705c26d99936eb1be584ea9b58a44b9b4faa07fd8247fa66cf4529d1b8cdb92ed7bd96bf0968db4376489c7d46f0f27d58ac884c29736502953723ef1ab41e19c7041d3e0e9091d7de2e3904d032de02292edb1225a672ab438d3c65f7921c06a9f181f8ffda4ac524d0e000fd05aa5d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000000000000000000000000493e0f81fd474bea478df0202b311c69e85dc6215f629491dd15ad0929faae2535abb00061b10000370b2714d2734e6b8cde085794dd7b41c8a9b6c03c1edd8e3db168ee7fce39493596e882b18b5b1b79c16400c6762b9856075821be6be9fab8f469f56820d8f341554400a8da7f1f1a8501902581d43b9fa6e5c68015716f718a2190b87fce41bf1b509aa61806394a42489d63c457fe4c79e7480eced1315edd731887e57704fc9102f50cb7f0d242d755cfd5a2172dbaf7f01b124861cc6d1dc804796bbb84165c805f0c0f3fb9ec97b74c2a694de56a9cf8d79d1a679260ee1169d78214b34c8a654ba22e59ddcd32beff4713de33549f035b342660405b0159a7a508e5691ef4805689140e72b8a0ef2e61be74dea5d0b8f5589e0e373cac2e2e1cc39b2121c05cf4122ad0f8b9af6fbf1de2ea26376c2650ccd306c13a7b64acbf2a3feed128754abe44658009e642768ae3d84f5e0fa5f7f360c2a1c76d26985817ae77b71fb59014a5483ebba9271cafa5e5d8031c569adeceb8bae6444e98d2522b28f6682109fc7d31cdb83ebd45e5d81e7f046df42345b49f470dbef9ed87709301d2c6131215d33a30b8d18e63e54a2aff85dd57672f8198bca6a67ee147c7d0ae649e5661ab6bf78a662fef9a164f1e332b9f16e6fb3d5769ddcbc1d1c07338d3394b9245d17618c2474e86c064fca4df00ad3a93dc051fd8c3328cde2a987798b0f22a21c90426700abeb1e6f38dffb485b5477ec44c690fa80e317b32a982fd3082253bba8595783290dbffee4fc9296ffdf16a8bf3154971bb720e78674969e9db2e0fbab9e9e13f24bc8b3af5e2f00f262f0da56de443f70398ab68f747d35370fcd8e1c0e130f7269e08f862b5a67f2c129be254df2358762ce3a947eb27d66450af51540e7721b47c8a5a86098ea64dad381f14e07aabbbc470949a99c07612add3ab4c575fe2e520bbe511a1a674aea37a44535c13ee3380f8f39bd230fc1481cd31912af36c6751e23c6f383cd37a8b13fa7df9f0c7e460739f2c6226638ee14f14d36366211cbc6a1e16b4856bf302a540aa9d9e833b1d59c510473096384c8b450f2f3f1dab9e614af822949d5cc93d76bc4d1a52891bc85f1981ef83161195ab7d8181ee4fb163bc6c685a10e87c7f4b15ed7d05833c230a4a5b63841fc65b959f0ff010e697f47c583f9b7fa9b389c0eff6614e47d85b83c483136f182be4c151d272f5d938b912a95e47d333e5de6a409ad271679a778a7eb3f169c71525302fac5d4575e2645c09763c2ef165736a7a726ca605038e2781404328790ffaacef2b9c2bf90122042cd571287bc4e3973da65fbd4e3da9e40e4347ca6eb4ef1ffef4e5a34be80425cae3e81533f7f2953f95fca53a22057a39125f5c76350fba7fc6c036838fb951d0aa8702e7f44c6f8a9cbce3b64fa8ddc2bb8c8b35d1e29a21beda6fdd332b31a749321455277231fd9d70ea4aded95053b395f88fa6916d126e1626fc0f1be6cd2a9538d17c498b40927f12b3bb40fa3e272e82cd2242b670afefa387470f4e6e0a1236028954c9e90311f486617187956a23b90b356d71e219e6dd055c2120771003a6c12769aa3ceacb9642bc01022731ca7a413b68ee7d1d5444f75dfa51a68b74a01ac85f6ceaf5e56987b9d67d6de896f5aafd25c78c413a6d4b5b03d571167524cd231ba13bd9f80fd7413faf21e8170cef0d08b242c5c38a2b0158da56e358ba0692f670d4611c7a3624b234adc30c5b7198e0afc941f5d13eae3a94ddffa652c784c34c582e04e948da91a5ac3038a9df38fd4f1733779f4f122ca2d7ff9d03bac9def35d9ee3a183161f8f2808d472b2e64581209359cea58ca7757164c666029982223877e2b14d2d537afb012f1ffc12cd083c16dfc64213c56f3d4d22b603d3dfab1d21e239d6fc1f9f153ed61f1ac91c29c85c16f4aa2985f84052f5a08d32bdd000027100000000008af34a0000000002c2322200183cab01341b3b937dc48c2d5d70e119b4fd5b4dc6d85e0bb49e98f1fe4ed87027bb80c7d8cec36237511da378ad5c121861660506bdf411240139e49a93e13aee25d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee270248e28af5950cd59a64545f84957e4cd869de45b4fb50b85313894a1567c134666207e5bec5803b71d6fd6a9cfea3e6ea9a8dfe645a7f4dc7c9cd6c4ce15a0002547bca65416a28af342589b5b771ce54464b17b2153b49a1d34a2ed0b0788ec37083e54750e6f368b4a40d18a8730522e8f23901ac856b697dbd91992d75e097e3f175514453a79521d93ca8b3ffed47cbe300afdd75d1dd23cbe0c14cf67b3856fbe2128b9b7543625f1da313516f7e441255f85e341371fb7f33c6833abf02000000000000000100245d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee000000002b40420f0000000000220020c57347ca54a9e6f279f6a1f1e50f19b48289c3baccdd356fb03033ae7ccb6e444752210252a01d9d8b03db1a99b1a82223f7dd60ecffa69dbef47c06228fdea2fe0f0215210322a747c1d7f77fc7577a689618bbeadf28b941412404ac5e216d684a32d57a8e52ae000100400000ffffffffffff0020cd6d4a4bf51a8c36a25cde5bc08188c5ae037fd9ad9e92b400bb3d10e473b55480007fffffffffff805d10939a524d7b2ab225cdb41decfc129208c372d51ca1db705bb1b9831e53ee061a8000002a00000000883a86353a00c855b5caa13998033c04330f88b88e084b3c00f228299e5554f0b66e9d5c630e194cd572acaee6e5124b612583b9722ccf24581716292785c4925c06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000060535ae20101009000000000000003e8000854d00000000a000000003b9aca000000" + val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + assert(decoded1.asInstanceOf[DATA_NORMAL].closingFeerates === None) + val newBin = stateDataCodec.encode(decoded1).require.bytes + // make sure that encoding used the new codec + assert(newBin.startsWith(hex"0007")) + val decoded2 = stateDataCodec.decode(newBin.bits).require.value + assert(decoded1 === decoded2) + } + + test("backward compatibility DATA_SHUTDOWN_COMPAT_03_Codec") { + val oldBin = hex"00030000000103af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d000090cc78408b6ea14d14bab8c89a4e1e0cbdc4cb4645f9c3a6457b0bed5788c389a80000001000000000000044c0000000008f0d1800000000000002710000000000000000000900064ff1600146663df19ca3fcc1c04447b18d2cd795c485f19410000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028a82039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e0386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c02c02cf88a39307d6e9aa4ba00bda029ab3cd426316eb63c186875c3a5d80969500381d707ac8494a96e1e8b8bf4a3a703a78358c69856f4741dda173d9f9448219303d6ba71bae191ee8d282e57d3a7793919d621b9e2df77dcafe45a5cdb913784f80203212eea182f9f5d5666a54b3e96a7525a48d5b9ee0f9a160d13926480cb9fbe00000003028a82000000000000000001000200fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a19700fd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd000027100000000011e1a300000000000bebc200249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52aefd01af020000000001019eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000d2a9188004400d030000000000160014a203180611650c5e77e05d64ff0d794aa4ad35ae400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa764286a040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfce093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e1604004730440220203be2c76c9d051b47db6da5a99ce689eff53ce5bf016f1d9ac403ab7c9fc9a40220290da75c0701cc212ac502d3d894801e92a701a20a4e85c3f640f579f96762e001473044022006c47ea4c15c389369ff75d25b83770396920a1f06a2c468ce56a32f9cb7cded0220759b6227e0da03ab542187f482b9f6123c93e3187070de646b07547e7493e8ce01475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae08758b2000020224cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000002b400d030000000000220020fea2002eacc21b3490a177df4ee69f30c231d919b99e0b32bf518767169aa7648576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9148acca5ea2fc12ce9d71ece802b1f500a7e84378d88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1010000000000000000015af3020000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b06000000000000000000402470f4a38f40a7d71f39f9552254c007ba5557adbec6ffa7176669fb309195d75a33b4556e6f35536757085c24fb83c09ecc1cdd976cb8d2b0bc679d461789d040db8c19bf05d1d55cbe50de7ce29c9388bb541f6397dc43f9452a92a931c76c42722ce6ad02d0343dde9b692e001349d9b1e075527f1a8d3d1aa90fc72164271a0224cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e1030000002be093040000000000220020e518e9c7e70e16eb89facff2af3f233e1af45d8a6d04ae7f36f4fffeadcf3e168576a91490767756314724028f101050cd76819b6a1756368763ac672102551ddb3bbc7ea3a02cadba123a00b171d7b75ee46e7aaba9caf112012c77ab587c820120876475527c2103b89e41f7c1cefc7864881d9a78f262a9077a99cb05194684e48591bbffc0a5ce52ae67a9144976a85ce7e37d8557d74cdd86254926d991f77b88ac68685e0200000001cd3bf6d98e9f7f88be0e61167f9797164b578df264206f79a96e104cf4abd0e103000000000000000001fa79040000000000220020f757d7d804dfbc35427f8bffb749454a114be5055a683d08ea66581d1e51dcfc101b060000000000000000004062a64d249f7301aa354b7951f0c7b13882b7196e7985727395c624bdb84ab9234a5e0edbff3b5a5b12bd385a695722be5aef598ab36ec1eb605e944964a1adbd4061013fe37fb5d1ed7f8d8137501a755c195bd2bdcf345e30cac31c2962f4e0aa0614d7f96b9c49d8120089f2d24919ddf40bda3af04312f7d5255248075d28e200000000000000010002fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500000000000000000000000011e1a3000a00cedece5db1cf150c39b679dd048140f491a72e93196f9e205841a23791c500061b100003f2b3c9277a3dd9ee0fd08afce183d75f570299155a8dc5c1c5acc79a572629c37ee817bee5023531eb3929c8e95025deb4600cb6ccb900176a23ed7cc35f99f7266ec8b0e59f8e0c0150916c2533133bfd105e883ed7b33915f7a532e36d46db78a2842d7e99f1f8abc863a5f6f8866b130025610795266d8a9603a8ae11ee23ca765f9a82ef00686612da9f70c2b2c2ba242bcaa9e3d95e542cbe59461a75e5a8f01553da2552d363593b8a85bc546f17f8e8bdd1449602fe3f46ed566e42a4155cf0b6c26df2cc5a8312ebebaa6bf11df9a55aaad2a96cdf41d7e70e815b7457c9332cc58ae9de3b9f52da71e7f328afa8de7777c76ea82e17e4a2cb46c3669ccc5f644414764019bcb2d4069c3e7a29814f0462abe479804d4dcceb498f1da52d5796f69f7d1d235e2da967e8683f77e7f206034ab12c02aba8554ece3444f8e0803df9ba5a9b1b918f13d94fd89235e1e2ec32df3154b204bb7642d33112ce70ef5384b11cf07bcace83a97339e1f9fb33e4b2cf1ccd62cb5311ab2124a8ed33a6c95c49ec90d9a477f3556d098a39235e8f1b904ed7aec0052deb3173f5d85126bf90271c567f3e9a10c12bc50f37f41ae40b49c71cf74cf18cb6e5eac206cc596349503f913178dcb3ee4ec5410b30474a4a2831b18ac07cb78fc143e056aee802aaa8bfd5d97e403e718069b221e3effcd57accfd13bb61d4eb78e98f269e79062c414a9e0b39075d8baf371c0fe09aaed436445d62664de1a96e8c974a3fe0cc045dab3064c7601dda9213d89b8fe30437f0b5cffa296bce9c141e3c012502431ff0cd5cde95eb52f9dc78ce3fca95b6a75de178ed13f9e3e7610cdbd1a1a893dd635e07459aae22a8f8c5bfc0a33a1f88fdcc1b99173080b602f91f1c9549b235e21bfc4ca071e0070f0057d65d98343cd82035970f6d75fc9a4b191270d83cb0dc6aa84238567e230bb68d16b0bba3563a28d151b308a74766eac50d4b4b287e1f46230ed1c4e8259cefdbba86c367392097909a740f692b745bb8c4809115ddd42de543dac65245faa6870f25aa2df16118d6bee2be181789ab64e5ba727048eeda81e6a90995c681924b2f807554401b6aef9436a345253be21747c313e8d9e2bf048d890350c055016cc61a06dc082a2b2c1b038405248ce18bd5037afa0e04b704198d6129da78ef9b97a8cee66a1104acd89c2bb2ab57d519f6693fd94fbabbca63fb9bd29decd9dacb34000f0f40af6969577f37a9aa38b4f2cb3d5d974a3d53440e78aa7ba013ac9a242be930e819ec36a96850cd7c630ea102fab614f81a4c9b9a01e60aaf9956a4f109cf5496ac8a43d03b0802303a6fe515199f9dfd4e66c2eac124d4653e999702832073f75ebc177fffc3efd404141927b3635980a40ee6ae50ff14010ad8e21493a38e4930a82b9d636d7115a55511bf4296d80ea8105a07f65730db9315a084600be9a3dd2b19943fa7cc85d2293de47f7799459288caa8d40c55605b5abfdfbf1325d0ea90aba749b0a625847f4b006ff34b6c9bd14a1c107c8a0175983ad10ae47a285b7d503e870625711fe3e1557377433ee845e2254c538304a97850e5acd2d4ba14241630b026230ed508b84f9442d6fb9a45369e2cbbd0d494cfa2573d4a0e553c0013ecb33d44f2395e4e4a3f752b8f86955061eecf4a9f9fed27648446369a8dcae2e6ae0b51b9a4fe315ad5c7521764738cb591734b32a8e35c1d6c80d9202aacdbf65cc243ba0640f8f8c23d637d7331c5b9f4c44fa5f2858cf2d74937beb4b345d22c2614bb6346f38933f84aea2bd5456505709fd8b5335fbe54e99c351e151f1f209dcac9e37742d3978c9a95eed0b29fc0b4150229a0990f465bd3bbcf97f73237961042359aaa0f11b1bc4c572de165a6372f3f438e7071a197fffd05aa9eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa438850000000000000001000000000bebc200a043ffa6f358e330873add2e0d4f05a72150cc831bb2f1062e5c378b9bced8e800061b1000025eacbc7939a2307c66fa4841480bc6c87f041d1bf722da78bf13983d485df3fece09e54b1fb1534dee2931eb75f0bb95f21e924e565e8e5b06dd16a9fd4868fe60295bcfee2f6c53481a13c9e49d468a8c6992aca5464619ee54f052f5cb2a6a4ed6abeb6951d22ccd0662816882497d6c8fcfe64152e6a9b6ab15bdcb43545fc3f2fe9caf2679f5705fb68b90f8dd1579b9346901dc16923d222125652c8b15b5e003f660fc1af452ee0edb61b67272a3369ffeb3671f2bfacaf3b9452db611760d95c734151639733adcf7dcd2ab7a298d7dd0d31c61004be368b9858d19907eb76b421e3524d83d4a2dff76c73abc0de61114f6c7d61622074e719b729c52e9b11f65e8260e90f66e71de5686c89da73e9b1001541d621663c3613eb4ce2faf7f9ae94f0f17047e3b283baf98145936ff491c6c031a2059e2c52a7725f0790943bdd21ecd3e237000fa8bf27e81343902489101dd96f2a9bd6cc1cb9c4f4a9bf33ec39b0743901f8bec541fe9090d984d9c071eca64ee9fb4996564cad02923c0a662db9b42d76bacb09314c6bae42077ddab05905a02f3cbab41fb6a9e8c1ed2b27b5052ced38f4192cd97e0b9747f219f2484b46add32e937fd7f8fd8098aa2147e3e1400226703860766549868d719a8c372fad4ea7c8256908ba2865c68683a2cc28436b1effcd6e5ad990b83824181fc80a576f8906a45f56ce7e3748ad4a4385143768876d1e73ad962eeb6a2466f42573da8cf4a082997ffb87efc7f2711b92af0b36a72f1157c0df404aef2f958b32100991574ef4593f134d680e1a87729a577d7432b3e2ccfde6fde5334cedb8b601a07932f2f63835705e480e57f92b51e4a08ec44e07157bf6f80095fe908637d569708779eaf5b9be3c33e0dd9b840c3e1ef3cba0a0bbb868b4f07b5abb934fcf5d47d3511941a32e54ab5d4c547b81708dd5a876d3667f8447e6819625f49ac1e8f8bfd9df2b190aa318927d212c964a5b8b36c2a94d7543c8d94c4e2a1a43824cf652873ddbbdd321b473640f0982cbcea32288c6ecd2272e57cdcc20a9bd60a760de289bd86cdcf590cc7118e407ecd369412e544cc1d256e177fadc3fb15d8238199782aeadea3f33558dd68ff042d9cb440f556132dae1fbcef3e693691b089939883ca0e8c94a630d45c6adc33201cb274ee3f3225cd2a835b4b83e0a319eba92cc2bf728f6e5bcb786ccf8ca7c42e6fd2a9d7941580a8f7983a6d9d88ffa6ada598ef4571d6d5ff67917f43bff4df2330081244bd3fbaefb21b6a8d14ab81846824379415c186726d20afcdd298b921bb850eab4bdd188d72dd42d6f1a211b580e0ca831c2443f7f50eb911ac3b3100513be666b3a48be024471f92e1689909392670c0f3d7f937ec318ff9a1f1fc147597e49e735978abe5ebc1c1fb6474acbb610af539c57a248fbaa8909587418a15e536a53801736ea27b120e43a070a8e2f402ce744d71c2f81caa3a23a669462c1e0012009c0201b9cfc5b01a78b26c2acd939c1ebaa5e127b57c58078a79dec8bc206111e4cba54b645d0dda5e7cdeaf1f9e6374037f8ecc7d72d2802b0a5981f48c75b1f15ade54472450e0f16736902702e915117ba02344ed0d58bcc9f2e03fdcf6907812a3655dfc07602144a16e5e1cbcf3629a8bb274f8c1652db6cbe12bab258d574181c94a8972392428a0bafd1c021bfe42df0a942f87c112c8bc340bd518b3cce475e481b7006e266065ca620d70923dadcc7518df79198990ac8e7d99f066eba8bcfb2992515f5cbf2da8ef55b19fac4418309cf7bd237cb69b1d7c6a3c6b456f1ee228b3e9fbaf1b3cadddfaa307039dff33ad59bcfe44851c33fd09346e5791adf0b0a846326266756fa5a035a840e79ff0fc1735b04a4f611da1c29f9ca64a0a9d1d07bd00002710000000000bebc2000000000011e1a300f36f3bd94a6751ab579d9481cbe484dd6b1d2abba32bcc757aea07e70d0ea28c0226f5d9131a28e7d24ecc1b7fa2b63b34a379879f645d8e3346aea28f9ab603eb00000000000000000000000000000000000000020000000000000000000200000000000000000003ea1865f4cd884edcb9921736b2ae36b50000000000000001000384fe405b653143169925b03dd2cf38bdff034a0fd7fb350f7c089c1129dfb82cc772d5525d0b922cc3d989fcaec7c16b03c3249eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885000000002b40420f00000000002200207f89b60332c0a813268782be20d14c67626b76aa4832e1b9726c57a01750bc83475221032edd70e88cbfcda8c91eb9b20ff6a2c569a52de908933b048bf6b8414d7667b7210386998d8c3ecc235c80eb4605ec24c5192d27ddef57d204227d3ac0c54547312c52ae000100400000ffffffffffff0020d22458cead349cb596ccc159d17f2809dcc22620268d1a300a6e152547f7235280007fffffffffff809eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa43885001600146663df19ca3fcc1c04447b18d2cd795c485f1941389eb27b82a5de32ca44c8d8e38e705c62f5f4d3b945dfb5f03973dae05fa4388500160014d5f0a47e22dd767bca0be8f6d109a81a8815ff33" + val decoded1 = stateDataCodec.decode(oldBin.bits).require.value + assert(decoded1.asInstanceOf[DATA_SHUTDOWN].closingFeerates === None) + val newBin = stateDataCodec.encode(decoded1).require.bytes + // make sure that encoding used the new codec + assert(newBin.startsWith(hex"0008")) + val decoded2 = stateDataCodec.decode(newBin.bits).require.value + assert(decoded1 === decoded2) + } + }