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 7de85814fc..8b1a98399f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -92,7 +92,7 @@ trait Eclair { def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], 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]]]] @@ -190,8 +190,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { 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 73f11da7dc..8fc29ded72 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 @@ -658,7 +658,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, fees.feeBase, fees.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.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = 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") @@ -852,7 +852,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() } @@ -874,7 +874,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId handleCommandError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), c) } else { val shutdown = Shutdown(d.channelId, localShutdownScript) - handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown + handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown), closingFeerates = c.feerates)) storing() sending shutdown } } @@ -933,7 +933,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, remoteShutdownScript, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdownScript, 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 @@ -941,7 +941,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 } } } @@ -1164,7 +1164,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 @@ -1173,7 +1173,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 @@ -1189,7 +1189,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 { @@ -1209,7 +1209,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 @@ -1236,7 +1236,19 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(WatchFundingSpentTriggered(tx), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d) - case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) => handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + case Event(c: CMD_CLOSE, d: DATA_SHUTDOWN) => + c.feerates match { + case Some(feerates) if c.feerates != d.closingFeerates => + if (c.scriptPubKey.nonEmpty && !c.scriptPubKey.contains(d.localShutdown.scriptPubKey)) { + log.warning("cannot update closing script when closing is already in progress") + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + } else { + log.info("updating our closing feerates: {}", feerates) + handleCommandSuccess(c, d.copy(closingFeerates = c.feerates)) storing() + } + case _ => + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + } case Event(e: Error, d: DATA_SHUTDOWN) => handleRemoteError(e, d) @@ -1251,39 +1263,78 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } stay() - case Event(c@ClosingSigned(_, remoteClosingFee, remoteSig, _), d: DATA_NEGOTIATING) => - log.info("received closingFeeSatoshis={}", remoteClosingFee) + case Event(c: ClosingSigned, d: DATA_NEGOTIATING) => + log.info("received closing fee={}", c.feeSatoshis) + val (remoteClosingFee, remoteSig) = (c.feeSatoshis, c.signature) 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: @unchecked) 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 + val localFeeRange = lastLocalClosingSigned_opt.flatMap(_.localClosingSigned.feeRange_opt).get + log.info("they chose a closing fee={} within our fee range (min={} max={})", remoteClosingFee, localFeeRange.min, localFeeRange.max) + 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: @unchecked) 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 localClosingFees = Closing.firstClosingFee(d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) + if (maxFee < localClosingFees.min) { + log.warning("their highest closing fee is below our minimum fee: {} < {}", maxFee, localClosingFees.min) + stay() sending Warning(d.channelId, s"closing fee range must not be below ${localClosingFees.min}") + } else { + val closingFee = localClosingFees 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 fee={}", 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 fee={} in their fee range (min={} max={})", closingSigned.feeSatoshis, minFee, maxFee) + val closingTxProposed1 = (d.closingTxProposed: @unchecked) 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: @unchecked) 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 fee={}", remoteClosingFee) + handleMutualClose(signedClosingTx, Left(d.copy(closingTxProposed = closingTxProposed1, bestUnpublishedClosingTx_opt = Some(signedClosingTx)))) sending closingSigned + } else { + log.info("proposing closing fee={}", 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)) } @@ -1298,7 +1349,24 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(WatchFundingSpentTriggered(tx), d: DATA_NEGOTIATING) => handleRemoteSpentOther(tx, d) - case Event(c: CMD_CLOSE, d: DATA_NEGOTIATING) => handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + case Event(c: CMD_CLOSE, d: DATA_NEGOTIATING) => + c.feerates match { + case Some(feerates) => + if (c.scriptPubKey.nonEmpty && !c.scriptPubKey.contains(d.localShutdown.scriptPubKey)) { + log.warning("cannot update closing script when closing is already in progress") + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + } else { + log.info("updating our closing feerates: {}", feerates) + val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, d.localShutdown.scriptPubKey, d.remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets, Some(feerates)) + val closingTxProposed1 = d.closingTxProposed match { + case previousNegotiations :+ currentNegotiation => previousNegotiations :+ (currentNegotiation :+ ClosingTxProposed(closingTx, closingSigned)) + case previousNegotiations => previousNegotiations :+ List(ClosingTxProposed(closingTx, closingSigned)) + } + handleCommandSuccess(c, d.copy(closingTxProposed = closingTxProposed1)) storing() sending closingSigned + } + case _ => + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + } case Event(e: Error, d: DATA_NEGOTIATING) => handleRemoteError(e, d) @@ -1690,7 +1758,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 { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index 317f7f268d..f62c3cd3be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -170,8 +170,14 @@ 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, cltvExpiryDelta_opt: Option[CltvExpiryDelta]) extends HasReplyToCommand final case class CMD_GETSTATE(replyTo: ActorRef) extends HasReplyToCommand @@ -422,9 +428,9 @@ final case class DATA_NORMAL(commitments: Commitments, channelAnnouncement: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, localShutdown: Option[Shutdown], - remoteShutdown: Option[Shutdown]) extends ChannelData with HasCommitments -final case class DATA_SHUTDOWN(commitments: Commitments, - localShutdown: Shutdown, remoteShutdown: Shutdown) extends ChannelData with HasCommitments + remoteShutdown: Option[Shutdown], + closingFeerates: Option[ClosingFeerates]) extends ChannelData with HasCommitments +final case class DATA_SHUTDOWN(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]) extends ChannelData 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 8b6792df1b..e4f172a4cb 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 @@ -459,63 +459,69 @@ 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 actualLocalScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey val actualRemoteScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, 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.channelFeatures.hasFeature(Features.AnchorOutputs)) { + val preferredFeerate = if (commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) { 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) + // NB: we choose a minimum fee that ensures the tx will easily propagate while allowing low fees since we can + // always use CPFP to speed up confirmation if necessary. + val closingFeerates = ClosingFeerates(preferredFeerate, preferredFeerate.min(feeEstimator.getFeeratePerKw(1008)), preferredFeerate * 2) + firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, closingFeerates) } 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._ val actualLocalScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey val actualRemoteScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey") require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "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, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) + log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, Commitments.specs2String(commitments)) + val dustLimit = localParams.dustLimit.max(remoteParams.dustLimit) + val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, 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 fee=${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.commitTxAndRemoteSig.commitTx.tx.txOut.map(_.amount).sum if (remoteClosingFee > lastCommitFeeSatoshi && !commitments.channelFeatures.hasFeature(Features.AnchorOutputs)) { - 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 fee=${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 023f08edb4..c03e14fa3b 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 @@ -381,7 +381,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) :: @@ -390,12 +391,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 e2cbad3dff..a68a2e8b63 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 03ca2c5c3c..59f05ec348 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 @@ -282,12 +282,14 @@ 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" | 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/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index dc213c2cc2..3c86a53a15 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, IncomingHtlc, import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.LightningMessageCodecs._ import fr.acinq.eclair.wire.protocol.UpdateMessage -import fr.acinq.eclair.{FeatureSupport, Features, MilliSatoshi} +import fr.acinq.eclair.{FeatureSupport, Features} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec} @@ -257,6 +257,11 @@ private[channel] object ChannelCodecs3 { ("remotePerCommitmentSecrets" | byteAligned(ShaChain.shaChainCodec)) })).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] @@ -296,6 +301,16 @@ private[channel] object ChannelCodecs3 { ("shortChannelId" | shortchannelid) :: ("lastSent" | lengthDelimited(fundingLockedCodec))).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) :: @@ -303,12 +318,20 @@ private[channel] object ChannelCodecs3 { ("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) :: @@ -334,13 +357,16 @@ private[channel] object ChannelCodecs3 { ("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/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala index ca7f7e458b..56e362350b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/ChannelTlv.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.wire.protocol +import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.channel.{ChannelType, ChannelTypes} import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream @@ -107,5 +108,13 @@ object ShutdownTlv { sealed trait ClosingSignedTlv extends Tlv object ClosingSignedTlv { - val closingSignedTlvCodec: Codec[TlvStream[ClosingSignedTlv]] = tlvStream(discriminated[ClosingSignedTlv].by(varint)) + + case class FeeRange(min: Satoshi, max: Satoshi) extends ClosingSignedTlv + + private val feeRange: Codec[FeeRange] = (("min_fee_satoshis" | satoshi) :: ("max_fee_satoshis" | satoshi)).as[FeeRange] + + val closingSignedTlvCodec: Codec[TlvStream[ClosingSignedTlv]] = tlvStream(discriminated[ClosingSignedTlv].by(varint) + .typecase(UInt64(1), variableSizeBytesLong(varintoverflow, feeRange)) + ) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index d33a5e4e5d..747f5c7242 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -148,7 +148,9 @@ case class Shutdown(channelId: ByteVector32, case class ClosingSigned(channelId: ByteVector32, feeSatoshis: Satoshi, signature: ByteVector64, - tlvStream: TlvStream[ClosingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId + tlvStream: TlvStream[ClosingSignedTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId { + val feeRange_opt = tlvStream.get[ClosingSignedTlv.FeeRange] +} case class UpdateAddHtlc(channelId: ByteVector32, id: Long, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index b9e9777259..f8b8ec24f5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -301,20 +301,24 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_FORCECLOSE(ActorRef.noSender)) ) - eclair.close(Left(ByteVector32.Zeroes) :: Nil, None) - register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None))) + eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, None) + register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, None))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, None) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None))) + val customClosingFees = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(200 sat), FeeratePerKw(1000 sat)) + eclair.close(Left(ByteVector32.Zeroes) :: Nil, None, Some(customClosingFees)) + register.expectMsg(Register.Forward(ActorRef.noSender, ByteVector32.Zeroes, CMD_CLOSE(ActorRef.noSender, None, Some(customClosingFees)))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, Some(ByteVector.empty)) - register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty)))) + eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, None, None) + register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None))) - eclair.close(Right(ShortChannelId("568749x2597x0")) :: Left(ByteVector32.One) :: Right(ShortChannelId("568749x2597x1")) :: Nil, None) + eclair.close(Right(ShortChannelId("568749x2597x0")) :: Nil, Some(ByteVector.empty), Some(customClosingFees)) + register.expectMsg(Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, Some(ByteVector.empty), Some(customClosingFees)))) + + eclair.close(Right(ShortChannelId("568749x2597x0")) :: Left(ByteVector32.One) :: Right(ShortChannelId("568749x2597x1")) :: Nil, None, None) register.expectMsgAllOf( - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None)), - Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None)), - Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x1"), CMD_CLOSE(ActorRef.noSender, None)) + Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x0"), CMD_CLOSE(ActorRef.noSender, None, None)), + Register.Forward(ActorRef.noSender, ByteVector32.One, CMD_CLOSE(ActorRef.noSender, None, None)), + Register.ForwardShortId(ActorRef.noSender, ShortChannelId("568749x2597x1"), CMD_CLOSE(ActorRef.noSender, None, None)) ) } 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 53bd4d6ab3..071cc26942 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 @@ -187,7 +187,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe 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) @@ -205,7 +205,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe 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/ChannelStateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala index af344ab4cc..0b5ee131b8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/ChannelStateTestsHelperMethods.scala @@ -287,7 +287,7 @@ trait ChannelStateTestsHelperMethods extends TestKitBase { def mutualClose(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, 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 c3e7448bf1..836b2dff51 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 @@ -290,7 +290,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 dae6e99b70..6fc21b5183 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 @@ -274,7 +274,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 25f1b61011..009f701bb0 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 0f3a3b23c1..6c2819db0c 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 @@ -123,7 +123,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) @@ -134,7 +134,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 9bb845d549..39fbe01b86 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 @@ -109,7 +109,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 a68c7783de..e77e405f37 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 @@ -195,7 +195,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 1f330dc13a..b206ff3a8c 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 @@ -124,7 +124,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 4fd9e5c001..8ca8f72a8f 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 @@ -422,7 +422,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) @@ -444,7 +444,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, 10000000 msat, randomBytes32(), CltvExpiry(300000), TestConstants.emptyOnionPacket, localOrigin(sender.ref)) @@ -1797,7 +1797,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, script_opt) + alice ! CMD_CLOSE(sender.ref, script_opt, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] script_opt.foreach(script => assert(script === shutdown.scriptPubKey)) @@ -1819,7 +1819,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] @@ -1831,7 +1831,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]] } @@ -1839,7 +1839,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] } @@ -1847,14 +1847,14 @@ 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]] } test("recv CMD_CLOSE (with unsupported native segwit script)") { f => import f._ val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, Some(hex"51050102030405")) + alice ! CMD_CLOSE(sender.ref, Some(hex"51050102030405"), None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } @@ -1867,7 +1867,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) @@ -1878,12 +1878,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]] } @@ -1894,7 +1894,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) @@ -1905,13 +1905,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.expectNoMessage(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) @@ -1921,7 +1921,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val shutdownScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) - alice ! CMD_CLOSE(sender.ref, Some(shutdownScript)) + alice ! CMD_CLOSE(sender.ref, Some(shutdownScript), None) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] } @@ -1929,7 +1929,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val shutdownScript = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey - alice ! CMD_CLOSE(sender.ref, Some(shutdownScript)) + alice ! CMD_CLOSE(sender.ref, Some(shutdownScript), None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) @@ -1940,7 +1940,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv CMD_CLOSE (upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => import f._ val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None) + alice ! CMD_CLOSE(sender.ref, None, None) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] val shutdown = alice2bob.expectMsgType[Shutdown] assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) @@ -1986,7 +1986,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) @@ -2024,7 +2024,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) @@ -2075,7 +2075,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") @@ -2123,7 +2123,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) @@ -2135,7 +2135,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 8584f6710a..5230cdd404 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 @@ -479,7 +479,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] @@ -666,7 +666,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 f1bf5f0f34..f1bcfb1f4d 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] @@ -837,10 +837,27 @@ 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]] } + test("recv CMD_CLOSE with updated feerates") { f => + import f._ + val sender = TestProbe() + val closingFeerates1 = ClosingFeerates(FeeratePerKw(500 sat), FeeratePerKw(250 sat), FeeratePerKw(2500 sat)) + alice ! CMD_CLOSE(sender.ref, None, Some(closingFeerates1)) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates1)) + + val closingScript = alice.stateData.asInstanceOf[DATA_SHUTDOWN].localShutdown.scriptPubKey + val closingFeerates2 = ClosingFeerates(FeeratePerKw(600 sat), FeeratePerKw(300 sat), FeeratePerKw(2500 sat)) + alice ! CMD_CLOSE(sender.ref, Some(closingScript.reverse), Some(closingFeerates2)) + sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] + alice ! CMD_CLOSE(sender.ref, Some(closingScript), Some(closingFeerates2)) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + assert(alice.stateData.asInstanceOf[DATA_SHUTDOWN].closingFeerates === Some(closingFeerates2)) + } + test("recv CMD_FORCECLOSE") { f => import f._ 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 0513f082c5..334468a1b5 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, Script} -import fr.acinq.eclair.TestConstants.Bob +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi, SatoshiLong} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} import fr.acinq.eclair.channel.Helpers.Closing @@ -27,11 +25,11 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishRawTx, PublishTx} import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} import fr.acinq.eclair.transactions.Transactions -import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomKey} +import fr.acinq.eclair.wire.protocol.ClosingSignedTlv.FeeRange +import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown, TlvStream, Warning} +import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -45,140 +43,389 @@ 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) - val bobShutdown = bob2alice.expectMsgType[Shutdown] - bob2alice.forward(alice) - val aliceShutdown = alice2bob.expectMsgType[Shutdown] - if (test.tags.contains(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { - assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) - assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) - } - 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]] + val aliceShutdown = alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob, aliceShutdown) + val bobShutdown = bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice, bobShutdown) + awaitCond(alice.stateName == NEGOTIATING) + if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) { + assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) + } + awaitCond(bob.stateName == NEGOTIATING) + if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) { + assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) + } + } + + 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]] + val bobShutdown = bob2alice.expectMsgType[Shutdown] + bob2alice.forward(alice, bobShutdown) + val aliceShutdown = alice2bob.expectMsgType[Shutdown] + alice2bob.forward(bob, aliceShutdown) + awaitCond(alice.stateName == NEGOTIATING) + if (alice.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) { + assert(aliceShutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) + } + awaitCond(bob.stateName == NEGOTIATING) + if (bob.stateData.asInstanceOf[HasCommitments].commitments.channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) { + assert(bobShutdown.scriptPubKey == bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey) + } + } + + def setFeerate(feeEstimator: TestConstants.TestFeeEstimator, feerate: FeeratePerKw, minFeerate: FeeratePerKw = FeeratePerKw(250 sat)): Unit = { + feeEstimator.setFeerate(FeeratesPerKw.single(feerate).copy(mempoolMinFee = minFeerate, blocks_1008 = minFeerate)) + } + 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.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] - alice2bob.forward(bob) - // bob answers with a counter proposition - val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] - assert(aliceCloseSig1.feeSatoshis > bobCloseSig1.feeSatoshis) - // actual test starts here - val initialState = alice.stateData.asInstanceOf[DATA_NEGOTIATING] - bob2alice.forward(alice) - 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(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) if (alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) { // check that the closing tx uses Alice and Bob's default closing scripts + val closingTx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].closingTxProposed.last.head.unsignedTx.tx val expectedLocalScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey val expectedRemoteScript = bob.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localParams.defaultFinalScriptPubKey - assert(closingTx.tx.txOut.map(_.publicKeyScript).toSet === Set(expectedLocalScript, expectedRemoteScript)) + assert(closingTx.txOut.map(_.publicKeyScript).toSet === Set(expectedLocalScript, expectedRemoteScript)) } - 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 + alice2bob.forward(bob) + // bob answers with a counter proposition in alice's fee range + val bobCloseSig1 = bob2alice.expectMsgType[ClosingSigned] + 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] + assert(aliceCloseSig2.feeSatoshis === bobCloseSig1.feeSatoshis) + alice2bob.forward(bob) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + + val mutualCloseTx = alice2blockchain.expectMsgType[PublishRawTx].tx + assert(bob2blockchain.expectMsgType[PublishRawTx].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[WatchTxConfirmed].txId === mutualCloseTx.txid) + assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) + 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(ChannelStateTestsTags.AnchorOutputs)) { - testClosingSigned _ + test("recv ClosingSigned (theirCloseFee != ourCloseFee, bob starts closing)") { f => + testClosingSignedDifferentFees(f, bobInitiates = true) } - test("recv ClosingSigned (anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { - testClosingSigned _ + test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + testClosingSignedDifferentFees(f) } - private def testFeeConverge(f: FixtureParam) = { + test("recv ClosingSigned (theirCloseFee != ourCloseFee, anchor outputs, upfront shutdown scripts)", Tag(ChannelStateTestsTags.AnchorOutputs), Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { 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) + setFeerate(alice.feeEstimator, FeeratePerKw(10000 sat)) + setFeerate(bob.feeEstimator, 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 (theirMaxCloseFee < ourCloseFee)") { f => + import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(5000 sat)) + setFeerate(bob.feeEstimator, 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) + } + + private def testClosingSignedSameFees(f: FixtureParam, bobInitiates: Boolean = false): Unit = { + import f._ + + // alice and bob see the same on-chain feerates + setFeerate(alice.feeEstimator, FeeratePerKw(5000 sat)) + setFeerate(bob.feeEstimator, 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[PublishRawTx].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[PublishRawTx].tx === mutualCloseTx) + assert(alice.stateName == CLOSING) + assert(bob.stateName == CLOSING) + } + + test("recv ClosingSigned (theirCloseFee == ourCloseFee)") { f => + testClosingSignedSameFees(f) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1)") { f => - testFeeConverge(f) + test("recv ClosingSigned (theirCloseFee == ourCloseFee, bob starts closing)") { f => + testClosingSignedSameFees(f, bobInitiates = true) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 2)", Tag("fee2")) { f => - testFeeConverge(f) + test("recv ClosingSigned (theirCloseFee == ourCloseFee, anchor outputs)", Tag(ChannelStateTestsTags.AnchorOutputs)) { f => + testClosingSignedSameFees(f) } - test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1, upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => - testFeeConverge(f) + test("recv ClosingSigned (theirCloseFee == ourCloseFee, upfront shutdown script)", Tag(ChannelStateTestsTags.OptionUpfrontShutdownScript)) { f => + testClosingSignedSameFees(f) + } + + test("override on-chain fee estimator (funder)") { f => + import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(10000 sat)) + setFeerate(bob.feeEstimator, 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[PublishRawTx].tx + assert(bob2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx) + awaitCond(alice.stateName === CLOSING) + awaitCond(bob.stateName === CLOSING) + } + + test("override on-chain fee estimator (fundee)") { f => + import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(10000 sat)) + setFeerate(bob.feeEstimator, 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[PublishRawTx].tx + assert(bob2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx) + awaitCond(alice.stateName === CLOSING) + awaitCond(bob.stateName === CLOSING) } test("recv ClosingSigned (nothing at stake)", Tag(ChannelStateTestsTags.NoPushMsat)) { f => import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(5000 sat)) + setFeerate(bob.feeEstimator, 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[PublishRawTx].tx - val mutualCloseTxBob = bob2blockchain.expectMsgType[PublishRawTx].tx - assert(mutualCloseTxAlice === mutualCloseTxBob) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTxAlice.txid) - assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTxBob.txid) - 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._ + setFeerate(alice.feeEstimator, 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[PublishRawTx].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[PublishRawTx].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[PublishRawTx].tx === alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.head.tx) + } + + test("recv ClosingSigned (fee too low, fundee)") { f => + import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(250 sat)) + setFeerate(bob.feeEstimator, FeeratePerKw(10000 sat), minFeerate = FeeratePerKw(750 sat)) + bobClose(f) + val aliceClosing = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing.feeRange_opt.get.max < 500.sat) + alice2bob.send(bob, aliceClosing) + // Bob refuses to sign with that fee range + bob2alice.expectMsgType[Warning] + bob2alice.expectNoMessage(100 millis) } 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.commitTxAndRemoteSig.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=99000 sat")) assert(bob2blockchain.expectMsgType[PublishRawTx].tx.txid === tx.txid) @@ -188,6 +435,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.commitTxAndRemoteSig.commitTx.tx bob ! aliceCloseSig.copy(signature = ByteVector64.Zeroes) @@ -200,21 +448,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("recv WatchFundingSpentTriggered (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[PublishRawTx].tx assert(bob2blockchain.expectMsgType[WatchTxConfirmed].txId === mutualCloseTx.txid) alice ! WatchFundingSpentTriggered(mutualCloseTx) @@ -226,33 +465,72 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike test("recv WatchFundingSpentTriggered (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 ! WatchFundingSpentTriggered(bobClosingTx.tx) - assert(alice2blockchain.expectMsgType[PublishRawTx].tx === bobClosingTx.tx) - assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === bobClosingTx.tx.txid) + setFeerate(alice.feeEstimator, FeeratePerKw(1000 sat)) + setFeerate(bob.feeEstimator, FeeratePerKw(10000 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 ! WatchFundingSpentTriggered(firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[PublishRawTx].tx === firstMutualCloseTx.tx) + assert(alice2blockchain.expectMsgType[WatchTxConfirmed].txId === firstMutualCloseTx.tx.txid) alice2blockchain.expectNoMessage(100 millis) - assert(alice.stateName == CLOSING) + 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, None) + sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] + } + + test("recv CMD_CLOSE with updated feerates") { f => + import f._ + setFeerate(alice.feeEstimator, FeeratePerKw(250 sat)) + setFeerate(bob.feeEstimator, FeeratePerKw(10000 sat), minFeerate = FeeratePerKw(750 sat)) + bobClose(f) + val aliceClosing1 = alice2bob.expectMsgType[ClosingSigned] + alice2bob.send(bob, aliceClosing1) + // Bob refuses to sign with that fee range + bob2alice.expectMsgType[Warning] + bob2alice.expectNoMessage(100 millis) + // Alice offered a fee range that was too low: she notices the warning sent by Bob and retries with higher fees. val sender = TestProbe() - alice ! CMD_CLOSE(sender.ref, None) + val closingScript = alice.stateData.asInstanceOf[DATA_NEGOTIATING].localShutdown.scriptPubKey + val closingFeerates = ClosingFeerates(FeeratePerKw(1000 sat), FeeratePerKw(500 sat), FeeratePerKw(2000 sat)) + alice ! CMD_CLOSE(sender.ref, Some(closingScript.reverse), Some(closingFeerates)) sender.expectMsgType[RES_FAILURE[CMD_CLOSE, ClosingAlreadyInProgress]] + alice ! CMD_CLOSE(sender.ref, Some(closingScript), Some(closingFeerates)) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + val aliceClosing2 = alice2bob.expectMsgType[ClosingSigned] + assert(aliceClosing2.feeSatoshis > aliceClosing1.feeSatoshis) + alice2bob.send(bob, aliceClosing2) + val bobClosing = bob2alice.expectMsgType[ClosingSigned] // Bob accepts this new fee range + bob2alice.forward(alice, bobClosing) + val aliceClosing3 = alice2bob.expectMsgType[ClosingSigned] + alice2bob.forward(bob, aliceClosing3) + val mutualCloseTx = alice2blockchain.expectMsgType[PublishRawTx].tx + assert(bob2blockchain.expectMsgType[PublishRawTx].tx === mutualCloseTx) + awaitCond(alice.stateName === CLOSING) + awaitCond(bob.stateName === CLOSING) } test("recv Error") { f => import f._ + bobClose(f) + alice2bob.expectMsgType[ClosingSigned] val tx = alice.stateData.asInstanceOf[DATA_NEGOTIATING].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx alice ! Error(ByteVector32.Zeroes, "oops") awaitCond(alice.stateName == CLOSING) 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 686026c561..5b83df3069 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 @@ -114,26 +114,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() @@ -288,31 +268,29 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelFeatures === channelFeatures) - // alice initiates a closing - alice ! CMD_CLOSE(sender.ref, None) + bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(2500 sat)).copy(mempoolMinFee = FeeratePerKw(250 sat), blocks_1008 = FeeratePerKw(250 sat))) + // 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 ! WatchFundingSpentTriggered(mutualCloseTx.tx) - alice ! WatchTxConfirmedTriggered(0, 0, mutualCloseTx.tx) - awaitCond(alice.stateName == CLOSED) + bob ! WatchFundingSpentTriggered(mutualCloseTx.tx) + bob ! WatchTxConfirmedTriggered(0, 0, mutualCloseTx.tx) + awaitCond(bob.stateName == CLOSED) } test("recv WatchFundingSpentTriggered (mutual close before converging)") { f => @@ -1601,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 1c2b331a89..f6feb733ba 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 @@ -565,7 +565,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](max = 60 seconds).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 0105cfad47..36adfdea38 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 @@ -116,7 +116,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"030002")) + assert(newbin.startsWith(hex"030007")) // make sure that round-trip yields the same data val newnormal = stateDataCodec.decode(newbin.bits).require.value assert(newnormal === oldnormal) @@ -224,7 +224,6 @@ class ChannelCodecsSpec extends AnyFunSuite { ) oldbins.foreach { oldbin => - // we decode with compat codec val oldnormal = stateDataCodec.decode(oldbin.bits).require.value // and we encode with new codec @@ -322,7 +321,7 @@ object ChannelCodecsSpec { commitInput = commitmentInput, remotePerCommitmentSecrets = ShaChain.init) - DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None) + DATA_NORMAL(commitments, ShortChannelId(42), buried = true, None, channelUpdate, None, None, None) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala index 4155eabdd1..47f1ad9557 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3Spec.scala @@ -18,10 +18,11 @@ package fr.acinq.eclair.wire.internal.channel.version3 import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.FeatureSupport.{Mandatory, Optional} import fr.acinq.eclair.Features.{ChannelRangeQueries, PaymentSecret, VariableLengthOnion} -import fr.acinq.eclair.channel.{ChannelConfig, ChannelConfigOption, RemoteParams} -import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} +import fr.acinq.eclair.channel._ import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec.normal import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs.{DATA_NORMAL_Codec, channelConfigCodec, remoteParamsCodec} +import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.stateDataCodec +import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshi, UInt64, randomKey} import org.scalatest.funsuite.AnyFunSuite import scodec.bits.{ByteVector, HexStringSyntax} @@ -94,4 +95,27 @@ class ChannelCodecs3Spec extends AnyFunSuite { val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams1).require).require === remoteParams1) } + + test("backward compatibility DATA_NORMAL_COMPAT_02_Codec") { + val oldBin = hex"00022aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009d2b17f27b3938b2b50ec713df1b1ae5fd3d23010c9e2e22385f13a168c6acf2c80000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff1600149d706d0fa71a0b6aa0f3fa400bee18102b45c8170000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e003f399d17a9e2fb13a52373d1631807c3161250b2774642fffa629858ad0831f68020b4ecc52820a93f6da40a60d7859307edc737347054d82592959ed1cd0b02e9c03db348203bfd939779bfdd825bc807e904225c3348fa5e3bb57d38ac4b9f40f850284c0df55fbfc2212cbd18cf8ab0eb5283b4b883350ff07e81988e01ae2bb71e20000000302498200000000000000000000000000002710000000002faf0800000000000bebc200242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae7d02000000012aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a7300000000000a1e418002400d0300000000001600146862a069e7038573a81f5627ae7d0c6ee5cd0acfb8180c0000000000220020a5f137a8049afcac9f0451b0f31677a81b5443f1c04c1910b37b0d2b8aa4ca0084a4eb2096e400507ac49b57910c914cff8338b8bc57884983541ee1b6919ece7431f4030b09a5fcd87b35682dea0d8faa394e47cc31af897cdf3e6ff502b086cfac37c700000000000000000000000000002710000000000bebc200000000002faf080028131acadf245e7d95d3d7a7f1ac0c0411ead7957ab00d310696b0b5d7d14ac8020597a38e090850030f255fb2781a53713faf7ba81c44de931a63a78efd9908ef000000000000000000000000000000000000000000000000000000000000ff035878c87f6ed100476648193e10a1462bfb55cea3ec5a8f4fbd0fe7304979094b242aed498450b3eb2f6aafedd40640b54efc60ae681da9e6a35299b8b6a6125a73000000002b40420f00000000002200203305e10ec90f004675285e9b0371a663ead7abe6caba8c6d5739ace6c9eab4534752210390d6a42ff78a21b41560f75359f0f8a9edaaf0ddcf1a6609130f0d5f234463662103fb08866066d5f6f539314220fc31a8e7fdf6ccd12ccde150acc59bac5bb971e052ae000000061a8000002a0000000088ec7ae2af533c270809b48f9a0b5a9650df9961a2177e04d7e9929ab319fcd0d150e3ded703b8b4a3f5faff0f2fedf22a6729760deefdea4feed868e4e3cdcf1b06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f061a8000002a000061163fb60101009000000000000003e8000858b800000014000000003b9aca000000" + 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"0003342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d301010003af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d00009f2408e50889ac3f6c19a007e25752bf143ab0d920cedb9f1c7b7b6b2270c1fe080000001000000000000044c000000001dcd65000000000000002710000000000000000000900064ff160014d0cd80743458194e5f86d9a74592765442fd15bb0000186b0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024982039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a358500000000000003e8ffffffffffffffff0000000000004e2000000000000003e80090001e03d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c50223dc803cceb3edff5deacc9965002cc366b719331e776abf76ee680e4afbf46a0315880bf27110fa57f5b75607bc32a098c209fe8a5a2a30120b2a246fd3fcd62e025dc435be4cb790e5ab12705fb3c0b717ddd9f4d0234fdb557358ecefc6a4e97a0238e3723a9d60dac60c2673ec836e2a909ba9fc888fcbd1cd2ad57ff0b10c5c360000000302498200000000000000000001000200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d2200fd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e000027100000000011e1a300000000000bebc20024342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552aed30200000001342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000007d30f88004400d030000000000160014dab249c7d3b8d826d45244f609bb90f72e691daa400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee286a04000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394ee093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d1b17b4207914784cfd82df8f93f15defff862cede27811ce4c59df98e86fd57e5b5105921e842a02a47ee6ddb313b37bc0b6706fe3011e2dd2d82066c85ab11a8d7b4e47000202241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000002b400d030000000000220020d24765dcd888e08581c2add26ddfaa6f3ef06b56578373c59915bcc25aa0c6ee8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a9142a784c1850f79298eab62c5c7709a5d468f5522788ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313010000000000000000015af302000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000001f8c46e9fa497a4f9f4fdf422840d264f67178defb43320b83c1d9825a6c152995775652ba3689025f4841f165acbd16732ad88629429b3811d71c7b03adf254402241ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de54313030000002be093040000000000220020039e4d3b414381fb8fda640b122e604b20aa6f64e87607dd9354cdc636cb483d8576a91474e49ec772064db47a555b032479b20992beeefe8763ac672102cd66c9b36fdd627754f492f15bc2387f44fd70bc4c9ccdb08df37e25250a5b197c820120876475527c2103d80a13db0dffe34a9b1ff383306a52aed0a40a99ce1c34b113ca7467327f608452ae67a91448d436723875e511f3a4fc540ca63c91637e926388ac68685e02000000011ad33e0af0717b31e19b50fd6b0469a42807eec1a080ad29cd56d04a4de5431303000000000000000001fa7904000000000022002054d7877901aedaf54d84b20a4233107c2fdab9ff637106d44097e69a813d394e101b06000000000000000000ce718c1999b28ddd1207bcdd6789541ec2f405f775efb096bd896bb237b7f5c17543684f99cb53efeade77bc386997e7b5361418c510ed442700cee38c36f18900000000000000010002fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300000000000000000000000011e1a300d91ec390338376481ff3aea604bf2106cf380d78e946e6a4fc0ad72dc0dec51b00061b100002909a15094ba406ca280bce032eab4d25eae4a9e8b51ff65574b6de2033a292cb1bc6ef576207f4b585273048502efad58af480f08dabcbff1a9642eb3b08811eb4417ac14bf6e5e3883d55ba3b7fdaf27e5ebac16ce63d183b2521867f6cedcf55692f1490da9bc655e38281a26150c44d56e02013faf1cdb2457bec4830e67c10ff0d731798d9e383cc3ce9ba21bddd8971e1d206c54c22317d75411b73abb08c9af348cca2aa2f17232b008aaef4bedb98c426041d3698924d089b59c639396ba38d2ba6d59ea2b0ee8ec9f548deced719e9aae73854d8b6c10c78890999ae09b912a8ae98d3513119bfa0ffdd3037cf5690122020f2c0d01b282ad0cbe345bd31cf7e55d8e78c61abfa62f97d2a095e2256c6569d3e6b5652b365d935d16451f2cf7df04e99e675d9eb649b583fc5d810dae3860001e667b9b78733b3ed6ca5058e6806ca4eb493c5c9a53b0ad78b9ea2091d6e94130afb1d79ce8e274a93d748af9d4ee7ae5b1c06cf1f65507bc79db22c30ae6df93a16c279e4beaf50a0e4e87449e9c830fe9b3877b7332b0632743d4e6267ffa404a35d74d79c5d724bdc9aaaa0423a157fb269b916b0022fd52bb0b2702e6abcc7d9d39d7f69a09a9814b3110cc72574fed031bbc0fd5a03fcee1181076369e76c183b2833385a38dd10773fe3228cb18aa731674a574d310a76052cf89c80077aa89380fcfdeda00508f79261c1e1886240de3bbc343eb84ea7879de24f7dbd0b1527201131bcaf5621e49c69ceec36280f9db6d27161c1676c9b11597d4bd8f10032a964fc1d1afcea594237f3408b184b16eba952778252bc6debc30e6e595c7ebbc2af0c00456ca771d024a56d9f074ca5a7f78f1ef107aa42c2a6426f8dbb2ea8962e2128cfc15161512d2e923e2879635c8ba80bc17f92d09abfae4e08a7f34decc1acd4aa4cc08181017bf248321558a8c4d1a45b92b96274dd9f336eb45d8af6d13147be3a250c17106ec6729e0cffc1af5255352296477dea870426aa82e7af3cc07156b65f8d98b482985618b69b99ee3871240db751923dbeddea6e36f5c3f0599300973fe064ccf4f15cadb7372aaab5b300674f5bf28ce905ab411f5605fba7075ed4a6b08dca3ed5867dc32e491c64e7f09ff4095081e0731cbfc53fa42c33e79bd1a26d8e9f8c5b546b07bd1b300f1f357919e955bded1f987401f4da1a453282b6978ff3bfd13b280f6253dd968cb4e7741a15faf7b4c70dbd9eab6461ca3d3a576410213bc4e144a63685488afeeb7857af4ef83e73c88b17798616a9ccbd603d61ec8f4dbbdd5fea5ca748cf40c38fec50d4eebf1e5b57fd32500baa892da24349598c3125162b2ea0ea049ea9aacdfa848f41fdf7f9eb983776bac58ddf762d131196ab18774349a5eca2fc5b51b60d7aceae1321c1decd462f9c7dbbd820399f71a3c3ccbb30a29f76cd8e94243e400c4dc6e6d32b6f94155a21eb89b6b4c0fd1660bd98509d11b3a45fb257ae1e3c5ebae7bae038200c0f7f2d4ee744c3f2d9007d6fb282b0254ccd71eae5b4c4996b52edb2f2f4120356096c81e42168ee43d98bd630b0af01c270db3fb680cd931d388e1c1260126707548ed1f27006a64db7885ac7500ce020f6349a1abf59fb3ca4bc5bdcbd19b7a0cfe82aeea62f0373d5c16002cd9adcb3ef5e0c862e2fda972530b399b0f6365f4edbc36a32bd1766763104aca34450f278418211d62ff3fff2d0de7f516028011e5681ee892288835a3ec6d0d353f8bc7168086a0eed30d5e81d8a51f7debbb2290dc1a1745e0fc2c08778d11ccb78dc7c98a8b4a4b28fd8decda3a2063ad73c23c112d609b6903613c5ad8e178c90fed2ebd10b176b17c9c2dfc630ef744ac289b437ad229b77a393c6b05c10f36ba2f80482ed6e26b3d9c34d22fffd05aa342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30000000000000001000000000bebc20092ebfcedc573c52ba840ff42bb322419ed390dc845c2fe80636b6b82cf41210000061b100002f211b0d97e6c7f7fb800dbb8d1c7536ad7ef284c40a85fc7316b771bb525c91f7e1d4bba90df0cf0d12c19906e4ae0a1b83954632b59f5f499dedaeba19d75c3bc244ccb57089abeaf29c7d73d67ae822aea2ed4d85d27dc65042589e8172aa3b66d928d7e35f027d96bf23e656701bdf13651e25bed745fef014fdec3faaba4795638299975d3cdc8d2c5b19e2e411e6e4f7b3cede7e269c8902306d25b1027e99736489fde667f4eaf5726349851986ff15a9e967385e080079a14c0a9c3001bbce5da720dcf739e67801cfe2a565b24f5515f8d8889a6d2dc0b51fae4facb72904ba8c1949550332cd8b9095655da72a9815537a80efd204d497af47fd18ef0f6d0639424eb81dab0e34f228aed9406d23773874b4c41d9a21dcaf632419e459baaca5f338f0f9718c89fb72108788d32aeb07e2eb343c45891958779e75fe0039eddaf7e5c4aa8ac2aee9928c68dc529d415780d8e14adcecc91b142b238e7657ef1dc6b6e9843bb5fbe8d04564307e515d88b34105b90b3fa9ed91025297a373507cba86160ba5b7b569948c95fd483b28b8970bebd5e17fcad76c6ed64615cc2d7297994483d94c010a210ad3602b7f359d7d1391b97968f9eaa70de83ea1168fdb05190a6e9fc7c984213da3e2a6672d09704d8e29e2aa9349c9ae44eca3dc0522c769887cbaa0926c5bad64eaf7c22399f7abcc01505d921cce15f4613406956715fe3030ab5c74506d1acc30649b596931316a8e210a368d758d7c1f406f5888499f8317a9ba629dfe85453b4413107796bde15636e6f628078863c0009db7d97cfadc2bfdcdfed237debac33dc525f25865663bc7a812099102fe9c65c8a37b179337141c8d76f365824b0db54b0b18d6c88b21406a1e35061fb4f5ef8a280f9c2ac2ada9eea8cb365d346c5e9eaa56728584aa71c4296a1c11d76f03351a91a030ae6df1da008a5d0f92207b8ca22f335b5a347707ef2500119597d5b92c559f477d8405f1d0dfd790c99ca5a7b4aeac232224ea8461ed07636931e65fee7b063d0ef49f471c04e2fd45b2fff3fb6923314e26045259640335c417ed14a0799553dda3eff5065ec5606303c42309221881c7dea17227da4003a6e396e308ca77da3b4b743a1c2771252c2fa8caad98c9985f9157dbcdd355d3a70b2edeca9682e4bba3e7da24fdd472017b89103e61ab9a4bcc9ab75bd28f7fad29327bf710b3b4759bb76983b003a0907794ad73194d14a70769ce6731b0ae21d565b53780275b6ac42c650d47f570331c7ac57fc75390d3b232eece8277a7c8453b5f28206ff1026879981f2ac83a0d4760554401e671a110de6c03cc104927c55de7254311680af4fb5d36b2199e1348f6fa50877d6cb163dd5f70381f2418f20d145d8ed5df2714cdcda59a4362b0c544ee1af01ac41bc15729951b1e53f3a135fe9011d3e938b08c50d53da259e2ab19b97a47b96d6b7fd559704ac9d1d8a223cbb88c9f77fa563c0c66524c260f0ff54471acae87e71c0507b14edf02bd42993966eea6b2f5d36db12b7fec7a63b80a13651822867db1f046081ad2cf3676b88fa2ef700b8c865a7dedcdee739795853584b74c3f59868daf79e81bf70afed8ea1ce689d67090a32dfe351fd0bdaf60b3903d7ff7c149134a95b2ddf1d63e0f89dfe0b710accf2ca3c37647973532338c59d32e562d7c55573277c1fab09d57a452f4214131178683533fbfe7726276903aacd579d36c6679187b1d22dda32fc5302b91244840a54c258e4d416deb0d20f96b214c335608db6b725175d8517f35f9d6b466c751a1253b20051863a02f2e0c073830cb7de27c96c4ccb196e079edc817ebdf70972b917b6a07c6a431567b99c6072b102e5103a22908683f60bb4fb72538ae3a3768de85f9fc682e67c6aea0e00002710000000000bebc2000000000011e1a300306acd0b21100f755a6d193a819042fde1b4c3839598e730905dfa7e6ef699220240a9cccdb58d887b42ccadeee5704030acd52bd9c7759ba01f47f0995531ca360000000000000000000000000000000000000002000000000000000000020000000000000000000334c7b350311d40b3b422e07f3b7759b400000000000000010003e3dd409b342046e7b410cc380c9a09c2ff03cdeeaf422678a9eac407da8f811a402718083016f88d5404f5508054fe25acb824342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d3000000002b40420f000000000022002091a342be9a9171a40f83766fbf34a0be03b79675dd20c14e13d91651f7f8273e47522103a1bd05e59ffa63f010cee2ff6848730c6f718da57aef6a5df381683d409a815e2103d1a9a2a574585f60abaaa33826488f9eb8d53ca76693bfa69b42ee2bc8ebe9c552ae000100400000ffffffffffff00204cec06f1f1496c01cd4840cddff19b47c24b7daa478bcaea862c6d11fcc9fff180007fffffffffff8038342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d300160014d0cd80743458194e5f86d9a74592765442fd15bb38342b8fd35a1f3384c6db37723ecc766b672f61b4b0f2e7d5d81cf1e451b584d30016001483366b494906052362a88e967304296937926cd1" + 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) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala index c422603007..987a090652 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala @@ -226,6 +226,25 @@ class LightningMessageCodecsSpec extends AnyFunSuite { } } + test("encode/decode closing_signed") { + val defaultSig = ByteVector64(hex"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") + val testCases = Seq( + hex"0100000000000000000000000000000000000000000000000000000000000000 0000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" -> ClosingSigned(ByteVector32.One, 0 sat, ByteVector64.Zeroes), + hex"0100000000000000000000000000000000000000000000000000000000000000 00000000000003e8 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" -> ClosingSigned(ByteVector32.One, 1000 sat, ByteVector64.Zeroes), + hex"0100000000000000000000000000000000000000000000000000000000000000 00000000000005dc 01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" -> ClosingSigned(ByteVector32.One, 1500 sat, defaultSig), + hex"0100000000000000000000000000000000000000000000000000000000000000 00000000000005dc 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0110000000000000006400000000000007d0" -> ClosingSigned(ByteVector32.One, 1500 sat, ByteVector64.Zeroes, TlvStream(ClosingSignedTlv.FeeRange(100 sat, 2000 sat))), + hex"0100000000000000000000000000000000000000000000000000000000000000 00000000000003e8 01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 0110000000000000006400000000000007d0" -> ClosingSigned(ByteVector32.One, 1000 sat, defaultSig, TlvStream(ClosingSignedTlv.FeeRange(100 sat, 2000 sat))), + hex"0100000000000000000000000000000000000000000000000000000000000000 0000000000000064 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0110000000000000006400000000000003e8 030401020304" -> ClosingSigned(ByteVector32.One, 100 sat, ByteVector64.Zeroes, TlvStream(Seq(ClosingSignedTlv.FeeRange(100 sat, 1000 sat)), Seq(GenericTlv(UInt64(3), hex"01020304")))), + ) + + for ((encoded, expected) <- testCases) { + val decoded = closingSignedCodec.decode(encoded.bits).require.value + assert(decoded === expected) + val reEncoded = closingSignedCodec.encode(decoded).require.bytes + assert(reEncoded === encoded) + } + } + test("encode/decode all channel messages") { val open = OpenChannel(randomBytes32(), randomBytes32(), 3 sat, 4 msat, 5 sat, UInt64(6), 7 sat, 8 msat, FeeratePerKw(9 sat), CltvExpiryDelta(10), 11, publicKey(1), point(2), point(3), point(4), point(5), point(6), 0.toByte) val accept = AcceptChannel(randomBytes32(), 3 sat, UInt64(4), 5 sat, 6 msat, 7, CltvExpiryDelta(8), 9, publicKey(1), point(2), point(3), point(4), point(5), point(6)) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala index ad3d0a0dc6..6863a03523 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala @@ -90,7 +90,7 @@ class ChannelPaneController(val channelRef: ActorRef, val peerNodeId: String) ex |""".stripMargin, ButtonType.YES, ButtonType.NO) alert.showAndWait if (alert.getResult eq ButtonType.YES) { - channelRef ! CMD_CLOSE(ActorRef.noSender, scriptPubKey = None) + channelRef ! CMD_CLOSE(ActorRef.noSender, scriptPubKey = None, feerates = None) } } }) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index f71f7f3e00..d243fddc55 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -23,8 +23,8 @@ import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.api.Service import fr.acinq.eclair.api.directives.EclairDirectives import fr.acinq.eclair.api.serde.FormParamExtractors._ -import fr.acinq.eclair.blockchain.fee.FeeratePerByte -import fr.acinq.eclair.channel.ChannelTypes +import fr.acinq.eclair.blockchain.fee.{FeeratePerByte, FeeratePerKw} +import fr.acinq.eclair.channel.{ChannelTypes, ClosingFeerates} import scodec.bits.ByteVector trait Channel { @@ -54,8 +54,15 @@ trait Channel { val close: Route = postRequest("close") { implicit t => withChannelsIdentifier { channels => - formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?) { scriptPubKey_opt => - complete(eclairApi.close(channels, scriptPubKey_opt)) + formFields("scriptPubKey".as[ByteVector](binaryDataUnmarshaller).?, "preferredFeerateSatByte".as[FeeratePerByte].?, "minFeerateSatByte".as[FeeratePerByte].?, "maxFeerateSatByte".as[FeeratePerByte].?) { + (scriptPubKey_opt, preferredFeerate_opt, minFeerate_opt, maxFeerate_opt) => + val closingFeerates = preferredFeerate_opt.map(preferredPerByte => { + val preferredFeerate = FeeratePerKw(preferredPerByte) + val minFeerate = minFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate / 2) + val maxFeerate = maxFeerate_opt.map(feerate => FeeratePerKw(feerate)).getOrElse(preferredFeerate * 2) + ClosingFeerates(preferredFeerate, minFeerate, maxFeerate) + }) + complete(eclairApi.close(channels, scriptPubKey_opt, closingFeerates)) } } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index bc7eb3ece9..1be3fdb7c0 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -312,13 +312,13 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val channelIdSerialized = channelId.toHex val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( - Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None), channelId)), + Left(channelId) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), channelId)), Left(channelId.reverse) -> Left(new RuntimeException("channel not found")), - Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) + Right(ShortChannelId(shortChannelIdSerialized)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), ByteVector32.fromValidHex(channelIdSerialized.reverse))) ) val eclair = mock[Eclair] - eclair.close(any, any)(any[Timeout]) returns Future.successful(response) + eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) val mockService = new MockService(eclair) Post("/close", FormData("shortChannelId" -> shortChannelIdSerialized).toEntity) ~> @@ -329,7 +329,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Right(ShortChannelId(shortChannelIdSerialized)) :: Nil, None)(any[Timeout]).wasCalled(once) + eclair.close(Right(ShortChannelId(shortChannelIdSerialized)) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } @@ -341,7 +341,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Left(channelId) :: Nil, None)(any[Timeout]).wasCalled(once) + eclair.close(Left(channelId) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } @@ -353,11 +353,66 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) val resp = entityAs[String] - eclair.close(Left(channelId) :: Right(ShortChannelId("42000x27x3")) :: Right(ShortChannelId("42000x561x1")) :: Nil, None)(any[Timeout]).wasCalled(once) + eclair.close(Left(channelId) :: Right(ShortChannelId("42000x27x3")) :: Right(ShortChannelId("42000x561x1")) :: Nil, None, None)(any[Timeout]).wasCalled(once) matchTestJson("close", resp) } } + test("'close' accepts custom closing feerates") { + val shortChannelId = "1701x42x3" + val response = Map[ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]( + Right(ShortChannelId(shortChannelId)) -> Right(RES_SUCCESS(CMD_CLOSE(ActorRef.noSender, None, None), randomBytes32())) + ) + + val eclair = mock[Eclair] + eclair.close(any, any, any)(any[Timeout]) returns Future.successful(response) + val mockService = new MockService(eclair) + + Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2", "maxFeerateSatByte" -> "50").toEntity) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.close) ~> + check { + assert(handled) + assert(status == OK) + val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(12500 sat)) + eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + } + + Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10").toEntity) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.close) ~> + check { + assert(handled) + assert(status == OK) + val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(5000 sat)) + eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + } + + Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "minFeerateSatByte" -> "2").toEntity) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.close) ~> + check { + assert(handled) + assert(status == OK) + val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(500 sat), FeeratePerKw(5000 sat)) + eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + } + + Post("/close", FormData("shortChannelId" -> shortChannelId, "preferredFeerateSatByte" -> "10", "maxFeerateSatByte" -> "50").toEntity) ~> + addCredentials(BasicHttpCredentials("", mockApi().password)) ~> + addHeader("Content-Type", "application/json") ~> + Route.seal(mockService.close) ~> + check { + assert(handled) + assert(status == OK) + val expectedFeerates = ClosingFeerates(FeeratePerKw(2500 sat), FeeratePerKw(1250 sat), FeeratePerKw(12500 sat)) + eclair.close(Right(ShortChannelId(shortChannelId)) :: Nil, None, Some(expectedFeerates))(any[Timeout]).wasCalled(once) + } + } + test("'connect' method should accept an URI and a triple with nodeId/host/port") { val remoteNodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") val remoteUri = NodeURI.parse("030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87@93.137.102.239:9735")