diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 70d3110c35..7dbe8ee01a 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -37,6 +37,10 @@ eclair { trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag // see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md features { + // option_upfront_shutdown_script is not activated by default. if you activate it, eclair will use a wallet (bitcoin core) address for the + // shutdown script it specifies when opening new channels (same as static remote key for example). + // make sure you understand what it implies before you activate this feature. + // option_upfront_shutdown_script = optional option_data_loss_protect = optional gossip_queries = optional gossip_queries_ex = optional diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index f91aa96435..a3247593ab 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -148,6 +148,11 @@ object Features { val mandatory = 2 } + case object OptionUpfrontShutdownScript extends Feature { + val rfcName = "option_upfront_shutdown_script" + val mandatory = 4 + } + case object ChannelRangeQueries extends Feature { val rfcName = "gossip_queries" val mandatory = 6 @@ -209,6 +214,7 @@ object Features { val knownFeatures: Set[Feature] = Set( OptionDataLossProtect, InitialRoutingSync, + OptionUpfrontShutdownScript, ChannelRangeQueries, VariableLengthOnion, ChannelRangeQueriesExtended, 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 3153deab04..40e2a80360 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 @@ -195,12 +195,15 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, _), Nothing) => + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, channelFeatures), Nothing) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty val open = OpenChannel(nodeParams.chainHash, temporaryChannelId = temporaryChannelId, fundingSatoshis = fundingSatoshis, @@ -219,9 +222,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), channelFlags = channelFlags, - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))) + tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript))) goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _, _), Nothing) if !localParams.isFunder => @@ -341,11 +342,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info("received OpenChannel={}", open) Helpers.validateParamsFundee(nodeParams, localParams.initFeatures, channelFeatures, open, remoteNodeId) match { case Left(t) => handleLocalError(t, d, Some(open)) - case _ => + case Right(remoteShutdownScript) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = false, open.temporaryChannelId, open.feeratePerKw, None)) val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey val channelKeyPath = keyManager.keyPath(localParams, channelConfig) val minimumDepth = Helpers.minDepthForFunding(nodeParams, open.fundingSatoshis) + // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used. + // See https://github.com/lightningnetwork/lightning-rfc/pull/714. + val localShutdownScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else ByteVector.empty val accept = AcceptChannel(temporaryChannelId = open.temporaryChannelId, dustLimitSatoshis = localParams.dustLimit, maxHtlcValueInFlightMsat = localParams.maxHtlcValueInFlightMsat, @@ -360,9 +364,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId delayedPaymentBasepoint = keyManager.delayedPaymentPoint(channelKeyPath).publicKey, htlcBasepoint = keyManager.htlcPoint(channelKeyPath).publicKey, firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0), - // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script. - // See https://github.com/lightningnetwork/lightning-rfc/pull/714. - tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))) + tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(localShutdownScript))) val remoteParams = RemoteParams( nodeId = remoteNodeId, dustLimit = open.dustLimitSatoshis, @@ -377,7 +379,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId delayedPaymentBasepoint = open.delayedPaymentBasepoint, htlcBasepoint = open.htlcBasepoint, initFeatures = remoteInit.features, - shutdownScript = None) + shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, None, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept } @@ -392,9 +394,9 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) => log.info(s"received AcceptChannel=$accept") - Helpers.validateParamsFunder(nodeParams, open, accept) match { + Helpers.validateParamsFunder(nodeParams, channelFeatures, open, accept) match { case Left(t) => handleLocalError(t, d, Some(accept)) - case _ => + case Right(remoteShutdownScript) => val remoteParams = RemoteParams( nodeId = remoteNodeId, dustLimit = accept.dustLimitSatoshis, @@ -409,7 +411,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId delayedPaymentBasepoint = accept.delayedPaymentBasepoint, htlcBasepoint = accept.htlcBasepoint, initFeatures = remoteInit.features, - shutdownScript = None) + shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) @@ -869,84 +871,88 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(r: RevocationTimeout, d: DATA_NORMAL) => handleRevocationTimeout(r, d) case Event(c: CMD_CLOSE, d: DATA_NORMAL) => - val localScriptPubKey = c.scriptPubKey.getOrElse(d.commitments.localParams.defaultFinalScriptPubKey) - val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.initFeatures, d.commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) - if (d.localShutdown.isDefined) { - handleCommandError(ClosingAlreadyInProgress(d.channelId), c) - } else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) { - // NB: simplistic behavior, we could also sign-then-close - handleCommandError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), c) - } else if (Commitments.localHasUnsignedOutgoingUpdateFee(d.commitments)) { - handleCommandError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), c) - } else if (!Closing.isValidFinalScriptPubkey(localScriptPubKey, allowAnySegwit)) { - handleCommandError(InvalidFinalScript(d.channelId), c) - } else { - val shutdown = Shutdown(d.channelId, localScriptPubKey) - handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown + d.commitments.getLocalShutdownScript(c.scriptPubKey) match { + case Left(e) => handleCommandError(e, c) + case Right(localShutdownScript) => + if (d.localShutdown.isDefined) { + handleCommandError(ClosingAlreadyInProgress(d.channelId), c) + } else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) { + // NB: simplistic behavior, we could also sign-then-close + handleCommandError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), c) + } else if (Commitments.localHasUnsignedOutgoingUpdateFee(d.commitments)) { + handleCommandError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), c) + } else { + val shutdown = Shutdown(d.channelId, localShutdownScript) + handleCommandSuccess(c, d.copy(localShutdown = Some(shutdown))) storing() sending shutdown + } } case Event(remoteShutdown@Shutdown(_, remoteScriptPubKey), d: DATA_NORMAL) => - // they have pending unsigned htlcs => they violated the spec, close the channel - // they don't have pending unsigned htlcs - // we have pending unsigned htlcs - // we already sent a shutdown message => spec violation (we can't send htlcs after having sent shutdown) - // we did not send a shutdown message - // we are ready to sign => we stop sending further htlcs, we initiate a signature - // we are waiting for a rev => we stop sending further htlcs, we wait for their revocation, will resign immediately after, and then we will send our shutdown message - // we have no pending unsigned htlcs - // we already sent a shutdown message - // there are pending signed changes => send our shutdown message, go to SHUTDOWN - // there are no htlcs => send our shutdown message, go to NEGOTIATING - // we did not send a shutdown message - // there are pending signed changes => go to SHUTDOWN - // there are no htlcs => go to NEGOTIATING - val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.initFeatures, d.commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit) - if (!Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit)) { - handleLocalError(InvalidFinalScript(d.channelId), d, Some(remoteShutdown)) - } else if (Commitments.remoteHasUnsignedOutgoingHtlcs(d.commitments)) { - handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown)) - } else if (Commitments.remoteHasUnsignedOutgoingUpdateFee(d.commitments)) { - handleLocalError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), d, Some(remoteShutdown)) - } else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) { // do we have unsigned outgoing htlcs? - require(d.localShutdown.isEmpty, "can't have pending unsigned outgoing htlcs after having sent Shutdown") - // are we in the middle of a signature? - d.commitments.remoteNextCommitInfo match { - case Left(waitForRevocation) => - // yes, let's just schedule a new signature ASAP, which will include all pending unsigned changes - val commitments1 = d.commitments.copy(remoteNextCommitInfo = Left(waitForRevocation.copy(reSignAsap = true))) - // in the meantime we won't send new changes - stay using d.copy(commitments = commitments1, remoteShutdown = Some(remoteShutdown)) - case Right(_) => - // no, let's sign right away - self ! CMD_SIGN() - // in the meantime we won't send new changes - stay using d.copy(remoteShutdown = Some(remoteShutdown)) - } - } else { - // so we don't have any unsigned outgoing changes - val (localShutdown, sendList) = d.localShutdown match { - case Some(localShutdown) => - (localShutdown, Nil) - case None => - val localShutdown = Shutdown(d.channelId, d.commitments.localParams.defaultFinalScriptPubKey) - // we need to send our shutdown if we didn't previously - (localShutdown, localShutdown :: Nil) - } - // are there pending signed changes on either side? we need to have received their last revocation! - if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { - // there are no pending signed changes, let's go directly to NEGOTIATING - if (d.commitments.localParams.isFunder) { - // we are funder, need to initiate the negotiation by sending the first closing_signed - val (closingTx, closingSigned) = Closing.makeFirstClosingTx(keyManager, d.commitments, localShutdown.scriptPubKey, remoteShutdown.scriptPubKey, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) - goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, List(List(ClosingTxProposed(closingTx, closingSigned))), bestUnpublishedClosingTx_opt = None) storing() sending sendList :+ closingSigned + d.commitments.getRemoteShutdownScript(remoteScriptPubKey) match { + case Left(e) => + log.warning("they sent an invalid closing script") + context.system.scheduler.scheduleOnce(2 second, peer, Peer.Disconnect(remoteNodeId)) + stay sending Warning(d.channelId, "invalid closing script") + case Right(remoteShutdownScript) => + // they have pending unsigned htlcs => they violated the spec, close the channel + // they don't have pending unsigned htlcs + // we have pending unsigned htlcs + // we already sent a shutdown message => spec violation (we can't send htlcs after having sent shutdown) + // we did not send a shutdown message + // we are ready to sign => we stop sending further htlcs, we initiate a signature + // we are waiting for a rev => we stop sending further htlcs, we wait for their revocation, will resign immediately after, and then we will send our shutdown message + // we have no pending unsigned htlcs + // we already sent a shutdown message + // there are pending signed changes => send our shutdown message, go to SHUTDOWN + // there are no htlcs => send our shutdown message, go to NEGOTIATING + // we did not send a shutdown message + // there are pending signed changes => go to SHUTDOWN + // there are no htlcs => go to NEGOTIATING + if (Commitments.remoteHasUnsignedOutgoingHtlcs(d.commitments)) { + handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown)) + } else if (Commitments.remoteHasUnsignedOutgoingUpdateFee(d.commitments)) { + handleLocalError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), d, Some(remoteShutdown)) + } else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) { // do we have unsigned outgoing htlcs? + require(d.localShutdown.isEmpty, "can't have pending unsigned outgoing htlcs after having sent Shutdown") + // are we in the middle of a signature? + d.commitments.remoteNextCommitInfo match { + case Left(waitForRevocation) => + // yes, let's just schedule a new signature ASAP, which will include all pending unsigned changes + val commitments1 = d.commitments.copy(remoteNextCommitInfo = Left(waitForRevocation.copy(reSignAsap = true))) + // in the meantime we won't send new changes + stay using d.copy(commitments = commitments1, remoteShutdown = Some(remoteShutdown)) + case Right(_) => + // no, let's sign right away + self ! CMD_SIGN() + // in the meantime we won't send new changes + stay using d.copy(remoteShutdown = Some(remoteShutdown)) + } } else { - // we are fundee, will wait for their closing_signed - goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending sendList + // so we don't have any unsigned outgoing changes + val (localShutdown, sendList) = d.localShutdown match { + case Some(localShutdown) => + (localShutdown, Nil) + case None => + val localShutdown = Shutdown(d.channelId, d.commitments.localParams.defaultFinalScriptPubKey) + // we need to send our shutdown if we didn't previously + (localShutdown, localShutdown :: Nil) + } + // are there pending signed changes on either side? we need to have received their last revocation! + if (d.commitments.hasNoPendingHtlcsOrFeeUpdate) { + // 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) + 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 + goto(NEGOTIATING) using DATA_NEGOTIATING(d.commitments, localShutdown, remoteShutdown, closingTxProposed = List(List()), bestUnpublishedClosingTx_opt = None) storing() sending sendList + } + } 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 + } } - } 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 - } } case Event(ProcessCurrentBlockCount(c), d: DATA_NORMAL) => handleNewBlock(c, d) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index f04f0a2b2d..276d374fe3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -55,6 +55,7 @@ case class CannotCloseWithUnsignedOutgoingHtlcs (override val channelId: Byte case class CannotCloseWithUnsignedOutgoingUpdateFee(override val channelId: ByteVector32) extends ChannelException(channelId, "cannot close when there is an unsigned fee update") case class ChannelUnavailable (override val channelId: ByteVector32) extends ChannelException(channelId, "channel is unavailable (offline or closing)") case class InvalidFinalScript (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid final script") +case class MissingUpfrontShutdownScript (override val channelId: ByteVector32) extends ChannelException(channelId, "missing upfront shutdown script") case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out") case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}") case class HtlcsTimedoutDownstream (override val channelId: ByteVector32, htlcs: Set[UpdateAddHtlc]) extends ChannelException(channelId, s"one or more htlcs timed out downstream: ids=${htlcs.take(10).map(_.id).mkString(",")}") // we only display the first 10 ids diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala index d739e08df5..29a3a5a7ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.channel -import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo} +import fr.acinq.eclair.Features.{AnchorOutputs, OptionUpfrontShutdownScript, StaticRemoteKey, Wumbo} import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat} import fr.acinq.eclair.{Feature, Features} @@ -63,6 +63,7 @@ object ChannelFeatures { StaticRemoteKey, Wumbo, AnchorOutputs, + OptionUpfrontShutdownScript ).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f)) ChannelFeatures(availableFeatures) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 80bcf87039..00e37aceb3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -21,6 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.Monitoring.Metrics import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.crypto.{Generators, ShaChain} @@ -30,6 +31,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.protocol._ +import scodec.bits.ByteVector // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { @@ -84,6 +86,43 @@ case class Commitments(channelId: ByteVector32, require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures") + /** + * + * @param scriptPubKey optional local script pubkey provided in CMD_CLOSE + * @return the actual local shutdown script that we should use + */ + def getLocalShutdownScript(scriptPubKey: Option[ByteVector]): Either[ChannelException, ByteVector] = { + // to check whether shutdown_any_segwit is active we check features in local and remote parameters, which are negotiated each time we connect to our peer. + val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) + (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), scriptPubKey) match { + case (true, Some(script)) if script != localParams.defaultFinalScriptPubKey => Left(InvalidFinalScript(channelId)) + case (false, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, Some(script)) => Right(script) + case _ => Right(localParams.defaultFinalScriptPubKey) + } + } + + /** + * + * @param remoteScriptPubKey remote script included in a Shutdown message + * @return the actual remote script that we should use + */ + def getRemoteShutdownScript(remoteScriptPubKey: ByteVector): Either[ChannelException, ByteVector] = { + // to check whether shutdown_any_segwit is active we check features in local and remote parameters, which are negotiated each time we connect to our peer. + val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit) + (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), remoteParams.shutdownScript) match { + case (false, _) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (false, _) => Right(remoteScriptPubKey) + case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => { + // this is a special case: they set option_upfront_shutdown_script but did not provide a script in their open/accept message + Left(InvalidFinalScript(channelId)) + } + case (true, None) => Right(remoteScriptPubKey) + case (true, Some(script)) if script != remoteScriptPubKey => Left(InvalidFinalScript(channelId)) + case (true, Some(script)) => Right(script) + } + } + def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def hasNoPendingHtlcsOrFeeUpdate: Boolean = 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 8c33672646..ec90c1fd06 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 @@ -78,10 +78,24 @@ object Helpers { nodeParams.minDepthBlocks.max(blocksToReachFunding) } + def extractShutdownScript(channelId: ByteVector32, channelFeatures: ChannelFeatures, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = + extractShutdownScript(channelId, channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), channelFeatures.hasFeature(Features.ShutdownAnySegwit), upfrontShutdownScript_opt) + + def extractShutdownScript(channelId: ByteVector32, hasOptionUpfrontShutdownScript: Boolean, allowAnySegwit: Boolean, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = { + (hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match { + case (true, None) => Left(MissingUpfrontShutdownScript(channelId)) + case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty + case (true, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId)) + case (true, Some(script)) => Right(Some(script)) + case (false, Some(_)) => Right(None) // they provided a script but the feature is not active, we just ignore it + case _ => Right(None) + } + } + /** * Called by the fundee */ - def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Unit] = { + def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Option[ByteVector]] = { // BOLT #2: if the chain_hash value, within the open_channel, message is set to a hash of a chain that is unknown to the receiver: // MUST reject the channel. if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash)) @@ -129,13 +143,13 @@ object Helpers { val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) - Right() + extractShutdownScript(open.temporaryChannelId, channelFeatures, open.upfrontShutdownScript_opt) } /** * Called by the funder */ - def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Unit] = { + def validateParamsFunder(nodeParams: NodeParams, channelFeatures: ChannelFeatures, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Option[ByteVector]] = { if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS)) // only enforce dust limit check on mainnet if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) { @@ -162,7 +176,7 @@ object Helpers { val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1) if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio)) - Right() + extractShutdownScript(accept.temporaryChannelId, channelFeatures, accept.upfrontShutdownScript_opt) } /** @@ -424,7 +438,9 @@ object Helpers { def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeratePerKw: FeeratePerKw)(implicit log: LoggingAdapter): Satoshi = { import commitments._ // this is just to estimate the weight, it depends on size of the pubkey scripts - val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec) + val 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) @@ -450,12 +466,14 @@ object Helpers { def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFee: Satoshi)(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(localScriptPubkey, allowAnySegwit), "invalid localScriptPubkey") - require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit), "invalid remoteScriptPubkey") + 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, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec) + val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimitSatoshis, closingFee, 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}") 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 2096c1037c..3be02cf3cb 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 @@ -101,7 +101,9 @@ case class OpenChannel(chainHash: ByteVector32, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, channelFlags: Byte, - tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash + tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script) +} case class AcceptChannel(temporaryChannelId: ByteVector32, dustLimitSatoshis: Satoshi, @@ -117,7 +119,9 @@ case class AcceptChannel(temporaryChannelId: ByteVector32, delayedPaymentBasepoint: PublicKey, htlcBasepoint: PublicKey, firstPerCommitmentPoint: PublicKey, - tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId + tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId { + val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script) +} case class FundingCreated(temporaryChannelId: ByteVector32, fundingTxid: ByteVector32, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala index 7e9abefba6..a5d0b829aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala @@ -69,6 +69,9 @@ class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with StateT TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelFeatures(StaticRemoteKey, Wumbo)), TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)), TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs)), + TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features.empty, ChannelFeatures()), + TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features(OptionUpfrontShutdownScript -> Optional), ChannelFeatures(OptionUpfrontShutdownScript)), + TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs, OptionUpfrontShutdownScript)), ) for (testCase <- testCases) { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index dbb1466aca..c142ad563d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -72,6 +72,8 @@ object StateTestsTags { val NoMaxHtlcValueInFlight = "no_max_htlc_value_in_flight" /** If set, max-htlc-value-in-flight will be set to a low value for Alice. */ val AliceLowMaxHtlcValueInFlight = "alice_low_max_htlc_value_in_flight" + /** If set, channels will use option_upfront_shutdown_script. */ + val OptionUpfrontShutdownScript = "option_upfront_shutdown_script" } trait StateTestsHelperMethods extends TestKitBase { @@ -86,7 +88,9 @@ trait StateTestsHelperMethods extends TestKitBase { relayerA: TestProbe, relayerB: TestProbe, channelUpdateListener: TestProbe, - wallet: EclairWallet) { + wallet: EclairWallet, + alicePeer: TestProbe, + bobPeer: TestProbe) { def currentBlockHeight: Long = alice.underlyingActor.nodeParams.currentBlockHeight } @@ -107,7 +111,7 @@ trait StateTestsHelperMethods extends TestKitBase { val router = TestProbe() val alice: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsA, wallet, Bob.nodeParams.nodeId, alice2blockchain.ref, relayerA.ref, FakeTxPublisherFactory(alice2blockchain)), alicePeer.ref) val bob: TestFSMRef[State, Data, Channel] = TestFSMRef(new Channel(nodeParamsB, wallet, Alice.nodeParams.nodeId, bob2blockchain.ref, relayerB.ref, FakeTxPublisherFactory(bob2blockchain)), bobPeer.ref) - SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet) + SetupFixture(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, router, relayerA, relayerB, channelUpdateListener, wallet, alicePeer, bobPeer) } def computeFeatures(setup: SetupFixture, tags: Set[String]): (LocalParams, ChannelFeatures, LocalParams, ChannelFeatures) = { @@ -118,11 +122,13 @@ trait StateTestsHelperMethods extends TestKitBase { .modify(_.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(StateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) val bobInitFeatures = Bob.nodeParams.features .modify(_.activated).usingIf(tags.contains(StateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional).updated(Features.AnchorOutputs, FeatureSupport.Optional)) .modify(_.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional)) + .modify(_.activated).usingIf(tags.contains(StateTestsTags.OptionUpfrontShutdownScript))(_.updated(Features.OptionUpfrontShutdownScript, FeatureSupport.Optional)) val aliceChannelFeatures = ChannelFeatures.pickChannelFeatures(aliceInitFeatures, bobInitFeatures) val bobChannelFeatures = ChannelFeatures.pickChannelFeatures(bobInitFeatures, aliceInitFeatures) 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 105667c684..f247aa2d81 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 @@ -39,7 +39,7 @@ import scala.concurrent.{Future, Promise} class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase { - case class FixtureParam(alice: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { val noopWallet = new TestWallet { @@ -70,7 +70,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) - withFixture(test.toNoArgTest(FixtureParam(alice, alice2bob, bob2alice, alice2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, alice2blockchain))) } } @@ -176,6 +176,33 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) } + test("recv AcceptChannel (upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) + bob2alice.forward(alice, accept) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].remoteParams.shutdownScript == accept.upfrontShutdownScript_opt) + } + + test("recv AcceptChannel (empty upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].localParams.defaultFinalScriptPubKey)) + val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))) + bob2alice.forward(alice, accept1) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].remoteParams.shutdownScript.isEmpty) + } + + test("recv AcceptChannel (invalid upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val accept = bob2alice.expectMsgType[AcceptChannel] + val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.fromValidHex("deadbeef")))) + bob2alice.forward(alice, accept1) + awaitCond(alice.stateName == CLOSED) + } + test("recv Error") { f => import f._ alice ! Error(ByteVector32.Zeroes, "oops") 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 44e458c69d..1e3e0eac78 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 @@ -36,7 +36,7 @@ import scala.concurrent.duration._ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase { - case class FixtureParam(bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe) + case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, bob2blockchain: TestProbe) override def withFixture(test: OneArgTest): Outcome = { @@ -56,7 +56,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) - withFixture(test.toNoArgTest(FixtureParam(bob, alice2bob, bob2alice, bob2blockchain))) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) } } @@ -216,6 +216,32 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) } + test("recv OpenChannel (upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.defaultFinalScriptPubKey)) + alice2bob.forward(bob, open) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript == open.upfrontShutdownScript_opt) + } + + test("recv OpenChannel (empty upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))) + alice2bob.forward(bob, open1) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + assert(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteParams.shutdownScript.isEmpty) + } + + test("recv OpenChannel (invalid upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val open = alice2bob.expectMsgType[OpenChannel] + val open1 = open.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.fromValidHex("deadbeef")))) + alice2bob.forward(bob, open1) + awaitCond(bob.stateName == CLOSED) + } + test("recv Error") { f => import f._ bob ! Error(ByteVector32.Zeroes, "oops") 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 655e72de53..c3043fdab3 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 @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel.states.e import akka.actor.ActorRef import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, SatoshiLong, Script, ScriptFlags, Transaction} import fr.acinq.eclair.Features.StaticRemoteKey import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ @@ -39,7 +39,7 @@ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions.{DefaultCommitmentFormat, HtlcSuccessTx, weight2fee} -import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc} +import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, ChannelUpdate, ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, TemporaryNodeFailure, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc, Warning} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits._ @@ -1915,6 +1915,37 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with awaitCond(alice.stateName == NORMAL) } + test("recv CMD_CLOSE (with a script that does not match our upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val sender = TestProbe() + val shutdownScript = Script.write(Script.pay2wpkh(randomKey().publicKey)) + alice ! CMD_CLOSE(sender.ref, Some(shutdownScript)) + sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]] + } + + test("recv CMD_CLOSE (with a script that does match our upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val sender = TestProbe() + val shutdownScript = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey + alice ! CMD_CLOSE(sender.ref, Some(shutdownScript)) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + val shutdown = alice2bob.expectMsgType[Shutdown] + assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + awaitCond(alice.stateName == NORMAL) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) + } + + test("recv CMD_CLOSE (upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + val sender = TestProbe() + alice ! CMD_CLOSE(sender.ref, None) + sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] + val shutdown = alice2bob.expectMsgType[Shutdown] + assert(shutdown.scriptPubKey == alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey) + awaitCond(alice.stateName == NORMAL) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined) + } + test("recv CMD_FORCECLOSE (with pending unsigned htlcs)") { f => import f._ val sender = TestProbe() @@ -2014,19 +2045,23 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with test("recv Shutdown (with invalid final script)") { f => import f._ bob ! Shutdown(ByteVector32.Zeroes, hex"00112233445566778899") - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(bob.stateName == CLOSING) + bob2alice.expectMsgType[Warning] + // we should fail the connection as per the BOLTs + bobPeer.fishForMessage(3 seconds) { + case Peer.Disconnect(nodeId) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId => true + case _ => false + } } test("recv Shutdown (with unsupported native segwit script)") { f => import f._ bob ! Shutdown(ByteVector32.Zeroes, hex"51050102030405") - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - awaitCond(bob.stateName == CLOSING) + bob2alice.expectMsgType[Warning] + // we should fail the connection as per the BOLTs + bobPeer.fishForMessage(3 seconds) { + case Peer.Disconnect(nodeId) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId => true + case _ => false + } } test("recv Shutdown (with native segwit script)", Tag(StateTestsTags.ShutdownAnySegwit)) { f => @@ -2042,11 +2077,22 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with bob2alice.expectMsgType[Shutdown] // actual test begins bob ! Shutdown(ByteVector32.Zeroes, hex"00112233445566778899") - bob2alice.expectMsgType[Error] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[PublishTx] - bob2blockchain.expectMsgType[WatchTxConfirmed] - awaitCond(bob.stateName == CLOSING) + // we should fail the connection as per the BOLTs + bobPeer.fishForMessage(3 seconds) { + case Peer.Disconnect(nodeId) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId => true + case _ => false + } + } + + test("recv Shutdown (with a script that does not match the upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + import f._ + bob ! Shutdown(ByteVector32.Zeroes, Script.write(Script.pay2wpkh(randomKey().publicKey))) + + // we should fail the connection as per the BOLTs + bobPeer.fishForMessage(3 seconds) { + case Peer.Disconnect(nodeId) if nodeId == bob.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId => true + case _ => false + } } def testShutdownWithHtlcs(f: FixtureParam): Unit = { 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 6cbea38dcf..fe39cddeb5 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.channel.states.g import akka.event.LoggingAdapter import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, SatoshiLong} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, SatoshiLong, Script} import fr.acinq.eclair.TestConstants.Bob import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw} @@ -28,7 +28,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishRawTx, PublishTx} import fr.acinq.eclair.channel.states.{StateTestsBase, StateTestsTags} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{CltvExpiry, MilliSatoshiLong, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -59,9 +59,13 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike bob.feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) } bob ! CMD_CLOSE(sender.ref, None) - bob2alice.expectMsgType[Shutdown] + val bobShutdown = bob2alice.expectMsgType[Shutdown] bob2alice.forward(alice) - alice2bob.expectMsgType[Shutdown] + val aliceShutdown = alice2bob.expectMsgType[Shutdown] + if (test.tags.contains(StateTestsTags.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 @@ -107,6 +111,12 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike 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 + 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 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(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 } @@ -118,6 +128,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testClosingSigned _ } + test("recv ClosingSigned (anchor outputs, upfront shutdown scripts)", Tag(StateTestsTags.AnchorOutputs), Tag(StateTestsTags.OptionUpfrontShutdownScript)) { + testClosingSigned _ + } + private def testFeeConverge(f: FixtureParam) = { import f._ var aliceCloseFee, bobCloseFee = 0.sat @@ -139,6 +153,10 @@ class NegotiatingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike testFeeConverge(f) } + test("recv ClosingSigned (theirCloseFee == ourCloseFee) (fee 1, upfront shutdown script)", Tag(StateTestsTags.OptionUpfrontShutdownScript)) { f => + testFeeConverge(f) + } + test("recv ClosingSigned (nothing at stake)", Tag(StateTestsTags.NoPushMsat)) { f => import f._ val aliceCloseFee = alice2bob.expectMsgType[ClosingSigned].feeSatoshis 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 e79de28115..eb76615543 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 @@ -132,7 +132,7 @@ class ChannelCodecsSpec extends AnyFunSuite { hex"00000303933884AAF1D6B108397E5EFE5C86BCF2D8CA8D2F700EDA99DB9214FC2712B1340004D443ECE9D9C43A11A19B554BAAA6AD150000000000000222000000003B9ACA0000000000000249F000000000000000010090001E800BD48A22F4C80A42CC8BB29A764DBAEFC95674931FBE9A4380000000C50134D4A745996002F219B5FDBA1E045374DF589ECA06ABE23CECAE47343E65EDCF800000000000011E80000001BA90824000000000000124F800000000000001F4038500F1810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E2266201E8BFEEEEED725775B8116F6F82CF8E87835A5B45B184E56F272AD70D6078118601E06212B8C8F2E25B73EE7974FDCDF007E389B437BBFE238CCC3F3BF7121B6C5E81AA8589D21E9584B24A11F3ABBA5DAD48D121DD63C57A69CD767119C05DA159CB81A649D8CC0E136EB8DFBD2268B69DCA86F8CE4A604235A03D9D37AE7B07FC563F80000000C080800000000000271C000000000177000000002808B14600000001970039BA00123767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB08800000000015E070F20000000000110010584241B5FB364208F6E64A80D1166DAD866186B10C015ED0283FF1C308C2105A0023A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA95700AD81000000000080B767F0F4F00D5E9FDF24177EF2872343D9F8FAEC65D3048BA575E70E00A0AB0880000000003E7AEDC0011ABE8A00000000001100101A9CE4B6AEF469590BC7BCC51DCEEAE9C86084055A63CC01E443C733FBE400B9B5B16800000000000B000A5E5700106D1A7097E4DE87EBAF1F8F2773842FA482002418228110805E84989A81F51ABD9D11889AE43E68FAD93659DEC019F1B8C0ADBF15A57B118B81101DCC1256F9306439AD3962C043FC47A5179CAAA001CCB23342BE0E8D92E4022780A4182281108074F306DA3751B84EC5FFB155BDCA7B8E02208BBDBC8D4F3327ABA557BF27CD1701102EF4AC8CC92F469DA9642D4D4162BC545F8B34ADE15B7D6F99808AA22B086B0180A3A910810AE1AF8A1D6F56F80855E26705F191BB07CD4E2434BC5BB1698E7E5880E226621081DE8ADFA110DC8A94D8B9E9EF616BAE8598287C8F82AFDF0FC068697D570266FDA9576F8099900000000000000000271C00000000017700000001970039BA000000002808B14648CE00AE97051EE10A3C361263F81A98165CE4AA7BA076933D4266E533585F24815C15DEACF0691332B38ECF23EC39982C5C978C748374A01BA9B30D501EE4F26E8000000000000000000000000000000000001224000000000000004B800040A911C460F1467952E3B99BED072F81BFB4454FF389636DCB399FE6A78113C28580091BB3F87A7806AF4FEF920BBF794391A1ECFC7D7632E98245D2BAF3870050558440000000000AF0387900000000000880082C2120DAFD9B21047B732540688B36D6C330C3588600AF68141FF8E18461082D0011D488408570D7C50EB7AB7C042AF13382F8C8DD83E6A7121A5E2DD8B4C73F2C407113310840EF456FD0886E454A6C5CF4F7B0B5D742CC143E47C157EF87E03434BEAB81337ED4AB8001C00F40003FFFFFFFEC7200403248A1D44DFA3AC9EC237D452C936400CAA86E9517CCCF2A8F77B7493CD70B6A00780001FFFFFFFF63A0041826829646B907A97FBD1455EA8673A12B8E7AA6EA790F7802E955CE3B69DE57E006E0001FFFFFFFF640081E51EB1F91218821E680B50E4B22DF8B094385BD33ACAE36BFC9E8C2F5AD2DA5400EC0003FFFFFFFEC7801047C26AD5435658D063EBCF73A5D0EEFE73ED6B73426246E8DFB3A21D1C4C7465001900007FFFFFFFE0040B115AC58BAAA900195893EA3B2AB408D2AD348AD047E3B6CB15E599625E38608006A0001FFFFFFFF7002033C39A21A38BB61F6FB33623771A9356D8885B7C12C939C770C939EF826286C200360000FFFFFFFFB4008104EF4271064A0973B053727C3E67352D00E25CAEED944F50782449CEAE8F50960001FFFFFFFF6390DD9FC3D3C0357A7F7C905DFBCA1C8D0F67E3EBB1974C122E95D79C380282AC222B21FA0007920001295AA1FB77029F7620A90EF7AE6A6CD31E4588B93264A7ADB76152D535C52E90B9E1B7C2376DABA316A6290F1A9730D4E5E44D0B1CB0EE6A795702E6A6BCDFCDA1A4BFEBFC134AB8847A5187ECE761D75D3CCB904274875680F51984800000000AC87E8001E480002E884D2A8080804800000000000001F4000001F40000003200000001BF08EB000" -> """{"type":"DATA_NORMAL","commitments":{"channelId":"6ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c01415611","channelConfig":[],"channelFeatures":[],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[3561221353,3653515793,2711311691,2863050005]},"dustLimit":546,"maxHtlcValueInFlightMsat":1000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":144,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"a91445e990148599176534ec9b75df92ace9263f7d3487","initFeatures":{"activated":{"option_data_loss_protect":"optional","initial_routing_sync":"optional","gossip_queries":"optional"},"unknown":[]}},"remoteParams":{"nodeId":"0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1000,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"0215c35f143adeadf010abc4ce0be323760f9a9c486978b762d31cfcb101c44cc4","revocationBasepoint":"03d17fdddddae4aeeb7022dedf059f1d0f06b4b68b6309cade4e55ae1ac0f0230c","paymentBasepoint":"03c0c4257191e5c4b6e7dcf2e9fb9be00fc713686f77fc4719987e77ee2436d8bd","delayedPaymentBasepoint":"03550b13a43d2b09649423e75774bb5a91a243bac78af4d39aece23380bb42b397","htlcBasepoint":"034c93b1981c26dd71bf7a44d16d3b950df19c94c0846b407b3a6f5cf60ff8ac7f","initFeatures":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries":"optional"},"unknown":[]}},"channelFlags":1,"localCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":1343316620,"toRemote":13656683380},"commitTxAndRemoteSig":{"commitTx":{"txid":"65fe0b1f079fa763448df3ab8d94b1ad7d377c061121376be90b9c0c1bb0cd43","tx":"02000000016ecfe1e9e01abd3fbe482efde50e4687b3f1f5d8cba609174aebce1c0141561100000000007cf5db8002357d1400000000002200203539c96d5de8d2b2178f798a3b9dd5d390c1080ab4c79803c8878e67f7c801736b62d00000000000160014bcae0020da34e12fc9bd0fd75e3f1e4ee7085f49df013320"},"remoteSig":"bd09313503ea357b3a231135c87cd1f5b26cb3bd8033e371815b7e2b4af623173b9824adf260c8735a72c58087f88f4a2f39554003996466857c1d1b25c8044f"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":20024,"spec":{"htlcs":[],"feeratePerKw":750,"toLocal":13656683380,"toRemote":1343316620},"txid":"919c015d2e0a3dc214786c24c7f035302cb9c954f740ed267a84cdca66b0be49","remotePerCommitmentPoint":"02b82bbd59e0d22665671d9e47d8733058b92f18e906e9403753661aa03dc9e4dd"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":9288,"remoteNextHtlcId":151,"originChannels":{},"remoteNextCommitInfo":"02a4471183c519e54b8ee66fb41cbe06fed1153fce258db72ce67f9a9e044f0a16","commitInput":{"outPoint":"115641011cceeb4a1709a6cbd8f5f1b387460ee5fd2e48be3fbd1ae0e9e1cf6e:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"1413373x969x0","buried":true,"channelUpdate":{"signature":"52b543f6ee053eec41521def5cd4d9a63c8b117264c94f5b6ec2a5aa6b8a5d2173c36f846edb57462d4c521e352e61a9cbc89a163961dcd4f2ae05cd4d79bf9b","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"1413373x969x0","timestamp":1561369173,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1000,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"unknownFields":""}}""", hex"0200020000000303933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13400098c4b989bbdced820a77a7186c2320e7d176a5c8b5c16d6ac2af3889d6bc8bf8080000001000000000000022200000004a817c80000000000000249f0000000000000000102d0001eff1600148061b7fbd2d84ed1884177ea785faecb2080b10302e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b300000004080aa982027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8000000000000023d000000037521048000000000000249f00000000000000001070a01e302eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b7503c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a5700000004808a52a1010000000000000004000000001046000000037e11d6000000000000000000245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aefd013b020000000001015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61040047304402207f8c1936d0a50671c993890f887c78c6019abc2a2e8018899dcdc0e891fd2b090220046b56afa2cb7e9470073c238654ecf584bcf5c00b96b91e38335a70e2739ec901483045022100871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c0220119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b01475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52aed7782c20000000000000000000040000000010460000000000000000000000037e11d600b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d802e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a000000000000000000000000000000000000000000000000000000000000ff03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d245986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b000000002bc0e1e40000000000220020690fb50de412adf9b20a7fc6c8fb86f1bfd4ebc1ef8e2d96a5a196560798d944475221023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d2102eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b52ae0001003e0000fffffffffffc0080474b8cf7bb98217dd8dc475cb7c057a3465d466728978bbb909d0a05d4ae7bbe0001fffffffffff85986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b1eedce0000010000fffffd01ae98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be54920134196992f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef09bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce0000010000027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b803933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b13402eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d88710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea3309000000001eedce000001000060e6eb14010100900000000000000001000003e800000064000000037e11d6000000" - -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[5,23,31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","unknownFields":""},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"unknownFields":""}}""" + -> """{"type":"DATA_NORMAL","commitments":{"channelId":"5986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b","channelConfig":["funding_pubkey_based_channel_keypath"],"channelFeatures":["option_static_remotekey"],"localParams":{"nodeId":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","fundingKeyPath":{"path":[2353764507,3184449568,2809819526,3258060413,392846475,1545000620,720603293,1808318336,2147483649]},"dustLimit":546,"maxHtlcValueInFlightMsat":20000000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":720,"maxAcceptedHtlcs":30,"isFunder":true,"defaultFinalScriptPubKey":"00148061b7fbd2d84ed1884177ea785faecb2080b103","walletStaticPaymentBasepoint":"02e56c8eca8d4f00df84ac34c23f49c006d57d316b7ada5c346e9d4211e11604b3","initFeatures":{"activated":{"gossip_queries":"optional","option_shutdown_anysegwit":"optional","payment_secret":"optional","option_data_loss_protect":"optional","option_static_remotekey":"optional","basic_mpp":"optional","gossip_queries_ex":"optional","option_support_large_channel":"optional","var_onion_optin":"mandatory"},"unknown":[]}},"remoteParams":{"nodeId":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","dustLimit":573,"maxHtlcValueInFlightMsat":14850000000,"channelReserve":150000,"htlcMinimum":1,"toSelfDelay":1802,"maxAcceptedHtlcs":483,"fundingPubKey":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","revocationBasepoint":"0343bf4bfbaea5c100f1f2bf1cdf82a0ef97c9a0069a2aec631e7c3084ba929b75","paymentBasepoint":"03c54e7d5ccfc13f1a6c7a441ffcfac86248574d1bc0fe9773836f4c724ea7b2bd","delayedPaymentBasepoint":"03765aaac2e8fa6dbce7de5143072e9d9d5e96a1fd451d02fe4ff803f413f303f8","htlcBasepoint":"022f3b055b0d35cde31dec5263a8ed638433e3424a4e197c06d94053985a364a57","initFeatures":{"activated":{"gossip_queries":"optional","basic_mpp":"optional","payment_secret":"mandatory","option_data_loss_protect":"mandatory","option_static_remotekey":"mandatory","option_upfront_shutdown_script":"optional","option_support_large_channel":"optional","var_onion_optin":"optional"},"unknown":[23,31]}},"channelFlags":1,"localCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":15000000000,"toRemote":0},"commitTxAndRemoteSig":{"commitTx":{"txid":"fa747ecb6f718c6831cc7148cf8d65c3468d2bb6c202605e2b82d2277491222f","tx":"02000000015986f644357956c1674f385e0878fb7bb44ab3d57ea9911fab98af8a71e1ad1b0000000000c2d6178001f8d5e4000000000022002080f1dfe71a865b605593e169677c952aaa1196fc2f541ef7d21c3b1006527b61d7782c20"},"remoteSig":"871afd240e20a171b9cba46f20555f848c5850f94ec7da7b33b9eeaf6af6653c119cda8cbf5f80986d6a4f0db2590c734d1de399a7060a477b5d94df0183625b"},"htlcTxsAndRemoteSigs":[]},"remoteCommit":{"index":4,"spec":{"htlcs":[],"feeratePerKw":4166,"toLocal":0,"toRemote":15000000000},"txid":"b5f2287b2d5edf4df5602a3c287db3b938c3f1a943e40715886db5bd400f95d8","remotePerCommitmentPoint":"02e7e1abac1feb54ee3ac2172c9e2231f77765df57664fb44a6dc2e4aa9e6a9a6a"},"localChanges":{"proposed":[],"signed":[],"acked":[]},"remoteChanges":{"proposed":[],"acked":[],"signed":[]},"localNextHtlcId":0,"remoteNextHtlcId":0,"originChannels":{},"remoteNextCommitInfo":"03fd10fe44564e2d7e1550099785c2c1bad32a5ae0feeef6e27f0c108d18b4931d","commitInput":{"outPoint":"1bade1718aaf98ab1f91a97ed5b34ab47bfb78085e384f67c156793544f68659:0","amountSatoshis":15000000},"remotePerCommitmentSecrets":null},"shortChannelId":"2026958x1x0","buried":true,"channelAnnouncement":{"nodeSignature1":"98d7a81bc1aa92fcfb74ced2213e85e0d92ae8ac622bf294b3551c7c27f6f84f782f3b318e4d0eb2c67ac719a7c65afcf85bf159f6ceea9427be549201341969","nodeSignature2":"92f6ed0e059db72105a13ec0e799bb08896cad8b4feb7e9ec7283c309b5f43123af1bd9e913fc2db018edadde8932d6992408f10c1ad020504361972dfa7fef0","bitcoinSignature1":"9bbc2b568cef3c8c006f7860106fd5984bcc271ff06c4829db2a665e59b7c0b22c311a340ff2ab9bcb74a50db10ed85503ad2d248d95af8151aca8ef96248e8f","bitcoinSignature2":"84b3075922385fbaf012f057e7ee84ecbc14c84880520b26d6fd22ab5f107db606a906efdcf0f88ffbe32dc6ecc10131e1ff0dc8d68dad89c98562557f00448b","features":{"activated":{},"unknown":[]},"chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","nodeId1":"027455aef8453d92f4706b560b61527cc217ddf14da41770e8ed6607190a1851b8","nodeId2":"03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134","bitcoinKey1":"02eff5309b9368340edc6114d738b3590e6969bec4e95d8a080cf185e8b9ce5e4b","bitcoinKey2":"023ced05ed1ab328b67477376d68a69ecd0f371a9d5843c6c3be4d31498d516d8d","unknownFields":""},"channelUpdate":{"signature":"710d73875607575f3d84bb507dd87cca5b85f0cdac84f4ccecce7af3a55897525a45070fe26c0ea43e9580d4ea4cfa62ee3273e5546911145cba6bbf56e59d8e","chainHash":"43497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000","shortChannelId":"2026958x1x0","timestamp":1625746196,"messageFlags":1,"channelFlags":1,"cltvExpiryDelta":144,"htlcMinimumMsat":1,"feeBaseMsat":1000,"feeProportionalMillionths":100,"htlcMaximumMsat":15000000000,"unknownFields":""}}""" ) refs.foreach { case (oldbin, refjson) => 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 5851bef80d..4155eabdd1 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 @@ -1,10 +1,29 @@ +/* + * Copyright 2019 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package fr.acinq.eclair.wire.internal.channel.version3 -import fr.acinq.eclair.channel.{ChannelConfigOption, ChannelConfig} +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.wire.internal.channel.ChannelCodecsSpec.normal -import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs.{DATA_NORMAL_Codec, channelConfigCodec} +import fr.acinq.eclair.wire.internal.channel.version3.ChannelCodecs3.Codecs.{DATA_NORMAL_Codec, channelConfigCodec, remoteParamsCodec} import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.HexStringSyntax +import scodec.bits.{ByteVector, HexStringSyntax} class ChannelCodecs3Spec extends AnyFunSuite { @@ -55,4 +74,24 @@ class ChannelCodecs3Spec extends AnyFunSuite { assert(decoded.options === declaredOptions) } + test("encode/decode optional shutdown script") { + val remoteParams = RemoteParams( + randomKey().publicKey, + Satoshi(600), + UInt64(123456L), + Satoshi(300), + MilliSatoshi(1000), + CltvExpiryDelta(42), + 42, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + randomKey().publicKey, + Features(ChannelRangeQueries -> Optional, VariableLengthOnion -> Mandatory, PaymentSecret -> Mandatory), + None) + assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams).require).require === remoteParams) + val remoteParams1 = remoteParams.copy(shutdownScript = Some(ByteVector.fromValidHex("deadbeef"))) + assert(remoteParamsCodec.decodeValue(remoteParamsCodec.encode(remoteParams1).require).require === remoteParams1) + } }