Skip to content

Commit 01b4073

Browse files
authored
Implement option-upfront-shutdown-script (#1846)
* Implement option-upfront-shutdown-script * Do not activate option_upfront_shutdown_Script by defaut Users will need to explicitly activate it. * Send back a warning when we receive an invalid shutdown script
1 parent 8c49f77 commit 01b4073

16 files changed

+369
-125
lines changed

eclair-core/src/main/resources/reference.conf

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ eclair {
3737
trampoline-payments-enable = false // TODO: @t-bast: once spec-ed this should use a global feature flag
3838
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/09-features.md
3939
features {
40+
// option_upfront_shutdown_script is not activated by default. if you activate it, eclair will use a wallet (bitcoin core) address for the
41+
// shutdown script it specifies when opening new channels (same as static remote key for example).
42+
// make sure you understand what it implies before you activate this feature.
43+
// option_upfront_shutdown_script = optional
4044
option_data_loss_protect = optional
4145
gossip_queries = optional
4246
gossip_queries_ex = optional

eclair-core/src/main/scala/fr/acinq/eclair/Features.scala

+6
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ object Features {
148148
val mandatory = 2
149149
}
150150

151+
case object OptionUpfrontShutdownScript extends Feature {
152+
val rfcName = "option_upfront_shutdown_script"
153+
val mandatory = 4
154+
}
155+
151156
case object ChannelRangeQueries extends Feature {
152157
val rfcName = "gossip_queries"
153158
val mandatory = 6
@@ -209,6 +214,7 @@ object Features {
209214
val knownFeatures: Set[Feature] = Set(
210215
OptionDataLossProtect,
211216
InitialRoutingSync,
217+
OptionUpfrontShutdownScript,
212218
ChannelRangeQueries,
213219
VariableLengthOnion,
214220
ChannelRangeQueriesExtended,

eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

+91-85
Large diffs are not rendered by default.

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ case class CannotCloseWithUnsignedOutgoingHtlcs (override val channelId: Byte
5555
case class CannotCloseWithUnsignedOutgoingUpdateFee(override val channelId: ByteVector32) extends ChannelException(channelId, "cannot close when there is an unsigned fee update")
5656
case class ChannelUnavailable (override val channelId: ByteVector32) extends ChannelException(channelId, "channel is unavailable (offline or closing)")
5757
case class InvalidFinalScript (override val channelId: ByteVector32) extends ChannelException(channelId, "invalid final script")
58+
case class MissingUpfrontShutdownScript (override val channelId: ByteVector32) extends ChannelException(channelId, "missing upfront shutdown script")
5859
case class FundingTxTimedout (override val channelId: ByteVector32) extends ChannelException(channelId, "funding tx timed out")
5960
case class FundingTxSpent (override val channelId: ByteVector32, spendingTx: Transaction) extends ChannelException(channelId, s"funding tx has been spent by txid=${spendingTx.txid}")
6061
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

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelFeatures.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package fr.acinq.eclair.channel
1818

19-
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
19+
import fr.acinq.eclair.Features.{AnchorOutputs, OptionUpfrontShutdownScript, StaticRemoteKey, Wumbo}
2020
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
2121
import fr.acinq.eclair.{Feature, Features}
2222

@@ -63,6 +63,7 @@ object ChannelFeatures {
6363
StaticRemoteKey,
6464
Wumbo,
6565
AnchorOutputs,
66+
OptionUpfrontShutdownScript
6667
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
6768

6869
ChannelFeatures(availableFeatures)

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

+39
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256}
2121
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, SatoshiLong}
2222
import fr.acinq.eclair._
2323
import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf}
24+
import fr.acinq.eclair.channel.Helpers.Closing
2425
import fr.acinq.eclair.channel.Monitoring.Metrics
2526
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
2627
import fr.acinq.eclair.crypto.{Generators, ShaChain}
@@ -30,6 +31,7 @@ import fr.acinq.eclair.transactions.DirectedHtlc._
3031
import fr.acinq.eclair.transactions.Transactions._
3132
import fr.acinq.eclair.transactions._
3233
import fr.acinq.eclair.wire.protocol._
34+
import scodec.bits.ByteVector
3335

3436
// @formatter:off
3537
case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) {
@@ -84,6 +86,43 @@ case class Commitments(channelId: ByteVector32,
8486

8587
require(channelFeatures.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (channel features: $channelFeatures")
8688

89+
/**
90+
*
91+
* @param scriptPubKey optional local script pubkey provided in CMD_CLOSE
92+
* @return the actual local shutdown script that we should use
93+
*/
94+
def getLocalShutdownScript(scriptPubKey: Option[ByteVector]): Either[ChannelException, ByteVector] = {
95+
// 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.
96+
val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit)
97+
(channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), scriptPubKey) match {
98+
case (true, Some(script)) if script != localParams.defaultFinalScriptPubKey => Left(InvalidFinalScript(channelId))
99+
case (false, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId))
100+
case (false, Some(script)) => Right(script)
101+
case _ => Right(localParams.defaultFinalScriptPubKey)
102+
}
103+
}
104+
105+
/**
106+
*
107+
* @param remoteScriptPubKey remote script included in a Shutdown message
108+
* @return the actual remote script that we should use
109+
*/
110+
def getRemoteShutdownScript(remoteScriptPubKey: ByteVector): Either[ChannelException, ByteVector] = {
111+
// 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.
112+
val allowAnySegwit = Features.canUseFeature(localParams.initFeatures, remoteParams.initFeatures, Features.ShutdownAnySegwit)
113+
(channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), remoteParams.shutdownScript) match {
114+
case (false, _) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => Left(InvalidFinalScript(channelId))
115+
case (false, _) => Right(remoteScriptPubKey)
116+
case (true, None) if !Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit) => {
117+
// this is a special case: they set option_upfront_shutdown_script but did not provide a script in their open/accept message
118+
Left(InvalidFinalScript(channelId))
119+
}
120+
case (true, None) => Right(remoteScriptPubKey)
121+
case (true, Some(script)) if script != remoteScriptPubKey => Left(InvalidFinalScript(channelId))
122+
case (true, Some(script)) => Right(script)
123+
}
124+
}
125+
87126
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight
88127

89128
def hasNoPendingHtlcsOrFeeUpdate: Boolean =

eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala

+26-8
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,24 @@ object Helpers {
7878
nodeParams.minDepthBlocks.max(blocksToReachFunding)
7979
}
8080

81+
def extractShutdownScript(channelId: ByteVector32, channelFeatures: ChannelFeatures, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] =
82+
extractShutdownScript(channelId, channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript), channelFeatures.hasFeature(Features.ShutdownAnySegwit), upfrontShutdownScript_opt)
83+
84+
def extractShutdownScript(channelId: ByteVector32, hasOptionUpfrontShutdownScript: Boolean, allowAnySegwit: Boolean, upfrontShutdownScript_opt: Option[ByteVector]): Either[ChannelException, Option[ByteVector]] = {
85+
(hasOptionUpfrontShutdownScript, upfrontShutdownScript_opt) match {
86+
case (true, None) => Left(MissingUpfrontShutdownScript(channelId))
87+
case (true, Some(script)) if script.isEmpty => Right(None) // but the provided script can be empty
88+
case (true, Some(script)) if !Closing.isValidFinalScriptPubkey(script, allowAnySegwit) => Left(InvalidFinalScript(channelId))
89+
case (true, Some(script)) => Right(Some(script))
90+
case (false, Some(_)) => Right(None) // they provided a script but the feature is not active, we just ignore it
91+
case _ => Right(None)
92+
}
93+
}
94+
8195
/**
8296
* Called by the fundee
8397
*/
84-
def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Unit] = {
98+
def validateParamsFundee(nodeParams: NodeParams, initFeatures: Features, channelFeatures: ChannelFeatures, open: OpenChannel, remoteNodeId: PublicKey): Either[ChannelException, Option[ByteVector]] = {
8599
// 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:
86100
// MUST reject the channel.
87101
if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash))
@@ -129,13 +143,13 @@ object Helpers {
129143
val reserveToFundingRatio = open.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
130144
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, open.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
131145

132-
Right()
146+
extractShutdownScript(open.temporaryChannelId, channelFeatures, open.upfrontShutdownScript_opt)
133147
}
134148

135149
/**
136150
* Called by the funder
137151
*/
138-
def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Unit] = {
152+
def validateParamsFunder(nodeParams: NodeParams, channelFeatures: ChannelFeatures, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Option[ByteVector]] = {
139153
if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
140154
// only enforce dust limit check on mainnet
141155
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
@@ -162,7 +176,7 @@ object Helpers {
162176
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
163177
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))
164178

165-
Right()
179+
extractShutdownScript(accept.temporaryChannelId, channelFeatures, accept.upfrontShutdownScript_opt)
166180
}
167181

