Skip to content

Commit

Permalink
Add support for OP_RETURN closing scripts
Browse files Browse the repository at this point in the history
The spec allows the closer to use an OP_RETURN output if their amount is
too low when using `option_simple_close`.
  • Loading branch information
t-bast committed Mar 27, 2024
1 parent 9bbf579 commit 3fa7de6
Show file tree
Hide file tree
Showing 4 changed files with 15 additions and 19 deletions.
22 changes: 7 additions & 15 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ 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, allowAnySegwit: Boolean): Boolean {
private fun isValidFinalScriptPubkey(scriptPubKey: ByteArray, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean {
return runTrying {
val script = Script.parse(scriptPubKey)
when {
Expand All @@ -366,12 +366,13 @@ object Helpers {
Script.isPay2wsh(script) -> true
// option_shutdown_anysegwit doesn't cover segwit v0
Script.isNativeWitnessScript(script) && script[0] != OP_0 -> allowAnySegwit
script[0] == OP_RETURN -> allowOpReturn
else -> false
}
}.getOrElse { false }
}

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

private fun firstClosingFee(commitment: FullCommitment, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: ClosingFeerates): ClosingFees {
// this is just to estimate the weight which depends on the size of the pubkey scripts
Expand Down Expand Up @@ -404,8 +405,9 @@ object Helpers {
closingFees: ClosingFees
): Pair<ClosingTx, ClosingSigned> {
val allowAnySegwit = Features.canUseFeature(commitment.params.localParams.features, commitment.params.remoteParams.features, Feature.ShutdownAnySegwit)
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit)) { "invalid remoteScriptPubkey" }
val allowOpReturn = Features.canUseFeature(commitment.params.localParams.features, commitment.params.remoteParams.features, Feature.SimpleClose)
require(isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit, allowOpReturn)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey, allowAnySegwit, allowOpReturn)) { "invalid remoteScriptPubkey" }
val dustLimit = commitment.params.localParams.dustLimit.max(commitment.params.remoteParams.dustLimit)
val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.params.localParams.isInitiator, dustLimit, closingFees.preferred, commitment.localCommit.spec)
val localClosingSig = Transactions.sign(closingTx, channelKeys.fundingKey(commitment.fundingTxIndex))
Expand Down Expand Up @@ -439,17 +441,7 @@ object Helpers {
* The various dust limits are detailed in https://github.com/lightningnetwork/lightning-rfc/blob/master/03-transactions.md#dust-limits
*/
fun checkClosingDustAmounts(closingTx: ClosingTx): Boolean {
return closingTx.tx.txOut.all { txOut ->
val publicKeyScript = Script.parse(txOut.publicKeyScript)
when {
Script.isPay2pkh(publicKeyScript) -> txOut.amount >= 546.sat
Script.isPay2sh(publicKeyScript) -> txOut.amount >= 540.sat
Script.isPay2wpkh(publicKeyScript) -> txOut.amount >= 294.sat
Script.isPay2wsh(publicKeyScript) -> txOut.amount >= 330.sat
Script.isNativeWitnessScript(publicKeyScript) -> txOut.amount >= 354.sat
else -> txOut.amount >= 546.sat
}
}
return closingTx.tx.txOut.all { txOut -> txOut.amount >= Transactions.dustLimit(txOut.publicKeyScript) }
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,13 @@ data class Normal(
}
is ChannelCommand.Close.MutualClose -> {
val allowAnySegwit = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.ShutdownAnySegwit)
val allowOpReturn = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.SimpleClose)
val localScriptPubkey = cmd.scriptPubKey ?: commitments.params.localParams.defaultFinalScriptPubKey
when {
localShutdown != null -> handleCommandError(cmd, ClosingAlreadyInProgress(channelId), channelUpdate)
commitments.changes.localHasUnsignedOutgoingHtlcs() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId), channelUpdate)
commitments.changes.localHasUnsignedOutgoingUpdateFee() -> handleCommandError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId), channelUpdate)
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate)
!Helpers.Closing.isValidFinalScriptPubkey(localScriptPubkey, allowAnySegwit, allowOpReturn) -> handleCommandError(cmd, InvalidFinalScript(channelId), channelUpdate)
else -> {
val shutdown = Shutdown(channelId, localScriptPubkey)
val newState = this@Normal.copy(localShutdown = shutdown, closingFeerates = cmd.feerates)
Expand Down Expand Up @@ -273,6 +274,7 @@ data class Normal(
}
is Shutdown -> {
val allowAnySegwit = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.ShutdownAnySegwit)
val allowOpReturn = Features.canUseFeature(commitments.params.localParams.features, commitments.params.remoteParams.features, Feature.SimpleClose)
// 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 @@ -288,7 +290,7 @@ data class Normal(
// there are pending signed changes => go to SHUTDOWN
// there are no changes => go to NEGOTIATING
when {
!Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit) -> handleLocalError(cmd, InvalidFinalScript(channelId))
!Helpers.Closing.isValidFinalScriptPubkey(cmd.message.scriptPubKey, allowAnySegwit, allowOpReturn) -> handleLocalError(cmd, InvalidFinalScript(channelId))
commitments.changes.remoteHasUnsignedOutgoingHtlcs() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingHtlcs(channelId))
commitments.changes.remoteHasUnsignedOutgoingUpdateFee() -> handleLocalError(cmd, CannotCloseWithUnsignedOutgoingUpdateFee(channelId))
commitments.changes.localHasUnsignedOutgoingHtlcs() -> {
Expand Down Expand Up @@ -400,7 +402,7 @@ data class Normal(
add(ChannelAction.Disconnect)
}
Pair(this@Normal.copy(spliceStatus = SpliceStatus.None), actions)
} else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true) } == false) {
} else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true, allowOpReturn = true) } == false) {
logger.warning { "cannot do splice: invalid splice-out script" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript)
val actions = buildList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ object Transactions {
Script.isPay2wpkh(script) -> 294.sat
Script.isPay2wsh(script) -> 330.sat
Script.isNativeWitnessScript(script) -> 354.sat
script[0] == OP_RETURN -> 0.sat // OP_RETURN is never dust
else -> 546.sat
}
}.getOrElse { 546.sat }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ class HelpersTestsCommon : LightningTestSuite() {
TxOut(540.sat, Script.pay2sh(Hex.decode("0000000000000000000000000000000000000000"))),
TxOut(294.sat, Script.pay2wpkh(randomKey().publicKey())),
TxOut(330.sat, Script.pay2wsh(Hex.decode("0000000000000000000000000000000000000000"))),
TxOut(354.sat, Script.pay2tr(randomKey().publicKey().xOnly()))
TxOut(354.sat, Script.pay2tr(randomKey().publicKey().xOnly())),
TxOut(0.sat, listOf(OP_RETURN, OP_PUSHDATA(Hex.decode("deadbeef")))),
)

fun toClosingTx(txOut: List<TxOut>): Transactions.TransactionWithInputInfo.ClosingTx {
Expand Down

0 comments on commit 3fa7de6

Please sign in to comment.