Skip to content

Commit

Permalink
More flexible mutual close fees
Browse files Browse the repository at this point in the history
Add support for https://github.com/lightningnetwork/lightning-rfc#847

With legacy nodes, we will keep the existing behavior (slowly converge on
a fee). But for nodes that support closing fee_ranges, mutual close will
take much less round-trips.
  • Loading branch information
t-bast committed Dec 9, 2021
1 parent 7554588 commit 72bbdef
Show file tree
Hide file tree
Showing 21 changed files with 748 additions and 353 deletions.
235 changes: 144 additions & 91 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package fr.acinq.lightning.channel

import fr.acinq.bitcoin.ByteVector
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Satoshi
import fr.acinq.lightning.CltvExpiry
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.transactions.Transactions.weight2fee
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.wire.FailureMessage
import fr.acinq.lightning.wire.OnionRoutingPacket
Expand All @@ -29,6 +31,16 @@ data class CMD_FAIL_HTLC(override val id: Long, val reason: Reason, val commit:
object CMD_SIGN : Command()
data class CMD_UPDATE_FEE(val feerate: FeeratePerKw, val commit: Boolean = false) : Command()

data class ClosingFees(val preferred: Satoshi, val min: Satoshi, val max: Satoshi) {
constructor(preferred: Satoshi) : this(preferred, preferred, preferred)
}

data class ClosingFeerates(val preferred: FeeratePerKw, val min: FeeratePerKw, val max: FeeratePerKw) {
constructor(preferred: FeeratePerKw) : this(preferred, preferred / 2, preferred * 2)

fun computeFees(closingTxWeight: Int): ClosingFees = ClosingFees(weight2fee(preferred, closingTxWeight), weight2fee(min, closingTxWeight), weight2fee(max, closingTxWeight))
}

sealed class CloseCommand : Command()
data class CMD_CLOSE(val scriptPubKey: ByteVector?) : CloseCommand()
data class CMD_CLOSE(val scriptPubKey: ByteVector?, val feerates: ClosingFeerates?) : CloseCommand()
object CMD_FORCECLOSE : CloseCommand()
28 changes: 14 additions & 14 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -435,14 +435,14 @@ object Helpers {
}.getOrElse { null }
}

fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: FeeratePerKw): Satoshi {
fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteArray, remoteScriptPubkey: ByteArray, requestedFeerate: ClosingFeerates): ClosingFees {
// this is just to estimate the weight which depends on the size of the pubkey scripts
val dummyClosingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, Satoshi(0), Satoshi(0), commitments.localCommit.spec)
val closingWeight = Transaction.weight(Transactions.addSigs(dummyClosingTx, dummyPublicKey, commitments.remoteParams.fundingPubKey, Transactions.PlaceHolderSig, Transactions.PlaceHolderSig).tx)
return Transactions.weight2fee(requestedFeerate, closingWeight)
return requestedFeerate.computeFees(closingWeight)
}

fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, requestedFeerate: FeeratePerKw): Satoshi =
fun firstClosingFee(commitments: Commitments, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, requestedFeerate: ClosingFeerates): ClosingFees =
firstClosingFee(commitments, localScriptPubkey.toByteArray(), remoteScriptPubkey.toByteArray(), requestedFeerate)

fun nextClosingFee(localClosingFee: Satoshi, remoteClosingFee: Satoshi): Satoshi = ((localClosingFee + remoteClosingFee) / 4) * 2
Expand All @@ -452,25 +452,25 @@ object Helpers {
commitments: Commitments,
localScriptPubkey: ByteArray,
remoteScriptPubkey: ByteArray,
requestedFeerate: FeeratePerKw
): Pair<Transactions.TransactionWithInputInfo.ClosingTx, ClosingSigned> {
val closingFee = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, requestedFeerate)
return makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFee)
requestedFeerate: ClosingFeerates
): Pair<ClosingTx, ClosingSigned> {
val closingFees = firstClosingFee(commitments, localScriptPubkey, remoteScriptPubkey, requestedFeerate)
return makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, closingFees)
}

