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 Aug 25, 2021
1 parent aa17728 commit 6734ec1
Show file tree
Hide file tree
Showing 20 changed files with 718 additions and 339 deletions.
235 changes: 144 additions & 91 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelTypes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import fr.acinq.lightning.channel.Helpers.watchConfirmedIfNeeded
import fr.acinq.lightning.channel.Helpers.watchSpentIfNeeded
import fr.acinq.lightning.transactions.Scripts
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
import fr.acinq.lightning.transactions.Transactions.weight2fee
import fr.acinq.lightning.utils.BitField
import fr.acinq.lightning.utils.UUID
import fr.acinq.lightning.wire.ClosingSigned
Expand Down Expand Up @@ -49,8 +50,17 @@ 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()

/*
Expand Down
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 @@ -403,14 +403,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 @@ -420,25 +420,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 @@ -449,11 +449,11 @@ 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))
val signedClosingTx = Transactions.addSigs(closingTx, commitments.localParams.channelKeys.fundingPubKey, commitments.remoteParams.fundingPubKey, closingSigned.signature, remoteClosingSig)
return 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))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,8 @@ data class Normal(
channelUpdate,
remoteChannelUpdate,
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

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

Expand Down Expand Up @@ -772,7 +774,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 @@ -698,7 +698,8 @@ data class Normal(
channelUpdate,
remoteChannelUpdate,
localShutdown,
remoteShutdown
remoteShutdown,
null
)
}

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

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

Expand Down
16 changes: 16 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.Output
import fr.acinq.lightning.channel.ChannelOrigin
import fr.acinq.lightning.channel.ChannelVersion
import fr.acinq.lightning.serialization.v2.SatoshiKSerializer
import fr.acinq.lightning.utils.BitField
import fr.acinq.lightning.utils.toByteVector
import kotlinx.serialization.Contextual
Expand Down Expand Up @@ -173,6 +174,21 @@ sealed class ShutdownTlv : Tlv {

@Serializable
sealed class ClosingSignedTlv : Tlv {
@Serializable
data class FeeRange(@Serializable(with = SatoshiKSerializer::class) val min: Satoshi, @Serializable(with = SatoshiKSerializer::class) 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 @@ -1044,7 +1044,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 @@ -173,32 +173,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 6734ec1

Please sign in to comment.