Skip to content

Commit 5a92f84

Browse files
authored
Add support for option_shutdown_anysegwit (#1801)
Opt-in to allow any future segwit script in shutdown as long as it complies with BIP 141 (see lightning/bolts#672).
1 parent 1fbede7 commit 5a92f84

File tree

8 files changed

+58
-20
lines changed

8 files changed

+58
-20
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ eclair {
5050
basic_mpp = optional
5151
option_support_large_channel = optional
5252
option_anchor_outputs = disabled
53+
option_shutdown_anysegwit = optional
5354
trampoline_payment = disabled
5455
keysend = disabled
5556
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ object Features {
188188
val mandatory = 20
189189
}
190190

191+
case object ShutdownAnySegwit extends Feature {
192+
val rfcName = "option_shutdown_anysegwit"
193+
val mandatory = 26
194+
}
195+
191196
// TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605)
192197
// We're not advertising these bits yet in our announcements, clients have to assume support.
193198
// This is why we haven't added them yet to `areSupported`.
@@ -213,6 +218,7 @@ object Features {
213218
TrampolinePayment,
214219
StaticRemoteKey,
215220
AnchorOutputs,
221+
ShutdownAnySegwit,
216222
KeySend
217223
)
218224

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -863,14 +863,15 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
863863

864864
case Event(c: CMD_CLOSE, d: DATA_NORMAL) =>
865865
val localScriptPubKey = c.scriptPubKey.getOrElse(d.commitments.localParams.defaultFinalScriptPubKey)
866+
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.features, d.commitments.remoteParams.features, Features.ShutdownAnySegwit)
866867
if (d.localShutdown.isDefined) {
867868
handleCommandError(ClosingAlreadyInProgress(d.channelId), c)
868869
} else if (Commitments.localHasUnsignedOutgoingHtlcs(d.commitments)) {
869870
// NB: simplistic behavior, we could also sign-then-close
870871
handleCommandError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), c)
871872
} else if (Commitments.localHasUnsignedOutgoingUpdateFee(d.commitments)) {
872873
handleCommandError(CannotCloseWithUnsignedOutgoingUpdateFee(d.channelId), c)
873-
} else if (!Closing.isValidFinalScriptPubkey(localScriptPubKey)) {
874+
} else if (!Closing.isValidFinalScriptPubkey(localScriptPubKey, allowAnySegwit)) {
874875
handleCommandError(InvalidFinalScript(d.channelId), c)
875876
} else {
876877
val shutdown = Shutdown(d.channelId, localScriptPubKey)
@@ -892,7 +893,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
892893
// we did not send a shutdown message
893894
// there are pending signed changes => go to SHUTDOWN
894895
// there are no htlcs => go to NEGOTIATING
895-
if (!Closing.isValidFinalScriptPubkey(remoteScriptPubKey)) {
896+
val allowAnySegwit = Features.canUseFeature(d.commitments.localParams.features, d.commitments.remoteParams.features, Features.ShutdownAnySegwit)
897+
if (!Closing.isValidFinalScriptPubkey(remoteScriptPubKey, allowAnySegwit)) {
896898
handleLocalError(InvalidFinalScript(d.channelId), d, Some(remoteShutdown))
897899
} else if (Commitments.remoteHasUnsignedOutgoingHtlcs(d.commitments)) {
898900
handleLocalError(CannotCloseWithUnsignedOutgoingHtlcs(d.channelId), d, Some(remoteShutdown))

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,13 @@ object Helpers {
410410
// used only to compute tx weights and estimate fees
411411
lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey
412412

413-
def isValidFinalScriptPubkey(scriptPubKey: ByteVector): Boolean = {
413+
def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = {
414414
Try(Script.parse(scriptPubKey)) match {
415415
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true
416416
case Success(OP_HASH160 :: OP_PUSHDATA(scriptHash, _) :: OP_EQUAL :: Nil) if scriptHash.size == 20 => true
417417
case Success(OP_0 :: OP_PUSHDATA(pubkeyHash, _) :: Nil) if pubkeyHash.size == 20 => true
418418
case Success(OP_0 :: OP_PUSHDATA(scriptHash, _) :: Nil) if scriptHash.size == 32 => true
419+
case Success((OP_1 | OP_2 | OP_3 | OP_4 | OP_5 | OP_6 | OP_7 | OP_8 | OP_9 | OP_10 | OP_11 | OP_12 | OP_13 | OP_14 | OP_15 | OP_16) :: OP_PUSHDATA(program, _) :: Nil) if allowAnySegwit && 2 <= program.length && program.length <= 40 => true
419420
case _ => false
420421
}
421422
}
@@ -449,8 +450,9 @@ object Helpers {
449450

450451
def makeClosingTx(keyManager: ChannelKeyManager, commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, closingFee: Satoshi)(implicit log: LoggingAdapter): (ClosingTx, ClosingSigned) = {
451452
import commitments._
452-
require(isValidFinalScriptPubkey(localScriptPubkey), "invalid localScriptPubkey")
453-
require(isValidFinalScriptPubkey(remoteScriptPubkey), "invalid remoteScriptPubkey")
453+
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Features.ShutdownAnySegwit)
454+
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit), "invalid localScriptPubkey")
455+
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit), "invalid remoteScriptPubkey")
454456
log.debug("making closing tx with closingFee={} and commitments:\n{}", closingFee, Commitments.specs2String(commitments))
455457
val dustLimitSatoshis = localParams.dustLimit.max(remoteParams.dustLimit)
456458
val closingTx = Transactions.makeClosingTx(commitInput, localScriptPubkey, remoteScriptPubkey, localParams.isFunder, dustLimitSatoshis, closingFee, localCommit.spec)

eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class FeaturesSpec extends AnyFunSuite {
209209
hex"" -> Features.empty,
210210
hex"0100" -> Features(VariableLengthOnion -> Mandatory),
211211
hex"028a8a" -> Features(OptionDataLossProtect -> Optional, InitialRoutingSync -> Optional, ChannelRangeQueries -> Optional, VariableLengthOnion -> Optional, ChannelRangeQueriesExtended -> Optional, PaymentSecret -> Optional, BasicMultiPartPayment -> Optional),
212-
hex"09004200" -> Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory), Set(UnknownFeature(24), UnknownFeature(27))),
212+
hex"09004200" -> Features(Map[Feature, FeatureSupport](VariableLengthOnion -> Optional, PaymentSecret -> Mandatory, ShutdownAnySegwit -> Optional), Set(UnknownFeature(24))),
213213
hex"52000000" -> Features(Map.empty[Feature, FeatureSupport], Set(UnknownFeature(25), UnknownFeature(28), UnknownFeature(30)))
214214
)
215215

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ object StateTestsTags {
6262
val StaticRemoteKey = "static_remotekey"
6363
/** If set, channels will use option_anchor_outputs. */
6464
val AnchorOutputs = "anchor_outputs"
65+
/** If set, channels will use option_shutdown_anysegwit. */
66+
val ShutdownAnySegwit = "shutdown_anysegwit"
6567
/** If set, channels will be public (otherwise we don't announce them by default). */
6668
val ChannelsPublic = "channels_public"
6769
/** If set, no amount will be pushed when opening a channel (by default we push a small amount). */
@@ -111,6 +113,7 @@ trait StateTestsHelperMethods extends TestKitBase {
111113
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.Wumbo))(_.updated(Features.Wumbo, FeatureSupport.Optional))
112114
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.StaticRemoteKey))(_.updated(Features.StaticRemoteKey, FeatureSupport.Optional))
113115
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.AnchorOutputs))(_.updated(Features.StaticRemoteKey, FeatureSupport.Mandatory).updated(Features.AnchorOutputs, FeatureSupport.Optional))
116+
.modify(_.features.activated).usingIf(tags.contains(StateTestsTags.ShutdownAnySegwit))(_.updated(Features.ShutdownAnySegwit, FeatureSupport.Optional))
114117
}
115118

