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

More flexible mutual close fees #261

Merged
merged 1 commit 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
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
t-bast marked this conversation as resolved.
Show resolved Hide resolved
)
}

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