Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for option_shutdown_anysegwit #312

Merged
merged 2 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ sealed class Feature {
override val mandatory get() = 20
}

@Serializable
object ShutdownAnySegwit : Feature() {
override val rfcName get() = "option_shutdown_anysegwit"
override val mandatory get() = 26
}

@Serializable
object ChannelType : Feature() {
override val rfcName get() = "option_channel_type"
Expand Down Expand Up @@ -228,6 +234,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.BasicMultiPartPayment,
Feature.Wumbo,
Feature.AnchorOutputs,
Feature.ShutdownAnySegwit,
Feature.ChannelType,
Feature.TrampolinePayment,
Feature.ZeroReserveChannels,
Expand Down
6 changes: 4 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1887,12 +1887,13 @@ data class Normal(
}
}
is CMD_CLOSE -> {
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
val localScriptPubkey = event.command.scriptPubKey ?: commitments.localParams.defaultFinalScriptPubKey
when {
this.localShutdown != null -> handleCommandError(event.command, ClosingAlreadyInProgress(channelId), channelUpdate)
this.commitments.localHasUnsignedOutgoingHtlcs() -> handleCommandError(event.command, CannotCloseWithUnsignedOutgoingHtlcs(channelId), channelUpdate)
this.commitments.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(event.command, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate)
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey) -> handleCommandError(event.command, InvalidFinalScript(channelId), channelUpdate)
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(event.command, InvalidFinalScript(channelId), channelUpdate)
else -> {
val shutdown = Shutdown(channelId, localScriptPubkey)
val newState = this.copy(localShutdown = shutdown, closingFeerates = event.command.feerates)
Expand Down Expand Up @@ -1995,6 +1996,7 @@ data class Normal(
}
}
is Shutdown -> {
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
// they have pending unsigned htlcs => they violated the spec, close the channel
// they don't have pending unsigned htlcs
// we have pending unsigned htlcs
Expand All @@ -2010,7 +2012,7 @@ data class Normal(
// there are pending signed changes => go to SHUTDOWN
// there are no changes => go to NEGOTIATING
when {
!Helpers.Closing.isValidFinalScriptPubkey(event.message.scriptPubKey) -> handleLocalError(event, InvalidFinalScript(channelId))
!Helpers.Closing.isValidFinalScriptPubkey(event.message.scriptPubKey, allowAnySegwit) -> handleLocalError(event, InvalidFinalScript(channelId))
commitments.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(event, CannotCloseWithUnsignedOutgoingHtlcs(channelId))
commitments.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(event, CannotCloseWithUnsignedOutgoingUpdateFee(channelId))
commitments.localHasUnsignedOutgoingHtlcs() -> {
Expand Down
19 changes: 14 additions & 5 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,22 @@ object Helpers {
// used only to compute tx weights and estimate fees
private val dummyPublicKey by lazy { PrivateKey(ByteArray(32) { 1.toByte() }).publicKey() }

private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray): Boolean {
private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray, allowAnySegwit: Boolean): Boolean {
return runTrying {
val script = Script.parse(scriptPubKey)
Script.isPay2pkh(script) || Script.isPay2sh(script) || Script.isPay2wpkh(script) || Script.isPay2wsh(script)
when {
Script.isPay2pkh(script) -> true
Script.isPay2sh(script) -> true
Script.isPay2wpkh(script) -> true
Script.isPay2wsh(script) -> true
// option_shutdown_anysegwit doesn't cover segwit v0
Script.isNativeWitnessScript(script) && script[0] != OP_0 -> allowAnySegwit
else -> false
}
}.getOrElse { false }
}

fun isValidFinalScriptPubkey(scriptPubKey: ByteVector): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray())
fun isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean): Boolean = isValidFinalScriptPubkey(scriptPubKey.toByteArray(), allowAnySegwit)

// To be replaced with corresponding function in bitcoin-kmp
fun btcAddressFromScriptPubKey(scriptPubKey: ByteVector, chainHash: ByteVector32): String? {
Expand Down Expand Up @@ -465,8 +473,9 @@ object Helpers {
remoteScriptPubkey: ByteArray,
closingFees: ClosingFees
): Pair<ClosingTx, ClosingSigned> {
require(isValidFinalScriptPubkey(localScriptPubkey)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey)) { "invalid remoteScriptPubkey" }
val allowAnySegwit = Features.canUseFeature(commitments.localParams.features, commitments.remoteParams.features, Feature.ShutdownAnySegwit)
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" }
val dustLimit = commitments.localParams.dustLimit.max(commitments.remoteParams.dustLimit)
val closingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, dustLimit, closingFees.preferred, commitments.localCommit.spec)
val localClosingSig = keyManager.sign(closingTx, commitments.localParams.channelKeys.fundingPrivateKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,10 @@ class FeaturesTestsCommon : LightningTestSuite() {
byteArrayOf(0x09, 0x00, 0x42, 0x00) to Features(
mapOf(
VariableLengthOnion to FeatureSupport.Optional,
PaymentSecret to FeatureSupport.Mandatory
PaymentSecret to FeatureSupport.Mandatory,
ShutdownAnySegwit to FeatureSupport.Optional
),
setOf(UnknownFeature(24), UnknownFeature(27))
setOf(UnknownFeature(24))
),
byteArrayOf(0x52, 0x00, 0x00, 0x00) to Features(
mapOf(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package fr.acinq.lightning.channel.states

import fr.acinq.bitcoin.*
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.Feature
import fr.acinq.lightning.*
import fr.acinq.lightning.Lightning.randomBytes32
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.blockchain.*
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.*
Expand Down Expand Up @@ -1413,6 +1410,28 @@ class NormalTestsCommon : LightningTestSuite() {
actions1.hasCommandError<InvalidFinalScript>()
}

@Test
fun `recv CMD_CLOSE (with unsupported native segwit script)`() {
val (alice, _) = reachNormal()
assertNull(alice.localShutdown)
val (alice1, actions1) = alice.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(ByteVector("51050102030405"), null)))
assertTrue(alice1 is Normal)
actions1.hasCommandError<InvalidFinalScript>()
}

@Test
fun `recv CMD_CLOSE (with native segwit script)`() {
val (alice, _) = reachNormal(
aliceFeatures = TestConstants.Alice.nodeParams.features.copy(TestConstants.Alice.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
bobFeatures = TestConstants.Bob.nodeParams.features.copy(TestConstants.Bob.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
)
assertNull(alice.localShutdown)
val (alice1, actions1) = alice.processEx(ChannelEvent.ExecuteCommand(CMD_CLOSE(ByteVector("51050102030405"), null)))
actions1.hasOutgoingMessage<Shutdown>()
assertTrue(alice1 is Normal)
assertNotNull(alice1.localShutdown)
}

@Test
fun `recv CMD_CLOSE (with signed sent htlcs)`() {
val (alice, bob) = reachNormal()
Expand Down Expand Up @@ -1551,6 +1570,27 @@ class NormalTestsCommon : LightningTestSuite() {
actions1.hasWatch<WatchConfirmed>()
}

@Test
fun `recv Shutdown (with unsupported native segwit script)`() {
val (_, bob) = reachNormal()
val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(Shutdown(bob.channelId, ByteVector("51050102030405"))))
assertTrue(bob1 is Closing)
actions1.hasOutgoingMessage<Error>()
assertEquals(2, actions1.filterIsInstance<ChannelAction.Blockchain.PublishTx>().count())
actions1.hasWatch<WatchConfirmed>()
}

@Test
fun `recv Shutdown (with native segwit script)`() {
val (_, bob) = reachNormal(
aliceFeatures = TestConstants.Alice.nodeParams.features.copy(TestConstants.Alice.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
bobFeatures = TestConstants.Bob.nodeParams.features.copy(TestConstants.Bob.nodeParams.features.activated + (Feature.ShutdownAnySegwit to FeatureSupport.Optional)),
)
val (bob1, actions1) = bob.processEx(ChannelEvent.MessageReceived(Shutdown(bob.channelId, ByteVector("51050102030405"))))
assertTrue(bob1 is Negotiating)
actions1.hasOutgoingMessage<Shutdown>()
}

@Test
fun `recv Shutdown (with invalid final script and signed htlcs, in response to a Shutdown)`() {
val (alice, bob) = reachNormal()
Expand Down