116119
def reachNormal(setup: SetupFixture, tags: Set[String] = Set.empty): Unit = {

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala

+37-13
Original file line numberDiff line numberDiff line change
@@ -1737,23 +1737,24 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
17371737
relayerA.expectNoMsg(1 seconds)
17381738
}
17391739

1740-
def testCmdClose(f: FixtureParam): Unit = {
1740+
def testCmdClose(f: FixtureParam, script_opt: Option[ByteVector]): Unit = {
17411741
import f._
17421742
val sender = TestProbe()
17431743
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isEmpty)
1744-
alice ! CMD_CLOSE(sender.ref, None)
1744+
alice ! CMD_CLOSE(sender.ref, script_opt)
17451745
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]
1746-
alice2bob.expectMsgType[Shutdown]
1746+
val shutdown = alice2bob.expectMsgType[Shutdown]
1747+
script_opt.foreach(script => assert(script === shutdown.scriptPubKey))
17471748
awaitCond(alice.stateName == NORMAL)
17481749
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined)
17491750
}
17501751

1751-
test("recv CMD_CLOSE (no pending htlcs)") {
1752-
testCmdClose _
1752+
test("recv CMD_CLOSE (no pending htlcs)") { f =>
1753+
testCmdClose(f, None)
17531754
}
17541755

1755-
test("recv CMD_CLOSE (no pending htlcs) (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) {
1756-
testCmdClose _
1756+
test("recv CMD_CLOSE (no pending htlcs) (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
1757+
testCmdClose(f, None)
17571758
}
17581759

17591760
test("recv CMD_CLOSE (with noSender)") { f =>
@@ -1794,6 +1795,17 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
17941795
sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]]
17951796
}
17961797