168182
/**
@@ -424,7 +438,9 @@ object Helpers {
424438
def firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, feeratePerKw: FeeratePerKw)(implicit log: LoggingAdapter): Satoshi = {
425439
import commitments._
426440
// this is just to estimate the weight, it depends on size of the pubkey scripts
427-
val dummyClosingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
441+
val actualLocalScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey
442+
val actualRemoteScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey
443+
val dummyClosingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, Satoshi(0), Satoshi(0), localCommit.spec)
428444
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx)
429445
log.info(s"using feeratePerKw=$feeratePerKw for initial closing tx")
430446
Transactions.weight2fee(feeratePerKw, closingWeight)
@@ -450,12 +466,14 @@ object Helpers {
450466

451467
def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
452468
import commitments._
469+
val actualLocalScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) localParams.defaultFinalScriptPubKey else localScriptPubkey
470+
val actualRemoteScript = if (channelFeatures.hasFeature(Features.OptionUpfrontShutdownScript)) remoteParams.shutdownScript.getOrElse(remoteScriptPubkey) else remoteScriptPubkey
453471
val allowAnySegwit = Features.canUseFeature(commitments.localParams.initFeatures, commitments.remoteParams.initFeatures, Features.ShutdownAnySegwit)
454-
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit), "invalid localScriptPubkey")
455-
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit), "invalid remoteScriptPubkey")
472+
require(isValidFinalScriptPubkey(actualLocalScript, allowAnySegwit), "invalid localScriptPubkey")
473+
require(isValidFinalScriptPubkey(actualRemoteScript, allowAnySegwit), "invalid remoteScriptPubkey")
456474
log.debug("making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments))
457475
val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit)
458-
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
476+
val closingTx = Transactions.makeClosingTx(commitInput, actualLocalScript, actualRemoteScript, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)
459477
val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitments.localParams.fundingKeyPath), TxOwner.Local, commitmentFormat)
460478
val closingSigned = ClosingSigned(channelId, closingFee, localClosingSig)
461479
log.info(s"signed closing txid=${closingTx.tx.txid} with closingFeeSatoshis=${closingSigned.feeSatoshis}")

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala

+6-2
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ case class OpenChannel(chainHash: ByteVector32,
101101
htlcBasepoint: PublicKey,
102102
firstPerCommitmentPoint: PublicKey,
103103
channelFlags: Byte,
104-
tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash
104+
tlvStream: TlvStream[OpenChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId with HasChainHash {
105+
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script)
106+
}
105107

106108
case class AcceptChannel(temporaryChannelId: ByteVector32,
107109
dustLimitSatoshis: Satoshi,
@@ -117,7 +119,9 @@ case class AcceptChannel(temporaryChannelId: ByteVector32,
117119
delayedPaymentBasepoint: PublicKey,
118120
htlcBasepoint: PublicKey,
119121
firstPerCommitmentPoint: PublicKey,
120-
tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId
122+
tlvStream: TlvStream[AcceptChannelTlv] = TlvStream.empty) extends ChannelMessage with HasTemporaryChannelId {
123+
val upfrontShutdownScript_opt: Option[ByteVector] = tlvStream.get[ChannelTlv.UpfrontShutdownScript].map(_.script)
124+
}
121125

122126
case class FundingCreated(temporaryChannelId: ByteVector32,
123127
fundingTxid: ByteVector32,

eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelTypesSpec.scala

+3
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class ChannelTypesSpec extends TestKitBaseClass with AnyFunSuiteLike with StateT
6969
TestCase(Features(StaticRemoteKey -> Optional, Wumbo -> Optional), Features(StaticRemoteKey -> Mandatory, Wumbo -> Mandatory), ChannelFeatures(StaticRemoteKey, Wumbo)),
7070
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional), ChannelFeatures(StaticRemoteKey)),
7171
TestCase(Features(StaticRemoteKey -> Mandatory, AnchorOutputs -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs)),
72+
TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features.empty, ChannelFeatures()),
73+
TestCase(Features(OptionUpfrontShutdownScript -> Optional), Features(OptionUpfrontShutdownScript -> Optional), ChannelFeatures(OptionUpfrontShutdownScript)),
74+
TestCase(Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), Features(StaticRemoteKey -> Optional, AnchorOutputs -> Optional, OptionUpfrontShutdownScript -> Optional), ChannelFeatures(StaticRemoteKey, AnchorOutputs, OptionUpfrontShutdownScript)),
7275
)
7376

7477
for (testCase <- testCases) {

0 commit comments

Comments
 (0)