Skip to content

Commit fac6586

Browse files
committed
Add support for option_shutdown_anysegwit
Opt-in to allow any future segwit script in shutdown as long as it complies with BIP 141 (see lightning/bolts#672). This is particularly useful to allow wallet users to close channels to a Taproot address.
1 parent 245598d commit fac6586

File tree

5 files changed

+71
-13
lines changed

5 files changed

+71
-13
lines changed

src/commonMain/kotlin/fr/acinq/lightning/Features.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ sealed class Feature {
9191
override val mandatory get() = 20
9292
}
9393

94+
@Serializable
95+
object ShutdownAnySegwit : Feature() {
96+
override val rfcName get() = "option_shutdown_anysegwit"
97+
override val mandatory get() = 26
98+
}
99+
94100
@Serializable
95101
object ChannelType : Feature() {
96102
override val rfcName get() = "option_channel_type"
@@ -228,6 +234,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
228234
Feature.BasicMultiPartPayment,
229235
Feature.Wumbo,
230236
Feature.AnchorOutputs,
237+
Feature.ShutdownAnySegwit,
231238
Feature.ChannelType,
232239
Feature.TrampolinePayment,
233240
Feature.ZeroReserveChannels,

src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,12 +1887,13 @@ data class Normal(
18871887
}
18881888
}
18891889
is CMD_CLOSE -> {
1890+
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
18901891
val localScriptPubkey = event.command.scriptPubKey ?: commitments.localParams.defaultFinalScriptPubKey
18911892
when {
18921893
this.localShutdown != null -> handleCommandError(event.command, ClosingAlreadyInProgress(channelId), channelUpdate)
18931894
this.commitments.localHasUnsignedOutgoingHtlcs() -> handleCommandError(event.command, CannotCloseWithUnsignedOutgoingHtlcs(channelId), channelUpdate)
18941895
this.commitments.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(event.command, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate)
1895-
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey) -> handleCommandError(event.command, InvalidFinalScript(channelId), channelUpdate)
1896+
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(event.command, InvalidFinalScript(channelId), channelUpdate)
18961897
else -> {
18971898
val shutdown = Shutdown(channelId, localScriptPubkey)
18981899
val newState = this.copy(localShutdown = shutdown, closingFeerates = event.command.feerates)
@@ -1995,6 +1996,7 @@ data class Normal(
19951996
}
19961997
}
19971998
is Shutdown -> {
1999+
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
19982000
// they have pending unsigned htlcs => they violated the spec, close the channel
19992001
// they don't have pending unsigned htlcs
20002002
// we have pending unsigned htlcs
@@ -2010,7 +2012,7 @@ data class Normal(
20102012
// there are pending signed changes => go to SHUTDOWN
20112013
// there are no changes => go to NEGOTIATING
20122014
when {
2013-
!Helpers.Closing.isValidFinalScriptPubkey(event.message.scriptPubKey) -> handleLocalError(event, InvalidFinalScript(channelId))
2015+
!Helpers.Closing.isValidFinalScriptPubkey(event.message.scriptPubKey, allowAnySegwit) -> handleLocalError(event, InvalidFinalScript(channelId))
20142016
commitments.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(event, CannotCloseWithUnsignedOutgoingHtlcs(channelId))
20152017
commitments.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(event, CannotCloseWithUnsignedOutgoingUpdateFee(channelId))
20162018
commitments.localHasUnsignedOutgoingHtlcs() -> {

src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,14 +384,21 @@ object Helpers {
384384
// used only to compute tx weights and estimate fees
385385
private val dummyPublicKey by lazy { PrivateKey(ByteArray(32) { 1.toByte() }).publicKey() }
386386

387-
private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray): Boolean {
387+
private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray, allowAnySegwit: Boolean): Boolean {
388388
return runTrying {
389389
val script = Script.parse(scriptPubKey)
390-
Script.isPay2pkh(script) || Script.isPay2sh(script) || Script.isPay2wpkh(script) || Script.isPay2wsh(script)
390+
when {
391+
Script.isPay2pkh(script) -> true
392+
Script.isPay2sh(script) -> true
393+
Script.isPay2wpkh(script) -> true
394+
Script.isPay2wsh(script) -> true
395+
Script.isNativeWitnessScript(script) -> allowAnySegwit
396+
else -> false
397+
}
391398
}.getOrElse { false }
392399
}
393400

394-
fun isValidFinalScriptPubkey(scriptPubKey: ByteVector): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray())
401+
fun isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray(), allowAnySegwit)
395402

396403
// To be replaced with corresponding function in bitcoin-kmp
397404
fun btcAddressFromScriptPubKey(scriptPubKey: ByteVector, chainHash: ByteVector32): String? {
@@ -465,8 +472,9 @@ object Helpers {
465472
remoteScriptPubkey: ByteArray,
466473
closingFees: ClosingFees
467474
): Pair<ClosingTx, ClosingSigned> {
468-
require(isValidFinalScriptPubkey(localScriptPubkey)) { "invalid localScriptPubkey" }
469-
require(isValidFinalScriptPubkey(remoteScriptPubkey)) { "invalid remoteScriptPubkey" }
475+
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
476+
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" }
477+
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" }
470478
val dustLimit = commitments.localParams.dustLimit.max(commitments.remoteParams.dustLimit)
471479
val closingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, dustLimit, closingFees.preferred, commitments.localCommit.spec)
472480
val localClosingSig = keyManager.sign(closingTx, commitments.localParams.channelKeys.fundingPrivateKey)

src/commonTest/kotlin/fr/acinq/lightning/FeaturesTestsCommon.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,10 @@ class FeaturesTestsCommon : LightningTestSuite() {
208208
byteArrayOf(0x09, 0x00, 0x42, 0x00) to Features(
209209
mapOf(
210210
VariableLengthOnion to FeatureSupport.Optional,
211-
PaymentSecret to FeatureSupport.Mandatory
211+
PaymentSecret to FeatureSupport.Mandatory,
212+
ShutdownAnySegwit to FeatureSupport.Optional
212213
),
213-
setOf(UnknownFeature(24), UnknownFeature(27))
214+
setOf(UnknownFeature(24))
214215
),
215216
byteArrayOf(0x52, 0x00, 0x00, 0x00) to Features(
216217
mapOf(),

src/commonTest/kotlin/fr/acinq/lightning/channel/states/NormalTestsCommon.kt

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package fr.acinq.lightning.channel.states
22

33
import fr.acinq.bitcoin.*
4-
import fr.acinq.lightning.CltvExpiry
5-
import fr.acinq.lightning.CltvExpiryDelta
6-
import fr.acinq.lightning.Feature
4+
import fr.acinq.lightning.*
75
import fr.acinq.lightning.Lightning.randomBytes32
8-
import fr.acinq.lightning.ShortChannelId
96
import fr.acinq.lightning.blockchain.*
107
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
118
import fr.acinq.lightning.channel.*
@@ -1413,6 +1410,28 @@ class NormalTestsCommon : LightningTestSuite() {
14131410
actions1.hasCommandError<InvalidFinalScript>()
14141411
}
14151412

1413+
@Test
1414+
fun `recv CMD_CLOSE (with unsupported native segwit script)`() {
1415+
val (alice, _) = reachNormal()
1416+
assertNull(alice.localShutdown)
1417+
val (alice1, actions1) = alice.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(ByteVector("51050102030405"), null)))
1418+
assertTrue(alice1 is Normal)
1419+
actions1.hasCommandError<InvalidFinalScript>()
1420+
}
1421+
1422+
@Test
1423+
fun `recv CMD_CLOSE (with native segwit script)`() {
1424+
val (alice, _) = reachNormal(
1425+
aliceFeatures = TestConstants.Alice.nodeParams.features.copy(TestConstants.Alice.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
1426+
bobFeatures = TestConstants.Bob.nodeParams.features.copy(TestConstants.Bob.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
1427+
)
1428+
assertNull(alice.localShutdown)
1429+
val (alice1, actions1) = alice.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(ByteVector("51050102030405"), null)))
1430+
actions1.hasOutgoingMessage<Shutdown>()
1431+
assertTrue(alice1 is Normal)
1432+
assertNotNull(alice1.localShutdown)
1433+
}
1434+
14161435
@Test
14171436
fun `recv CMD_CLOSE (with signed sent htlcs)`() {
14181437
val (alice, bob) = reachNormal()
@@ -1551,6 +1570,27 @@ class NormalTestsCommon : LightningTestSuite() {
15511570
actions1.hasWatch<WatchConfirmed>()
15521571
}
15531572

1573+
@Test
1574+
fun `recv Shutdown (with unsupported native segwit script)`() {
1575+
val (_, bob) = reachNormal()
1576+
val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(Shutdown(bob.channelId, ByteVector("51050102030405"))))
1577+
assertTrue(bob1 is Closing)
1578+
actions1.hasOutgoingMessage<Error>()
1579+
assertEquals(2, actions1.filterIsInstance<ChannelAction.Blockchain.PublishTx>().count())
1580+
actions1.hasWatch<WatchConfirmed>()
1581+
}
1582+
1583+
@Test
1584+
fun `recv Shutdown (with native segwit script)`() {
1585+
val (_, bob) = reachNormal(
1586+
aliceFeatures = TestConstants.Alice.nodeParams.features.copy(TestConstants.Alice.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
1587+
bobFeatures = TestConstants.Bob.nodeParams.features.copy(TestConstants.Bob.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
1588+
)
1589+
val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(Shutdown(bob.channelId, ByteVector("51050102030405"))))
1590+
assertTrue(bob1 is Negotiating)
1591+
actions1.hasOutgoingMessage<Shutdown>()
1592+
}
1593+
15541594
@Test
15551595
fun `recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)`() {
15561596
val (alice, bob) = reachNormal()

0 commit comments

Comments
 (0)