commonMain.fr.acinq.lightning.channel.states.Closing.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lightning-kmp-jvm Show documentation
Show all versions of lightning-kmp-jvm Show documentation
A Kotlin Multiplatform implementation of the Lightning Network
package fr.acinq.lightning.channel.states
import fr.acinq.bitcoin.Satoshi
import fr.acinq.bitcoin.Transaction
import fr.acinq.bitcoin.updated
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.blockchain.*
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
import fr.acinq.lightning.channel.*
import fr.acinq.lightning.channel.Helpers.Closing.claimCurrentLocalCommitTxOutputs
import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitTxOutputs
import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedHtlcTxOutputs
import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxHtlcOutputs
import fr.acinq.lightning.channel.Helpers.Closing.extractPreimages
import fr.acinq.lightning.channel.Helpers.Closing.onChainOutgoingHtlcs
import fr.acinq.lightning.channel.Helpers.Closing.overriddenOutgoingHtlcs
import fr.acinq.lightning.channel.Helpers.Closing.timedOutHtlcs
import fr.acinq.lightning.transactions.Transactions
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx
import fr.acinq.lightning.utils.getValue
import fr.acinq.lightning.wire.ChannelReestablish
import fr.acinq.lightning.wire.Error
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) {
fun computeFees(closingTxWeight: Int): ClosingFees = ClosingFees(Transactions.weight2fee(preferred, closingTxWeight), Transactions.weight2fee(min, closingTxWeight), Transactions.weight2fee(max, closingTxWeight))
companion object {
operator fun invoke(preferred: FeeratePerKw): ClosingFeerates = ClosingFeerates(preferred, preferred / 2, preferred * 2)
}
}
sealed class ClosingType
data class MutualClose(val tx: ClosingTx) : ClosingType()
data class LocalClose(val localCommit: LocalCommit, val localCommitPublished: LocalCommitPublished) : ClosingType()
sealed class RemoteClose : ClosingType() {
abstract val remoteCommit: RemoteCommit
abstract val remoteCommitPublished: RemoteCommitPublished
}
data class CurrentRemoteClose(override val remoteCommit: RemoteCommit, override val remoteCommitPublished: RemoteCommitPublished) : RemoteClose()
data class NextRemoteClose(override val remoteCommit: RemoteCommit, override val remoteCommitPublished: RemoteCommitPublished) : RemoteClose()
data class RecoveryClose(val remoteCommitPublished: RemoteCommitPublished) : ClosingType()
data class RevokedClose(val revokedCommitPublished: RevokedCommitPublished) : ClosingType()
data class Closing(
override val commitments: Commitments,
val waitingSinceBlock: Long, // how many blocks since we initiated the closing
val mutualCloseProposed: List = emptyList(), // all exchanged closing sigs are flattened, we use this only to keep track of what publishable tx they have
val mutualClosePublished: List = emptyList(),
val localCommitPublished: LocalCommitPublished? = null,
val remoteCommitPublished: RemoteCommitPublished? = null,
val nextRemoteCommitPublished: RemoteCommitPublished? = null,
val futureRemoteCommitPublished: RemoteCommitPublished? = null,
val revokedCommitPublished: List = emptyList()
) : ChannelStateWithCommitments() {
private val spendingTxs: List by lazy {
mutualClosePublished.map { it.tx } + revokedCommitPublished.map { it.commitTx } + listOfNotNull(
localCommitPublished?.commitTx,
remoteCommitPublished?.commitTx,
nextRemoteCommitPublished?.commitTx,
futureRemoteCommitPublished?.commitTx
)
}
override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input)
override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> {
return when (cmd) {
is ChannelCommand.WatchReceived -> {
val watch = cmd.watch
when {
watch is WatchEventConfirmed && watch.event is BITCOIN_FUNDING_DEPTHOK -> {
when (val res = acceptFundingTxConfirmed(watch)) {
is Either.Right -> {
val (commitments1, commitment, actions) = res.value
if (commitments.latest.fundingTxIndex == commitment.fundingTxIndex && commitments.latest.fundingTxId != commitment.fundingTxId) {
// This is a corner case where:
// - the funding tx was RBF-ed
// - *and* we went to CLOSING before any funding tx got confirmed (probably due to a local or remote error)
// - *and* an older version of the funding tx confirmed and reached min depth (it won't be re-orged out)
//
// This means that:
// - the whole current commitment tree has been double-spent and can safely be forgotten
// - from now on, we only need to keep track of the commitment associated to the funding tx that got confirmed
//
// Force-closing is our only option here, if we are in this state the channel was closing and it is too late
// to negotiate a mutual close.
// The best funding tx candidate has been confirmed, we can forget alternative commitments.
logger.info { "channel was confirmed at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with a previous funding txid=${watch.tx.txid}" }
val commitments2 = commitments1.copy(
active = listOf(commitment),
inactive = emptyList()
)
val (nextState, actions1) = [email protected](commitments = commitments2).run { spendLocalCurrent() }
Pair(nextState, actions + actions1)
} else {
// We're still on the same splice history, nothing to do
val nextState = [email protected](commitments = commitments1)
Pair(nextState, actions + listOf(ChannelAction.Storage.StoreState(nextState)))
}
}
is Either.Left -> Pair(this@Closing, listOf())
}
}
watch is WatchEventSpent && watch.event is BITCOIN_FUNDING_SPENT -> when {
commitments.all.any { it.fundingTxId == watch.tx.txid } -> {
// if the spending tx is itself a funding tx, this is a splice and there is nothing to do
Pair(this@Closing, listOf())
}
mutualClosePublished.any { it.tx.txid == watch.tx.txid } -> {
// we already know about this tx, probably because we have published it ourselves after successful negotiation
Pair(this@Closing, listOf())
}
mutualCloseProposed.any { it.tx.txid == watch.tx.txid } -> {
// at any time they can publish a closing tx with any sig we sent them
val closingTx = mutualCloseProposed.first { it.tx.txid == watch.tx.txid }.copy(tx = watch.tx)
val nextState = [email protected](mutualClosePublished = mutualClosePublished + listOf(closingTx))
val actions = listOf(ChannelAction.Storage.StoreState(nextState), ChannelAction.Blockchain.PublishTx(closingTx))
Pair(nextState, actions)
}
localCommitPublished?.commitTx == watch.tx || remoteCommitPublished?.commitTx == watch.tx || nextRemoteCommitPublished?.commitTx == watch.tx || futureRemoteCommitPublished?.commitTx == watch.tx -> {
// this is because WatchSpent watches never expire and we are notified multiple times
Pair(this@Closing, listOf())
}
watch.tx.txid == commitments.latest.remoteCommit.txid -> {
// counterparty may attempt to spend its last commit tx at any time
handleRemoteSpentCurrent(watch.tx, commitments.latest)
}
watch.tx.txid == commitments.latest.nextRemoteCommit?.commit?.txid -> {
// counterparty may attempt to spend its next commit tx at any time
handleRemoteSpentNext(watch.tx, commitments.latest)
}
watch.tx.txIn.map { it.outPoint }.contains(commitments.latest.commitInput.outPoint) -> {
// counterparty may attempt to spend a revoked commit tx at any time
handleRemoteSpentOther(watch.tx)
}
else -> when (val commitment = commitments.resolveCommitment(watch.tx)) {
is Commitment -> {
logger.warning { "a commit tx for an older commitment has been published fundingTxId=${commitment.fundingTxId} fundingTxIndex=${commitment.fundingTxIndex}" }
Pair(this@Closing, listOf(ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_ALTERNATIVE_COMMIT_TX_CONFIRMED))))
}
else -> {
logger.warning { "unrecognized tx=${watch.tx.txid}" }
Pair(this@Closing, listOf())
}
}
}
watch is WatchEventConfirmed && watch.event is BITCOIN_ALTERNATIVE_COMMIT_TX_CONFIRMED -> when (val commitment = commitments.resolveCommitment(watch.tx)) {
is Commitment -> {
logger.warning { "a commit tx for fundingTxIndex=${commitment.fundingTxIndex} fundingTxId=${commitment.fundingTxId} has been confirmed" }
val commitments1 = commitments.copy(
active = listOf(commitment),
inactive = emptyList()
)
val newState = [email protected](commitments = commitments1)
// This commitment may be revoked: we need to verify that its index matches our latest known index before overwriting our previous commitments.
when {
watch.tx.txid == commitments1.latest.localCommit.publishableTxs.commitTx.tx.txid -> {
// our local commit has been published from the outside, it's unexpected but let's deal with it anyway
newState.run { spendLocalCurrent() }
}
watch.tx.txid == commitments1.latest.remoteCommit.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex -> {
// counterparty may attempt to spend its last commit tx at any time
newState.run { handleRemoteSpentCurrent(watch.tx, commitments1.latest) }
}
watch.tx.txid == commitments1.latest.nextRemoteCommit?.commit?.txid && commitments1.remoteCommitIndex == commitments.remoteCommitIndex && commitments.remoteNextCommitInfo.isLeft -> {
// counterparty may attempt to spend its next commit tx at any time
newState.run { handleRemoteSpentNext(watch.tx, commitments1.latest) }
}
else -> {
// counterparty may attempt to spend a revoked commit tx at any time
newState.run { handleRemoteSpentOther(watch.tx) }
}
}
}
else -> {
logger.warning { "unrecognized alternative commit tx=${watch.tx.txid}" }
Pair(this@Closing, listOf())
}
}
watch is WatchEventSpent && watch.event is BITCOIN_OUTPUT_SPENT -> {
// when a remote or local commitment tx containing outgoing htlcs is published on the network,
// we watch it in order to extract payment preimage if funds are pulled by the counterparty
// we can then use these preimages to fulfill payments
logger.info { "processing BITCOIN_OUTPUT_SPENT with txid=${watch.tx.txid} tx=${watch.tx}" }
val htlcSettledActions = mutableListOf()
extractPreimages(commitments.latest.localCommit, watch.tx).forEach { (htlc, preimage) ->
when (val paymentId = commitments.payments[htlc.id]) {
null -> {
// if we don't have a reference to the payment, it means that we already have forwarded the fulfill so that's not a big deal.
// this can happen if they send a signature containing the fulfill, then fail the channel before we have time to sign it
logger.info { "cannot fulfill htlc #${htlc.id} paymentHash=${htlc.paymentHash} (payment not found)" }
}
else -> {
logger.info { "fulfilling htlc #${htlc.id} paymentHash=${htlc.paymentHash} paymentId=$paymentId" }
htlcSettledActions += ChannelAction.ProcessCmdRes.AddSettledFulfill(paymentId, htlc, ChannelAction.HtlcResult.Fulfill.OnChainFulfill(preimage))
}
}
}
val revokedCommitPublishActions = mutableListOf()
val revokedCommitPublished1 = revokedCommitPublished.map { rev ->
val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(channelKeys(), commitments.params, rev, watch.tx, currentOnChainFeerates())
penaltyTxs.forEach {
revokedCommitPublishActions += ChannelAction.Blockchain.PublishTx(it)
revokedCommitPublishActions += ChannelAction.Blockchain.SendWatch(WatchSpent(channelId, watch.tx, it.input.outPoint.index.toInt(), BITCOIN_OUTPUT_SPENT))
}
newRevokedCommitPublished
}
val nextState = copy(revokedCommitPublished = revokedCommitPublished1)
val actions = buildList {
add(ChannelAction.Storage.StoreState(nextState))
// one of the outputs of the local/remote/revoked commit was spent
// we just put a watch to be notified when it is confirmed
add(ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(watch.tx))))
addAll(revokedCommitPublishActions)
addAll(htlcSettledActions)
}
Pair(nextState, actions)
}
watch is WatchEventConfirmed && watch.event is BITCOIN_TX_CONFIRMED -> {
logger.info { "txid=${watch.tx.txid} has reached mindepth, updating closing state" }
// first we check if this tx belongs to one of the current local/remote commits, update it and update the channel data
val closing1 = [email protected](
localCommitPublished = localCommitPublished?.update(watch.tx),
remoteCommitPublished = remoteCommitPublished?.update(watch.tx),
nextRemoteCommitPublished = nextRemoteCommitPublished?.update(watch.tx),
futureRemoteCommitPublished = futureRemoteCommitPublished?.update(watch.tx),
revokedCommitPublished = revokedCommitPublished.map { it.update(watch.tx) }
)
// we may need to fail some htlcs in case a commitment tx was published and they have reached the timeout threshold
val htlcSettledActions = mutableListOf()
val timedOutHtlcs = when (val closingType = closing1.closingTypeAlreadyKnown()) {
is LocalClose -> timedOutHtlcs(closingType.localCommit, closingType.localCommitPublished, commitments.params.localParams.dustLimit, watch.tx)
is RemoteClose -> timedOutHtlcs(closingType.remoteCommit, closingType.remoteCommitPublished, commitments.params.remoteParams.dustLimit, watch.tx)
else -> setOf() // we lose htlc outputs in option_data_loss_protect scenarios (future remote commit)
}
timedOutHtlcs.forEach { add ->
when (val paymentId = commitments.payments[add.id]) {
null -> {
// same as for fulfilling the htlc (no big deal)
logger.info { "cannot fail timedout htlc #${add.id} paymentHash=${add.paymentHash} (payment not found)" }
}
else -> {
logger.info { "failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId: htlc timed out" }
htlcSettledActions += ChannelAction.ProcessCmdRes.AddSettledFail(paymentId, add, ChannelAction.HtlcResult.Fail.OnChainFail(HtlcsTimedOutDownstream(channelId, setOf(add))))
}
}
}
// we also need to fail outgoing htlcs that we know will never reach the blockchain
overriddenOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit?.commit, closing1.revokedCommitPublished, watch.tx).forEach { add ->
when (val paymentId = commitments.payments[add.id]) {
null -> {
// same as for fulfilling the htlc (no big deal)
logger.info { "cannot fail overridden htlc #${add.id} paymentHash=${add.paymentHash} (payment not found)" }
}
else -> {
logger.info { "failing htlc #${add.id} paymentHash=${add.paymentHash} paymentId=$paymentId: overridden by local commit" }
htlcSettledActions += ChannelAction.ProcessCmdRes.AddSettledFail(paymentId, add, ChannelAction.HtlcResult.Fail.OnChainFail(HtlcOverriddenByLocalCommit(channelId, add)))
}
}
}
// for our outgoing payments, let's log something if we know that they will settle on chain
onChainOutgoingHtlcs(commitments.latest.localCommit, commitments.latest.remoteCommit, commitments.latest.nextRemoteCommit?.commit, watch.tx).forEach { add ->
commitments.payments[add.id]?.let { paymentId -> logger.info { "paymentId=$paymentId will settle on-chain (htlc #${add.id} sending ${add.amountMsat})" } }
}
val (nextState, closedActions) = when (val closingType = closing1.isClosed(watch.tx)) {
null -> Pair(closing1, listOf())
else -> {
logger.info { "channel is now closed" }
if (closingType !is MutualClose) {
logger.debug { "last known remoteChannelData=${commitments.remoteChannelData}" }
}
Pair(Closed(closing1), listOf(setClosingStatus(closingType)))
}
}
val actions = buildList {
add(ChannelAction.Storage.StoreState(nextState))
addAll(htlcSettledActions)
addAll(closedActions)
}
Pair(nextState, actions)
}
else -> unhandled(cmd)
}
}
is ChannelCommand.Closing.GetHtlcInfosResponse -> {
val index = revokedCommitPublished.indexOfFirst { it.commitTx.txid == cmd.revokedCommitTxId }
if (index >= 0) {
val revokedCommitPublished1 = claimRevokedRemoteCommitTxHtlcOutputs(channelKeys(), commitments.params, revokedCommitPublished[index], currentOnChainFeerates(), cmd.htlcInfos)
val nextState = copy(revokedCommitPublished = revokedCommitPublished.updated(index, revokedCommitPublished1))
val actions = buildList {
add(ChannelAction.Storage.StoreState(nextState))
addAll(revokedCommitPublished1.run { doPublish(channelId, staticParams.nodeParams.minDepthBlocks.toLong()) })
}
Pair(nextState, actions)
} else {
logger.warning { "cannot find revoked commit with txid=${cmd.revokedCommitTxId}" }
Pair(this@Closing, listOf())
}
}
is ChannelCommand.MessageReceived -> when (cmd.message) {
is ChannelReestablish -> {
// they haven't detected that we were closing and are trying to reestablish a connection
// we give them one of the published txs as a hint
// note spendingTx != Nil (that's a requirement of DATA_CLOSING)
val exc = FundingTxSpent(channelId, spendingTxs.first().txid)
val error = Error(channelId, exc.message)
Pair(this@Closing, listOf(ChannelAction.Message.Send(error)))
}
is Error -> {
logger.error { "peer sent error: ascii=${cmd.message.toAscii()} bin=${cmd.message.data.toHex()}" }
// nothing to do, there is already a spending tx published
Pair(this@Closing, listOf())
}
else -> unhandled(cmd)
}
is ChannelCommand.Close -> handleCommandError(cmd, ClosingAlreadyInProgress(channelId))
is ChannelCommand.Htlc.Add -> {
logger.info { "rejecting htlc request in state=${this::class}" }
// we don't provide a channel_update: this will be a permanent channel failure
handleCommandError(cmd, ChannelUnavailable(channelId))
}
is ChannelCommand.Htlc.Settlement.Fulfill -> when (val result = commitments.sendFulfill(cmd)) {
is Either.Right -> {
logger.info { "got valid payment preimage, recalculating transactions to redeem the corresponding htlc on-chain" }
val commitments1 = result.value.first
val localCommitPublished1 = localCommitPublished?.let {
claimCurrentLocalCommitTxOutputs(channelKeys(), commitments1.latest, it.commitTx, currentOnChainFeerates())
}
val remoteCommitPublished1 = remoteCommitPublished?.let {
claimRemoteCommitTxOutputs(channelKeys(), commitments1.latest, commitments1.latest.remoteCommit, it.commitTx, currentOnChainFeerates())
}
val nextRemoteCommitPublished1 = nextRemoteCommitPublished?.let {
val remoteCommit = commitments1.latest.nextRemoteCommit?.commit ?: error("next remote commit must be defined")
claimRemoteCommitTxOutputs(channelKeys(), commitments1.latest, remoteCommit, it.commitTx, currentOnChainFeerates())
}
val republishList = buildList {
val minDepth = staticParams.nodeParams.minDepthBlocks.toLong()
localCommitPublished1?.run { addAll(doPublish(channelId, minDepth)) }
remoteCommitPublished1?.run { addAll(doPublish(channelId, minDepth)) }
nextRemoteCommitPublished1?.run { addAll(doPublish(channelId, minDepth)) }
}
val nextState = copy(
commitments = commitments1,
localCommitPublished = localCommitPublished1,
remoteCommitPublished = remoteCommitPublished1,
nextRemoteCommitPublished = nextRemoteCommitPublished1
)
val actions = buildList {
add(ChannelAction.Storage.StoreState(nextState))
addAll(republishList)
}
Pair(nextState, actions)
}
is Either.Left -> handleCommandError(cmd, result.value)
}
is ChannelCommand.Htlc.Settlement -> unhandled(cmd)
is ChannelCommand.Commitment.CheckHtlcTimeout -> checkHtlcTimeout()
is ChannelCommand.Commitment -> unhandled(cmd)
is ChannelCommand.Init -> unhandled(cmd)
is ChannelCommand.Funding -> unhandled(cmd)
is ChannelCommand.Connected -> unhandled(cmd)
is ChannelCommand.Disconnected -> unhandled(cmd)
}
}
/**
* Checks if a channel is closed (i.e. its closing tx has been confirmed)
*
* @param additionalConfirmedTx additional confirmed transaction; we need this for the mutual close scenario because we don't store the closing tx in the channel state
* @return the channel closing type, if applicable
*/
private fun isClosed(additionalConfirmedTx: Transaction?): ClosingType? {
return when {
additionalConfirmedTx?.let { tx -> mutualClosePublished.any { it.tx.txid == tx.txid } } ?: false -> {
val closingTx = mutualClosePublished.first { it.tx.txid == additionalConfirmedTx!!.txid }.copy(tx = additionalConfirmedTx!!)
MutualClose(closingTx)
}
localCommitPublished?.isDone() ?: false -> LocalClose(commitments.latest.localCommit, localCommitPublished!!)
remoteCommitPublished?.isDone() ?: false -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished!!)
nextRemoteCommitPublished?.isDone() ?: false -> NextRemoteClose(commitments.latest.nextRemoteCommit!!.commit, nextRemoteCommitPublished!!)
futureRemoteCommitPublished?.isDone() ?: false -> RecoveryClose(futureRemoteCommitPublished!!)
revokedCommitPublished.any { it.isDone() } -> RevokedClose(revokedCommitPublished.first { it.isDone() })
else -> null
}
}
fun closingTypeAlreadyKnown(): ClosingType? {
return when {
localCommitPublished?.isConfirmed() ?: false -> LocalClose(commitments.latest.localCommit, localCommitPublished!!)
remoteCommitPublished?.isConfirmed() ?: false -> CurrentRemoteClose(commitments.latest.remoteCommit, remoteCommitPublished!!)
nextRemoteCommitPublished?.isConfirmed() ?: false -> NextRemoteClose(commitments.latest.nextRemoteCommit!!.commit, nextRemoteCommitPublished!!)
futureRemoteCommitPublished?.isConfirmed() ?: false -> RecoveryClose(futureRemoteCommitPublished!!)
revokedCommitPublished.any { it.isConfirmed() } -> RevokedClose(revokedCommitPublished.first { it.isConfirmed() })
else -> null
}
}
companion object {
/**
* This method returns updates the status of the closing transaction that should be persisted in a database. It should be
* called when the channel has been actually closed, so that we know those transactions are confirmed. Note that we only
* keep track of the commit tx in the non-mutual-close case.
*/
private fun setClosingStatus(closingType: ClosingType): ChannelAction.Storage.SetLocked {
val txId = when (closingType) {
is MutualClose -> closingType.tx.tx.txid
is LocalClose -> closingType.localCommit.publishableTxs.commitTx.tx.txid
is RemoteClose -> closingType.remoteCommit.txid
is RecoveryClose -> closingType.remoteCommitPublished.commitTx.txid
is RevokedClose -> closingType.revokedCommitPublished.commitTx.txid
}
return ChannelAction.Storage.SetLocked(txId)
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy