All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.fr.acinq.lightning.channel.states.Syncing.kt Maven / Gradle / Ivy

There is a newer version: 1.8.4
Show newest version
package fr.acinq.lightning.channel.states

import fr.acinq.bitcoin.PrivateKey
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.blockchain.*
import fr.acinq.lightning.channel.*
import fr.acinq.lightning.crypto.KeyManager
import fr.acinq.lightning.utils.toByteVector
import fr.acinq.lightning.wire.*

data class Syncing(val state: PersistedChannelState, val channelReestablishSent: Boolean) : ChannelState() {

    val channelId = state.channelId

    fun ChannelContext.channelKeys(): KeyManager.ChannelKeys = when (state) {
        is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(keyManager)
        is ChannelStateWithCommitments -> state.commitments.params.localParams.channelKeys(keyManager)
    }

    override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> {
        return when (cmd) {
            is ChannelCommand.MessageReceived -> when (cmd.message) {
                is ChannelReestablish -> {
                    val (nextState, actions) = when (state) {
                        is LegacyWaitForFundingConfirmed -> {
                            Pair(state, listOf())
                        }
                        is WaitForFundingSigned -> {
                            when (cmd.message.nextFundingTxId) {
                                // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
                                state.signingSession.fundingTx.txId -> {
                                    val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession)
                                    Pair(state, listOf(ChannelAction.Message.Send(commitSig)))
                                }
                                else -> Pair(state, listOf())
                            }
                        }
                        is WaitForFundingConfirmed -> {
                            when (cmd.message.nextFundingTxId) {
                                null -> Pair(state, listOf())
                                else -> {
                                    if (state.rbfStatus is RbfStatus.WaitingForSigs && state.rbfStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) {
                                        // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
                                        logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" }
                                        val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session)
                                        val actions = listOf(ChannelAction.Message.Send(commitSig))
                                        Pair(state, actions)
                                    } else if (state.latestFundingTx.txId == cmd.message.nextFundingTxId) {
                                        val actions = buildList {
                                            if (state.latestFundingTx.sharedTx is PartiallySignedSharedTransaction) {
                                                // We have not received their tx_signatures: we retransmit our commit_sig because we don't know if they received it.
                                                logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" }
                                                val commitSig = state.commitments.latest.remoteCommit.sign(
                                                    channelKeys(),
                                                    state.commitments.params,
                                                    fundingTxIndex = 0,
                                                    state.commitments.latest.remoteFundingPubkey,
                                                    state.commitments.latest.commitInput
                                                )
                                                add(ChannelAction.Message.Send(commitSig))
                                            }
                                            logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" }
                                            add(ChannelAction.Message.Send(state.latestFundingTx.sharedTx.localSigs))
                                        }
                                        Pair(state, actions)
                                    } else {
                                        // The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving their tx_complete).
                                        // We tell them to abort that RBF attempt.
                                        logger.info { "aborting obsolete rbf attempt for fundingTxId=${cmd.message.nextFundingTxId}" }
                                        Pair(state.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(state.channelId, RbfAttemptAborted(state.channelId).message))))
                                    }
                                }
                            }
                        }
                        is WaitForChannelReady -> {
                            val actions = ArrayList()

                            if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) {
                                if (state.commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx) {
                                    if (state.commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction) {
                                        // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it
                                        logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" }
                                        val commitSig = state.commitments.latest.remoteCommit.sign(
                                            channelKeys(),
                                            state.commitments.params,
                                            fundingTxIndex = state.commitments.latest.fundingTxIndex,
                                            state.commitments.latest.remoteFundingPubkey,
                                            state.commitments.latest.commitInput
                                        )
                                        actions.add(ChannelAction.Message.Send(commitSig))
                                    }
                                    logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" }
                                    actions.add(ChannelAction.Message.Send(state.commitments.latest.localFundingStatus.sharedTx.localSigs))
                                } else {
                                    // The funding tx is confirmed, and they have not received our tx_signatures, but they must have received our commit_sig, otherwise they
                                    // would not have sent their tx_signatures and we would not have been able to publish the funding tx in the first place. We could in theory
                                    // recompute our tx_signatures, but instead we do nothing: they will shortly be notified that the funding tx has confirmed.
                                    logger.warning { "cannot re-send tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}, transaction is already confirmed" }
                                }
                            }

                            logger.debug { "re-sending channel_ready" }
                            val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
                            val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint)
                            actions.add(ChannelAction.Message.Send(channelReady))