1798+
test("recv CMD_CLOSE (with unsupported native segwit script)") { f =>
1799+
import f._
1800+
val sender = TestProbe()
1801+
alice ! CMD_CLOSE(sender.ref, Some(hex"51050102030405"))
1802+
sender.expectMsgType[RES_FAILURE[CMD_CLOSE, InvalidFinalScript]]
1803+
}
1804+
1805+
test("recv CMD_CLOSE (with native segwit script)", Tag(StateTestsTags.ShutdownAnySegwit)) { f =>
1806+
testCmdClose(f, Some(hex"51050102030405"))
1807+
}
1808+
17971809
test("recv CMD_CLOSE (with signed sent htlcs)") { f =>
17981810
import f._
17991811
val sender = TestProbe()
@@ -1849,22 +1861,22 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
18491861
awaitCond(alice.stateName == NORMAL)
18501862
}
18511863

1852-
def testShutdown(f: FixtureParam): Unit = {
1864+
def testShutdown(f: FixtureParam, script_opt: Option[ByteVector]): Unit = {
18531865
import f._
1854-
alice ! Shutdown(ByteVector32.Zeroes, Bob.channelParams.defaultFinalScriptPubKey)
1866+
alice ! Shutdown(ByteVector32.Zeroes, script_opt.getOrElse(Bob.channelParams.defaultFinalScriptPubKey))
18551867
alice2bob.expectMsgType[Shutdown]
18561868
alice2bob.expectMsgType[ClosingSigned]
18571869
awaitCond(alice.stateName == NEGOTIATING)
18581870
// channel should be advertised as down
18591871
assert(channelUpdateListener.expectMsgType[LocalChannelDown].channelId === alice.stateData.asInstanceOf[DATA_NEGOTIATING].channelId)
18601872
}
18611873

1862-
test("recv Shutdown (no pending htlcs)") {
1863-
testShutdown _
1874+
test("recv Shutdown (no pending htlcs)") { f =>
1875+
testShutdown(f, None)
18641876
}
18651877

1866-
test("recv Shutdown (no pending htlcs) (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) {
1867-
testShutdown _
1878+
test("recv Shutdown (no pending htlcs) (anchor outputs)", Tag(StateTestsTags.AnchorOutputs)) { f =>
1879+
testShutdown(f, None)
18681880
}
18691881

18701882
test("recv Shutdown (with unacked sent htlcs)") { f =>
@@ -1938,6 +1950,18 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
19381950
awaitCond(bob.stateName == CLOSING)
19391951
}
19401952

1953+
test("recv Shutdown (with unsupported native segwit script)") { f =>
1954+
import f._
1955+
bob ! Shutdown(ByteVector32.Zeroes, hex"51050102030405")
1956+
bob2alice.expectMsgType[Error]
1957+
bob2blockchain.expectMsgType[PublishTx]
1958+
awaitCond(bob.stateName == CLOSING)
1959+
}
1960+
1961+
test("recv Shutdown (with native segwit script)", Tag(StateTestsTags.ShutdownAnySegwit)) { f =>
1962+
testShutdown(f, Some(hex"51050102030405"))
1963+
}
1964+
19411965
test("recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)") { f =>
19421966
import f._
19431967
val sender = TestProbe()

eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentRequestSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ class PaymentRequestSpec extends AnyFunSuite {
399399
// those are useful for nonreg testing of the areSupported method (which needs to be updated with every new supported mandatory bit)
400400
PaymentRequestFeatures(bin" 0010000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
401401
PaymentRequestFeatures(bin" 000001000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
402-
PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
402+
PaymentRequestFeatures(bin" 000100000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = true),
403403
PaymentRequestFeatures(bin"00000010000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false),
404404
PaymentRequestFeatures(bin"00001000000000000000000000000000000") -> Result(allowMultiPart = false, requirePaymentSecret = false, areSupported = false)
405405
)

0 commit comments

Comments
 (0)