fun makeClosingTx(
keyManager: KeyManager,
commitments: Commitments,
localScriptPubkey: ByteArray,
remoteScriptPubkey: ByteArray,
closingFee: Satoshi
): Pair<Transactions.TransactionWithInputInfo.ClosingTx, ClosingSigned> {
closingFees: ClosingFees
): Pair<ClosingTx, ClosingSigned> {
require(isValidFinalScriptPubkey(localScriptPubkey)) { "invalid localScriptPubkey" }
require(isValidFinalScriptPubkey(remoteScriptPubkey)) { "invalid remoteScriptPubkey" }
val dustLimit = commitments.localParams.dustLimit.max(commitments.remoteParams.dustLimit)
val closingTx = Transactions.makeClosingTx(commitments.commitInput, localScriptPubkey, remoteScriptPubkey, commitments.localParams.isFunder, dustLimit, closingFee, commitments.localCommit.spec)
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)
val closingSigned = ClosingSigned(commitments.channelId, closingFee, localClosingSig)
val closingSigned = ClosingSigned(commitments.channelId, closingFees.preferred, localClosingSig, TlvStream(listOf(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))))
return Pair(closingTx, closingSigned)
}

Expand All @@ -481,12 +481,12 @@ object Helpers {
remoteScriptPubkey: ByteArray,
remoteClosingFee: Satoshi,
remoteClosingSig: ByteVector64
): Either<ChannelException, ClosingTx> {
val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, remoteClosingFee)
): Either<ChannelException, Pair<ClosingTx, ClosingSigned>> {
val (closingTx, closingSigned) = makeClosingTx(keyManager, commitments, localScriptPubkey, remoteScriptPubkey, ClosingFees(remoteClosingFee))
return if (checkClosingDustAmounts(closingTx)) {
val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.channelKeys.fundingPubKey, commitments.remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
when (Transactions.checkSpendable(signedClosingTx)) {
is Try.Success -> Either.Right(signedClosingTx)
is Try.Success -> Either.Right(Pair(signedClosingTx, closingSigned))
is Try.Failure -> Either.Left(InvalidCloseSignature(commitments.channelId, signedClosingTx.tx))
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,8 @@ data class Normal(
channelUpdate,
remoteChannelUpdate,
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

Expand Down Expand Up @@ -759,7 +760,8 @@ data class ShuttingDown(
currentOnChainFeerates.export(),
commitments.export(nodeParams),
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

Expand Down Expand Up @@ -798,7 +800,8 @@ data class Negotiating(
localShutdown,
remoteShutdown,
closingTxProposed.map { x -> x.map { it.export() } },
bestUnpublishedClosingTx
bestUnpublishedClosingTx,
null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,8 @@ data class Normal(
channelUpdate,
remoteChannelUpdate,
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

Expand Down Expand Up @@ -752,7 +753,8 @@ data class ShuttingDown(
currentOnChainFeerates.export(),
commitments.export(nodeParams),
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

Expand Down Expand Up @@ -791,7 +793,8 @@ data class Negotiating(
localShutdown,
remoteShutdown,
closingTxProposed.map { x -> x.map { it.export() } },
bestUnpublishedClosingTx
bestUnpublishedClosingTx,
null
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ data class ChannelFeatures(@Serializable(with = ByteVectorKSerializer::class) va
fun export() = fr.acinq.lightning.channel.ChannelFeatures(Features(bin.toByteArray()).activated.keys)
}

@Serializable
data class ClosingFeerates(val preferred: FeeratePerKw, val min: FeeratePerKw, val max: FeeratePerKw) {
constructor(from: fr.acinq.lightning.channel.ClosingFeerates) : this(from.preferred, from.min, from.max)

fun export() = fr.acinq.lightning.channel.ClosingFeerates(preferred, min, max)
}

@Serializable
data class ClosingTxProposed(val unsignedTx: Transactions.TransactionWithInputInfo.ClosingTx, val localClosingSigned: ClosingSigned) {
constructor(from: fr.acinq.lightning.channel.ClosingTxProposed) : this(from.unsignedTx, from.localClosingSigned)
Expand Down Expand Up @@ -701,7 +708,8 @@ data class Normal(
val channelUpdate: ChannelUpdate,
val remoteChannelUpdate: ChannelUpdate?,
val localShutdown: Shutdown?,
val remoteShutdown: Shutdown?
val remoteShutdown: Shutdown?,
val closingFeerates: ClosingFeerates?
) : ChannelStateWithCommitments() {
constructor(from: fr.acinq.lightning.channel.Normal) : this(
StaticParams(from.staticParams),
Expand All @@ -714,7 +722,8 @@ data class Normal(
from.channelUpdate,
from.remoteChannelUpdate,
from.localShutdown,
from.remoteShutdown
from.remoteShutdown,
from.closingFeerates?.let { ClosingFeerates(it) }
)

override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Normal(
Expand All @@ -728,7 +737,8 @@ data class Normal(
channelUpdate,
remoteChannelUpdate,
localShutdown,
remoteShutdown
remoteShutdown,
closingFeerates?.export()
)
}

Expand All @@ -739,15 +749,17 @@ data class ShuttingDown(
override val currentOnChainFeerates: OnChainFeerates,
override val commitments: Commitments,
val localShutdown: Shutdown,
val remoteShutdown: Shutdown
val remoteShutdown: Shutdown,
val closingFeerates: ClosingFeerates?
) : ChannelStateWithCommitments() {
constructor(from: fr.acinq.lightning.channel.ShuttingDown) : this(
StaticParams(from.staticParams),
from.currentTip,
OnChainFeerates(from.currentOnChainFeerates),
Commitments(from.commitments),
from.localShutdown,
from.remoteShutdown
from.remoteShutdown,
from.closingFeerates?.let { ClosingFeerates(it) }
)

override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.ShuttingDown(
Expand All @@ -756,7 +768,8 @@ data class ShuttingDown(
currentOnChainFeerates.export(),
commitments.export(nodeParams),
localShutdown,
remoteShutdown
remoteShutdown,
closingFeerates?.export()
)
}

Expand All @@ -769,7 +782,8 @@ data class Negotiating(
val localShutdown: Shutdown,
val remoteShutdown: Shutdown,
val closingTxProposed: List<List<ClosingTxProposed>>,
val bestUnpublishedClosingTx: Transactions.TransactionWithInputInfo.ClosingTx?
val bestUnpublishedClosingTx: Transactions.TransactionWithInputInfo.ClosingTx?,
val closingFeerates: ClosingFeerates?
) : ChannelStateWithCommitments() {
init {
require(closingTxProposed.isNotEmpty()) { "there must always be a list for the current negotiation" }
Expand All @@ -784,7 +798,8 @@ data class Negotiating(
from.localShutdown,
from.remoteShutdown,
from.closingTxProposed.map { x -> x.map { ClosingTxProposed(it) } },
from.bestUnpublishedClosingTx
from.bestUnpublishedClosingTx,
from.closingFeerates?.let { ClosingFeerates(it) }
)

override fun export(nodeParams: NodeParams) = fr.acinq.lightning.channel.Negotiating(
Expand All @@ -795,7 +810,8 @@ data class Negotiating(
localShutdown,
remoteShutdown,
closingTxProposed.map { x -> x.map { it.export() } },
bestUnpublishedClosingTx
bestUnpublishedClosingTx,
closingFeerates?.export()
)
}

Expand Down
15 changes: 15 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,21 @@ sealed class ShutdownTlv : Tlv {

@Serializable
sealed class ClosingSignedTlv : Tlv {
@Serializable
data class FeeRange(@Contextual val min: Satoshi, @Contextual val max: Satoshi) : ClosingSignedTlv() {
override val tag: Long get() = FeeRange.tag

override fun write(out: Output) {
LightningCodecs.writeU64(min.toLong(), out)
LightningCodecs.writeU64(max.toLong(), out)
}

companion object : TlvValueReader<FeeRange> {
const val tag: Long = 1
override fun read(input: Input): FeeRange = FeeRange(Satoshi(LightningCodecs.u64(input)), Satoshi(LightningCodecs.u64(input)))
}
}

@Serializable
data class ChannelData(@Contextual val ecb: EncryptedChannelData) : ClosingSignedTlv() {
override val tag: Long get() = ChannelData.tag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,10 @@ data class ClosingSigned(
const val type: Long = 39

@Suppress("UNCHECKED_CAST")
val readers = mapOf(ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader<ClosingSignedTlv>)
val readers = mapOf(
ClosingSignedTlv.FeeRange.tag to ClosingSignedTlv.FeeRange.Companion as TlvValueReader<ClosingSignedTlv>,
ClosingSignedTlv.ChannelData.tag to ClosingSignedTlv.ChannelData.Companion as TlvValueReader<ClosingSignedTlv>
)

override fun read(input: Input): ClosingSigned {
return ClosingSigned(
Expand Down
54 changes: 30 additions & 24 deletions src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,32 +180,38 @@ object TestsHelper {
return Pair(alice as Normal, bob as Normal)
}

fun mutualClose(
alice: Normal,
bob: Normal,
tweakFees: Boolean = false,
scriptPubKey: ByteVector? = null
): Triple<Negotiating, Negotiating, ClosingSigned> {
val alice1 = alice.updateFeerate(if (tweakFees) FeeratePerKw(4_319.sat) else FeeratePerKw(10_000.sat))
val bob1 = bob.updateFeerate(if (tweakFees) FeeratePerKw(4_319.sat) else FeeratePerKw(10_000.sat))

// Bob is fundee and initiates the closing
val (bob2, actions) = bob1.process(ChannelEvent.ExecuteCommand(CMD_CLOSE(scriptPubKey)))
assertTrue(bob2 is Normal)
val shutdown = actions.findOutgoingMessage<Shutdown>()

// Alice is funder, she will sign the first closing tx
val (alice2, actions1) = alice1.process(ChannelEvent.MessageReceived(shutdown))
fun mutualCloseAlice(alice: Normal, bob: Normal, scriptPubKey: ByteVector? = null, feerates: ClosingFeerates? = null): Triple<Negotiating, Negotiating, ClosingSigned> {
val (alice1, actionsAlice1) = alice.process(ChannelEvent.ExecuteCommand(CMD_CLOSE(scriptPubKey, feerates)))
assertTrue(alice1 is Normal)
val shutdownAlice = actionsAlice1.findOutgoingMessage<Shutdown>()
assertNull(actionsAlice1.findOutgoingMessageOpt<ClosingSigned>())

val (bob1, actionsBob1) = bob.process(ChannelEvent.MessageReceived(shutdownAlice))
assertTrue(bob1 is Negotiating)
val shutdownBob = actionsBob1.findOutgoingMessage<Shutdown>()
assertNull(actionsBob1.findOutgoingMessageOpt<ClosingSigned>())

val (alice2, actionsAlice2) = alice1.process(ChannelEvent.MessageReceived(shutdownBob))
assertTrue(alice2 is Negotiating)
val shutdown1 = actions1.findOutgoingMessage<Shutdown>()
val closingSigned = actions1.findOutgoingMessage<ClosingSigned>()

val alice3 = alice2.updateFeerate(if (tweakFees) FeeratePerKw(4_316.sat) else FeeratePerKw(5_000.sat))
val bob3 = bob2.updateFeerate(if (tweakFees) FeeratePerKw(4_316.sat) else FeeratePerKw(5_000.sat))
val closingSignedAlice = actionsAlice2.findOutgoingMessage<ClosingSigned>()
return Triple(alice2, bob1, closingSignedAlice)
}

val (bob4, _) = bob3.process(ChannelEvent.MessageReceived(shutdown1))
assertTrue(bob4 is Negotiating)
return Triple(alice3, bob4, closingSigned)
fun mutualCloseBob(alice: Normal, bob: Normal, scriptPubKey: ByteVector? = null, feerates: ClosingFeerates? = null): Triple<Negotiating, Negotiating, ClosingSigned> {
val (bob1, actionsBob1) = bob.process(ChannelEvent.ExecuteCommand(CMD_CLOSE(scriptPubKey, feerates)))
assertTrue(bob1 is Normal)
val shutdownBob = actionsBob1.findOutgoingMessage<Shutdown>()
assertNull(actionsBob1.findOutgoingMessageOpt<ClosingSigned>())

val (alice1, actionsAlice1) = alice.process(ChannelEvent.MessageReceived(shutdownBob))
assertTrue(alice1 is Negotiating)
val shutdownAlice = actionsAlice1.findOutgoingMessage<Shutdown>()
val closingSignedAlice = actionsAlice1.findOutgoingMessage<ClosingSigned>()

val (bob2, actionsBob2) = bob1.process(ChannelEvent.MessageReceived(shutdownAlice))
assertTrue(bob2 is Negotiating)
assertNull(actionsBob2.findOutgoingMessageOpt<ClosingSigned>())
return Triple(alice1, bob2, closingSignedAlice)
}

fun localClose(s: ChannelState): Pair<Closing, LocalCommitPublished> {
Expand Down
Loading

0 comments on commit 72bbdef

Please sign in to comment.