                            Pair(state, actions)
                        }
                        is LegacyWaitForFundingLocked -> {
                            logger.debug { "re-sending channel_ready" }
                            val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
                            val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint)
                            val actions = listOf(ChannelAction.Message.Send(channelReady))
                            Pair(state, actions)
                        }
                        is Normal -> {
                            when (val syncResult = handleSync(state.commitments, cmd.message)) {
                                is SyncResult.Failure -> handleSyncFailure(state.commitments, cmd.message, syncResult)
                                is SyncResult.Success -> {
                                    // normal case, our data is up-to-date
                                    val actions = ArrayList()

                                    // re-send channel_ready if necessary
                                    if (state.commitments.latest.fundingTxIndex == 0L && cmd.message.nextLocalCommitmentNumber == 1L && state.commitments.localCommitIndex == 0L) {
                                        // If next_local_commitment_number is 1 in both the channel_reestablish it sent and received, then the node MUST retransmit channel_ready, otherwise it MUST NOT
                                        logger.debug { "re-sending channel_ready" }
                                        val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
                                        val channelReady = ChannelReady(state.commitments.channelId, nextPerCommitmentPoint)
                                        actions.add(ChannelAction.Message.Send(channelReady))
                                    }

                                    // resume splice signing session if any
                                    val spliceStatus1 = if (state.spliceStatus is SpliceStatus.WaitingForSigs && state.spliceStatus.session.fundingTx.txId == cmd.message.nextFundingTxId) {
                                        // We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
                                        logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" }
                                        val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session)
                                        actions.add(ChannelAction.Message.Send(commitSig))
                                        state.spliceStatus
                                    } else if (state.commitments.latest.fundingTxId == cmd.message.nextFundingTxId) {
                                        when (val localFundingStatus = state.commitments.latest.localFundingStatus) {
                                            is LocalFundingStatus.UnconfirmedFundingTx -> {
                                                if (localFundingStatus.sharedTx is PartiallySignedSharedTransaction) {
                                                    // If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it
                                                    logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" }
                                                    val commitSig = state.commitments.latest.remoteCommit.sign(
                                                        channelKeys(),
                                                        state.commitments.params,
                                                        fundingTxIndex = state.commitments.latest.fundingTxIndex,
                                                        state.commitments.latest.remoteFundingPubkey,
                                                        state.commitments.latest.commitInput
                                                    )
                                                    actions.add(ChannelAction.Message.Send(commitSig))
                                                }
                                                logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" }
                                                actions.add(ChannelAction.Message.Send(localFundingStatus.sharedTx.localSigs))
                                            }
                                            is LocalFundingStatus.ConfirmedFundingTx -> {
                                                // The funding tx is confirmed, and they have not received our tx_signatures, but they must have received our commit_sig, otherwise they
                                                // would not have sent their tx_signatures and we would not have been able to publish the funding tx in the first place.
                                                logger.info { "re-sending tx_signatures for fundingTxId=${cmd.message.nextFundingTxId}" }
                                                actions.add(ChannelAction.Message.Send(localFundingStatus.localSigs))
                                            }
                                        }
                                        state.spliceStatus
                                    } else if (cmd.message.nextFundingTxId != null) {
                                        // The fundingTxId must be for a splice attempt that we didn't store (we got disconnected before receiving their tx_complete)
                                        logger.info { "aborting obsolete splice attempt for fundingTxId=${cmd.message.nextFundingTxId}" }
                                        actions.add(ChannelAction.Message.Send(TxAbort(state.channelId, SpliceAborted(state.channelId).message)))
                                        SpliceStatus.Aborted
                                    } else {
                                        state.spliceStatus
                                    }

                                    // Re-send splice_locked (must come *after* potentially retransmitting tx_signatures).
                                    // NB: there is a key difference between channel_ready and splice_locked:
                                    // - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
                                    // - splice_locked: the commitment index can be updated as long as it is compatible with all splices, so
                                    //   we must keep sending our most recent splice_locked at each reconnection
                                    state.commitments.active
                                        .filter { it.fundingTxIndex > 0L } // only consider splice txs
                                        .firstOrNull { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
                                        ?.let {
                                            logger.debug { "re-sending splice_locked for fundingTxId=${it.fundingTxId}" }
                                            val spliceLocked = SpliceLocked(channelId, it.fundingTxId)
                                            actions.add(ChannelAction.Message.Send(spliceLocked))
                                        }

                                    // we may need to retransmit updates and/or commit_sig and/or revocation
                                    actions.addAll(syncResult.retransmit.map { ChannelAction.Message.Send(it) })

                                    // then we clean up unsigned updates
                                    val commitments1 = discardUnsignedUpdates(state.commitments)

                                    if (commitments1.changes.localHasChanges()) {
                                        actions.add(ChannelAction.Message.SendToSelf(ChannelCommand.Commitment.Sign))
                                    }

                                    // When a channel is reestablished after a wallet restarts, we need to reprocess incoming HTLCs that may have been only partially processed
                                    // (either because they didn't reach the payment handler, or because the payment handler response didn't reach the channel).
                                    // Otherwise these HTLCs will stay in our commitment until they timeout and our peer closes the channel.
                                    val htlcsToReprocess = commitments1.reprocessIncomingHtlcs()
                                    logger.info { "re-processing signed IN: ${htlcsToReprocess.map { it.add.id }.joinToString()}" }
                                    actions.addAll(htlcsToReprocess)

                                    // BOLT 2: A node if it has sent a previous shutdown MUST retransmit shutdown.
                                    state.localShutdown?.let {
                                        logger.debug { "re-sending local shutdown" }
                                        actions.add(ChannelAction.Message.Send(it))
                                    }
                                    Pair(state.copy(commitments = commitments1, spliceStatus = spliceStatus1), actions)
                                }
                            }
                        }
                        is ShuttingDown -> {
                            when (val syncResult = handleSync(state.commitments, cmd.message)) {
                                is SyncResult.Failure -> handleSyncFailure(state.commitments, cmd.message, syncResult)
                                is SyncResult.Success -> {
                                    val commitments1 = discardUnsignedUpdates(state.commitments)
                                    val actions = buildList {
                                        addAll(syncResult.retransmit)
                                        add(state.localShutdown)
                                    }.map { ChannelAction.Message.Send(it) }
                                    Pair(state.copy(commitments = commitments1), actions)
                                }
                            }
                        }
                        // negotiation restarts from the beginning, and is initialized by the initiator
                        // note: in any case we still need to keep all previously sent closing_signed, because they may publish one of them
                        is Negotiating ->
                            if (state.paysClosingFees) {
                                // we could use the last closing_signed we sent, but network fees may have changed while we were offline so it is better to restart from scratch
                                val (closingTx, closingSigned) = Helpers.Closing.makeFirstClosingTx(
                                    channelKeys(),
                                    state.commitments.latest,
                                    state.localShutdown.scriptPubKey.toByteArray(),
                                    state.remoteShutdown.scriptPubKey.toByteArray(),
                                    state.closingFeerates ?: ClosingFeerates(currentOnChainFeerates().mutualCloseFeerate)
                                )
                                val closingTxProposed1 = state.closingTxProposed + listOf(listOf(ClosingTxProposed(closingTx, closingSigned)))
                                val nextState = state.copy(closingTxProposed = closingTxProposed1)
                                val actions = listOf(
                                    ChannelAction.Storage.StoreState(nextState),
                                    ChannelAction.Message.Send(state.localShutdown),
                                    ChannelAction.Message.Send(closingSigned)
                                )
                                Pair(nextState, actions)
                            } else {
                                // we start a new round of negotiation
                                val closingTxProposed1 = if (state.closingTxProposed.last().isEmpty()) state.closingTxProposed else state.closingTxProposed + listOf(listOf())
                                val nextState = state.copy(closingTxProposed = closingTxProposed1)
                                val actions = listOf(
                                    ChannelAction.Storage.StoreState(nextState),
                                    ChannelAction.Message.Send(state.localShutdown)
                                )
                                Pair(nextState, actions)
                            }
                        is Closing, is Closed, is WaitForRemotePublishFutureCommitment -> unhandled(cmd)
                    }
                    Pair(nextState, buildList {
                        if (!channelReestablishSent) {
                            val channelReestablish = state.run { createChannelReestablish() }
                            add(ChannelAction.Message.Send(channelReestablish))
                        }
                        addAll(actions)
                    })
                }
                is Error -> state.run { handleRemoteError(cmd.message) }
                else -> unhandled(cmd)
            }
            is ChannelCommand.WatchReceived -> when (state) {
                is ChannelStateWithCommitments -> when (val watch = cmd.watch) {
                    is WatchEventSpent -> when {
                        state is Negotiating && state.closingTxProposed.flatten().any { it.unsignedTx.tx.txid == watch.tx.txid } -> {
                            logger.info { "closing tx published: closingTxId=${watch.tx.txid}" }
                            val closingTx = state.getMutualClosePublished(watch.tx)
                            val nextState = Closing(
                                state.commitments,
                                waitingSinceBlock = currentBlockHeight.toLong(),
                                mutualCloseProposed = state.closingTxProposed.flatten().map { it.unsignedTx },
                                mutualClosePublished = listOf(closingTx)
                            )
                            val actions = listOf(
                                ChannelAction.Storage.StoreState(nextState),
                                ChannelAction.Blockchain.PublishTx(closingTx),
                                ChannelAction.Blockchain.SendWatch(WatchConfirmed(channelId, watch.tx, staticParams.nodeParams.minDepthBlocks.toLong(), BITCOIN_TX_CONFIRMED(watch.tx)))
                            )
                            Pair(nextState, actions)
                        }
                        else -> {
                            val (nextState, actions) = state.run { handlePotentialForceClose(watch) }
                            when (nextState) {
                                is Closing -> Pair(nextState, actions)
                                is Closed -> Pair(nextState, actions)
                                else -> Pair(Syncing(nextState, channelReestablishSent), actions)
                            }
                        }
                    }
                    is WatchEventConfirmed -> {
                        if (watch.event is BITCOIN_FUNDING_DEPTHOK) {
                            when (val res = state.run { acceptFundingTxConfirmed(watch) }) {
                                is Either.Left -> Pair(this@Syncing, listOf())
                                is Either.Right -> {
                                    val (commitments1, _, actions) = res.value
                                    val nextState = when (state) {
                                        is WaitForFundingConfirmed -> {
                                            logger.info { "was confirmed while syncing at blockHeight=${watch.blockHeight} txIndex=${watch.txIndex} with funding txid=${watch.tx.txid}" }
                                            val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
                                            val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId))))
                                            val shortChannelId = ShortChannelId(watch.blockHeight, watch.txIndex, commitments1.latest.commitInput.outPoint.index.toInt())
                                            WaitForChannelReady(commitments1, shortChannelId, channelReady)
                                        }
                                        else -> state
                                    }
                                    Pair([email protected](state = nextState), actions + listOf(ChannelAction.Storage.StoreState(nextState)))
                                }
                            }
                        } else {
                            Pair(this@Syncing, listOf())
                        }
                    }
                }
                is WaitForFundingSigned -> Pair(this@Syncing, listOf())
            }
            is ChannelCommand.Commitment.CheckHtlcTimeout -> when (state) {
                is ChannelStateWithCommitments -> {
                    val (newState, actions) = state.run { checkHtlcTimeout() }
                    when (newState) {
                        is Closing -> Pair(newState, actions)
                        is Closed -> Pair(newState, actions)
                        else -> Pair(Syncing(newState, channelReestablishSent), actions)
                    }
                }
                is WaitForFundingSigned -> Pair(this@Syncing, listOf())
            }
            is ChannelCommand.Commitment -> unhandled(cmd)
            is ChannelCommand.Htlc -> unhandled(cmd)
            is ChannelCommand.Connected -> unhandled(cmd)
            is ChannelCommand.Disconnected -> Pair(Offline(state), listOf())
            is ChannelCommand.Close.MutualClose -> Pair(this@Syncing, listOf(ChannelAction.ProcessCmdRes.NotExecuted(cmd, CommandUnavailableInThisState(channelId, stateName))))
            is ChannelCommand.Close.ForceClose -> state.run { handleLocalError(cmd, ForcedLocalCommit(channelId)) }
            is ChannelCommand.Init -> unhandled(cmd)
            is ChannelCommand.Funding -> unhandled(cmd)
            is ChannelCommand.Closing -> unhandled(cmd)
        }
    }

    private fun ChannelContext.handleSyncFailure(commitments: Commitments, remoteChannelReestablish: ChannelReestablish, syncFailure: SyncResult.Failure): Pair> {
        return when (syncFailure) {
            is SyncResult.Failure.LocalLateProven -> {
                // their data checks out, we indeed seem to be using an old revoked commitment, and must absolutely *NOT* publish it, because that would be a cheating attempt and they
                // would punish us by taking all the funds in the channel
                logger.warning { "counterparty proved that we have an outdated (revoked) local commitment!!! ourCommitmentNumber=${commitments.localCommitIndex} theirCommitmentNumber=${remoteChannelReestablish.nextRemoteRevocationNumber}" }
                handleOutdatedCommitment(remoteChannelReestablish, commitments)
            }
            is SyncResult.Failure.LocalLateUnproven -> {
                // there is no way to make sure that they are saying the truth, the best thing to do is ask them to publish their commitment right now
                // maybe they will publish their commitment, in that case we need to remember their commitment point in order to be able to claim our outputs
                // not that if they don't comply, we could publish our own commitment (it is not stale, otherwise we would be in the case above)
                logger.warning { "counterparty says that they have a more recent commitment than the one we know of!!! ourCommitmentNumber=${commitments.latest.nextRemoteCommit?.commit?.index ?: commitments.latest.remoteCommit.index} theirCommitmentNumber=${remoteChannelReestablish.nextLocalCommitmentNumber}" }
                handleOutdatedCommitment(remoteChannelReestablish, commitments)
            }
            is SyncResult.Failure.RemoteLying -> {
                // they are deliberately trying to fool us into thinking we have a late commitment, but we cannot risk publishing it ourselves, because it may really be revoked!
                logger.warning { "counterparty claims that we have an outdated commitment, but they sent an invalid proof, so our commitment may or may not be revoked: ourLocalCommitmentNumber=${commitments.localCommitIndex} theirRemoteCommitmentNumber=${remoteChannelReestablish.nextRemoteRevocationNumber}" }
                handleOutdatedCommitment(remoteChannelReestablish, commitments)
            }
            is SyncResult.Failure.RemoteLate -> {
                logger.error { "counterparty appears to be using an outdated commitment, they may request a force-close, standing by..." }
                Pair(this@Syncing, listOf())
            }
        }
    }

    companion object {

        // @formatter:off
        sealed class SyncResult {
            data class Success(val retransmit: List) : SyncResult()
            sealed class Failure : SyncResult() {
                data class LocalLateProven(val ourLocalCommitmentNumber: Long, val theirRemoteCommitmentNumber: Long) : Failure()
                data class LocalLateUnproven(val ourRemoteCommitmentNumber: Long, val theirLocalCommitmentNumber: Long) : Failure()
                data class RemoteLying(val ourLocalCommitmentNumber: Long, val theirRemoteCommitmentNumber: Long, val invalidPerCommitmentSecret: PrivateKey) : Failure()
                data object RemoteLate : Failure()
            }
        }
        // @formatter:on

        /**
         * Check whether we are in sync with our peer.
         */
        fun ChannelContext.handleSync(commitments: Commitments, remoteChannelReestablish: ChannelReestablish): SyncResult {
            val channelKeys = keyManager.channelKeys(commitments.params.localParams.fundingKeyPath)
            // This is done in two steps:
            // - step 1: we check our local commitment
            // - step 2: we check the remote commitment
            // step 2 depends on step 1 because we need to preserve ordering between commit_sig and revocation

            // step 2: we check the remote commitment
            fun checkRemoteCommit(remoteChannelReestablish: ChannelReestablish, retransmitRevocation: RevokeAndAck?): SyncResult {
                return when (val rnci = commitments.remoteNextCommitInfo) {
                    is Either.Left -> {
                        when {
                            remoteChannelReestablish.nextLocalCommitmentNumber == commitments.nextRemoteCommitIndex -> {
                                // we just sent a new commit_sig but they didn't receive it
                                // we resend the same updates and the same sig, and preserve the same ordering
                                val signedUpdates = commitments.changes.localChanges.signed
                                val commitSigs = commitments.active.map { it.nextRemoteCommit }.filterIsInstance().map { it.sig }
                                SyncResult.Success(retransmit = when (retransmitRevocation) {
                                    null -> buildList {
                                        addAll(signedUpdates)
                                        addAll(commitSigs)
                                    }
                                    else -> if (commitments.localCommitIndex > rnci.value.sentAfterLocalCommitIndex) {
                                        buildList {
                                            addAll(signedUpdates)
                                            addAll(commitSigs)
                                            add(retransmitRevocation)
                                        }
                                    } else {
                                        buildList {
                                            add(retransmitRevocation)
                                            addAll(signedUpdates)
                                            addAll(commitSigs)
                                        }
                                    }
                                })
                            }
                            remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.nextRemoteCommitIndex + 1) -> {
                                // we just sent a new commit_sig, they have received it but we haven't received their revocation
                                SyncResult.Success(retransmit = listOfNotNull(retransmitRevocation))
                            }
                            remoteChannelReestablish.nextLocalCommitmentNumber < commitments.nextRemoteCommitIndex -> {
                                // they are behind
                                SyncResult.Failure.RemoteLate
                            }
                            else -> {
                                // we are behind
                                SyncResult.Failure.LocalLateUnproven(
                                    ourRemoteCommitmentNumber = commitments.nextRemoteCommitIndex,
                                    theirLocalCommitmentNumber = remoteChannelReestablish.nextLocalCommitmentNumber - 1
                                )
                            }
                        }
                    }
                    is Either.Right -> {
                        when {
                            remoteChannelReestablish.nextLocalCommitmentNumber == (commitments.remoteCommitIndex + 1) -> {
                                // they have acknowledged the last commit_sig we sent
                                SyncResult.Success(retransmit = listOfNotNull(retransmitRevocation))
                            }
                            remoteChannelReestablish.nextLocalCommitmentNumber < (commitments.remoteCommitIndex + 1) -> {
                                // they are behind
                                SyncResult.Failure.RemoteLate
                            }
                            else -> {
                                // we are behind
                                SyncResult.Failure.LocalLateUnproven(
                                    ourRemoteCommitmentNumber = commitments.remoteCommitIndex,
                                    theirLocalCommitmentNumber = remoteChannelReestablish.nextLocalCommitmentNumber - 1
                                )
                            }
                        }
                    }
                }
            }

            // step 1: we check our local commitment
            return if (commitments.localCommitIndex == remoteChannelReestablish.nextRemoteRevocationNumber) {
                // our local commitment is in sync, let's check the remote commitment
                checkRemoteCommit(remoteChannelReestablish, retransmitRevocation = null)
            } else if (commitments.localCommitIndex == remoteChannelReestablish.nextRemoteRevocationNumber + 1) {
                // they just sent a new commit_sig, we have received it but they didn't receive our revocation
                val localPerCommitmentSecret = channelKeys.commitmentSecret(commitments.localCommitIndex - 1)
                val localNextPerCommitmentPoint = channelKeys.commitmentPoint(commitments.localCommitIndex + 1)
                val revocation = RevokeAndAck(
                    channelId = commitments.channelId,
                    perCommitmentSecret = localPerCommitmentSecret,
                    nextPerCommitmentPoint = localNextPerCommitmentPoint
                )
                checkRemoteCommit(remoteChannelReestablish, retransmitRevocation = revocation)
            } else if (commitments.localCommitIndex > remoteChannelReestablish.nextRemoteRevocationNumber + 1) {
                SyncResult.Failure.RemoteLate
            } else {
                // if next_remote_revocation_number is greater than our local commitment index, it means that either we are using an outdated commitment, or they are lying
                // but first we need to make sure that the last per_commitment_secret that they claim to have received from us is correct for that next_remote_revocation_number minus 1
                if (channelKeys.commitmentSecret(remoteChannelReestablish.nextRemoteRevocationNumber - 1) == remoteChannelReestablish.yourLastCommitmentSecret) {
                    SyncResult.Failure.LocalLateProven(
                        ourLocalCommitmentNumber = commitments.localCommitIndex,
                        theirRemoteCommitmentNumber = remoteChannelReestablish.nextRemoteRevocationNumber
                    )
                } else {
                    // they lied! the last per_commitment_secret they claimed to have received from us is invalid
                    SyncResult.Failure.RemoteLying(
                        ourLocalCommitmentNumber = commitments.localCommitIndex,
                        theirRemoteCommitmentNumber = remoteChannelReestablish.nextRemoteRevocationNumber,
                        invalidPerCommitmentSecret = remoteChannelReestablish.yourLastCommitmentSecret
                    )
                }
            }
        }

        /** When reconnecting, we drop all unsigned changes. */
        fun ChannelContext.discardUnsignedUpdates(commitments: Commitments): Commitments {
            logger.debug { "discarding proposed OUT: ${commitments.changes.localChanges.proposed}" }
            logger.debug { "discarding proposed IN: ${commitments.changes.remoteChanges.proposed}" }
            val commitments1 = commitments.copy(
                changes = commitments.changes.copy(
                    localChanges = commitments.changes.localChanges.copy(proposed = emptyList()),
                    remoteChanges = commitments.changes.remoteChanges.copy(proposed = emptyList()),
                    localNextHtlcId = commitments.changes.localNextHtlcId - commitments.changes.localChanges.proposed.filterIsInstance().size,
                    remoteNextHtlcId = commitments.changes.remoteNextHtlcId - commitments.changes.remoteChanges.proposed.filterIsInstance().size
                )
            )
            logger.debug { "localNextHtlcId=${commitments.changes.localNextHtlcId}->${commitments1.changes.localNextHtlcId}" }
            logger.debug { "remoteNextHtlcId=${commitments.changes.remoteNextHtlcId}->${commitments1.changes.remoteNextHtlcId}" }
            return commitments1
        }

        private fun handleOutdatedCommitment(remoteChannelReestablish: ChannelReestablish, commitments: Commitments): Pair> {
            val exc = PleasePublishYourCommitment(commitments.channelId)
            val error = Error(commitments.channelId, exc.message.encodeToByteArray().toByteVector())
            val nextState = WaitForRemotePublishFutureCommitment(commitments, remoteChannelReestablish)
            val actions = listOf(
                ChannelAction.Storage.StoreState(nextState),
                ChannelAction.Message.Send(error)
            )
            return Pair(nextState, actions)
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy