commonMain.fr.acinq.lightning.channel.states.WaitForFundingConfirmed.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.TxId
import fr.acinq.bitcoin.utils.Either
import fr.acinq.lightning.MilliSatoshi
import fr.acinq.lightning.ShortChannelId
import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK
import fr.acinq.lightning.blockchain.WatchConfirmed
import fr.acinq.lightning.blockchain.WatchEventConfirmed
import fr.acinq.lightning.channel.*
import fr.acinq.lightning.utils.msat
import fr.acinq.lightning.utils.toMilliSatoshi
import fr.acinq.lightning.wire.*
/** We wait for the channel funding transaction to confirm. */
data class WaitForFundingConfirmed(
override val commitments: Commitments,
val localPushAmount: MilliSatoshi,
val remotePushAmount: MilliSatoshi,
val waitingSinceBlock: Long, // how many blocks have we been waiting for the funding tx to confirm
val deferred: ChannelReady?,
// We can have at most one ongoing RBF attempt.
val rbfStatus: RbfStatus
) : ChannelStateWithCommitments() {
val latestFundingTx = commitments.latest.localFundingStatus as LocalFundingStatus.UnconfirmedFundingTx
private val allFundingTxs = commitments.active.map { it.localFundingStatus }.filterIsInstance()
val previousFundingTxs = allFundingTxs.filter { it.txId != latestFundingTx.txId }
override fun updateCommitments(input: Commitments): ChannelStateWithCommitments = this.copy(commitments = input)
override suspend fun ChannelContext.processInternal(cmd: ChannelCommand): Pair> {
return when (cmd) {
is ChannelCommand.MessageReceived -> when (cmd.message) {
is TxSignatures -> when (latestFundingTx.sharedTx) {
is PartiallySignedSharedTransaction -> when (val fullySignedTx = latestFundingTx.sharedTx.addRemoteSigs(channelKeys(), latestFundingTx.fundingParams, cmd.message)) {
null -> {
logger.warning { "received invalid remote funding signatures for txId=${cmd.message.txId}" }
// The funding transaction may still confirm (since our peer should be able to generate valid signatures), so we cannot close the channel yet.
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Warning(channelId, InvalidFundingSignature(channelId, cmd.message.txId).message))))
}
else -> {
when (val res = commitments.run { updateLocalFundingSigned(fullySignedTx) }) {
is Either.Left -> Pair(this@WaitForFundingConfirmed, listOf())
is Either.Right -> {
logger.info { "received remote funding signatures, publishing txId=${fullySignedTx.signedTx.txid}" }
val nextState = [email protected](commitments = res.value.first)
val actions = buildList {
add(ChannelAction.Blockchain.PublishTx(fullySignedTx.signedTx, ChannelAction.Blockchain.PublishTx.Type.FundingTx))
add(ChannelAction.Storage.StoreState(nextState))
}
Pair(nextState, actions)
}
}
}
}
is FullySignedSharedTransaction -> when (rbfStatus) {
is RbfStatus.WaitingForSigs -> {
when (val res = rbfStatus.session.receiveTxSigs(channelKeys(), cmd.message, currentBlockHeight.toLong())) {
is Either.Left -> {
val action: InteractiveTxSigningSessionAction.AbortFundingAttempt = res.value
logger.warning { "rbf attempt failed: ${action.reason.message}" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, action.reason.message))))
}
is Either.Right -> {
val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value
sendRbfTxSigs(action, cmd.message.channelData)
}
}
}
else -> {
logger.warning { "rejecting unexpected tx_signatures for txId=${cmd.message.txId}" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, UnexpectedFundingSignatures(channelId).message))))
}
}
}
is TxInitRbf -> {
if (isInitiator) {
logger.info { "rejecting tx_init_rbf, we're the initiator, not them!" }
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Error(channelId, InvalidRbfNonInitiator(channelId).message))))
} else {
val minNextFeerate = latestFundingTx.fundingParams.minNextFeerate
when (rbfStatus) {
RbfStatus.None -> {
if (cmd.message.feerate < minNextFeerate) {
logger.info { "rejecting rbf attempt: the new feerate must be at least $minNextFeerate (proposed=${cmd.message.feerate})" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfFeerate(channelId, cmd.message.feerate, minNextFeerate).message))))
} else if (cmd.message.fundingContribution.toMilliSatoshi() < remotePushAmount) {
logger.info { "rejecting rbf attempt: invalid amount pushed (fundingAmount=${cmd.message.fundingContribution}, pushAmount=$remotePushAmount)" }
Pair(
[email protected](rbfStatus = RbfStatus.RbfAborted),
listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidPushAmount(channelId, remotePushAmount, cmd.message.fundingContribution.toMilliSatoshi()).message)))
)
} else {
logger.info { "our peer wants to raise the feerate of the funding transaction (previous=${latestFundingTx.fundingParams.targetFeerate} target=${cmd.message.feerate})" }
val fundingParams = InteractiveTxParams(
channelId,
isInitiator,
latestFundingTx.fundingParams.localContribution, // we don't change our funding contribution
cmd.message.fundingContribution,
latestFundingTx.fundingParams.remoteFundingPubkey,
cmd.message.lockTime,
latestFundingTx.fundingParams.dustLimit,
cmd.message.feerate
)
val toSend = buildList> {
addAll(latestFundingTx.sharedTx.tx.localInputs.map { Either.Left(it) })
addAll(latestFundingTx.sharedTx.tx.localOutputs.map { Either.Right(it) })
}
val session = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs)
val nextState = [email protected](rbfStatus = RbfStatus.InProgress(session))
Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution))))
}
}
RbfStatus.RbfAborted -> {
logger.info { "rejecting rbf attempt: our previous tx_abort was not acked" }
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Warning(channelId, InvalidRbfTxAbortNotAcked(channelId).message))))
}
else -> {
logger.info { "rejecting rbf attempt: the current rbf attempt must be completed or aborted first" }
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Warning(channelId, InvalidRbfAlreadyInProgress(channelId).message))))
}
}
}
}
is TxAckRbf -> when (rbfStatus) {
is RbfStatus.RbfRequested -> {
logger.info { "our peer accepted our rbf attempt and will contribute ${cmd.message.fundingContribution} to the funding transaction" }
val fundingParams = InteractiveTxParams(
channelId,
isInitiator,
rbfStatus.command.fundingAmount,
cmd.message.fundingContribution,
latestFundingTx.fundingParams.remoteFundingPubkey,
rbfStatus.command.lockTime,
latestFundingTx.fundingParams.dustLimit,
rbfStatus.command.targetFeerate
)
when (val contributions = FundingContributions.create(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, rbfStatus.command.walletInputs)) {
is Either.Left -> {
logger.warning { "error creating funding contributions: ${contributions.value}" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
is Either.Right -> {
val (session, action) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send()
when (action) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = [email protected](rbfStatus = RbfStatus.InProgress(session))
Pair(nextState, listOf(ChannelAction.Message.Send(action.msg)))
}
else -> {
logger.warning { "could not start rbf session: $action" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
}
}
}
}
else -> {
logger.info { "ignoring unexpected tx_ack_rbf" }
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Warning(channelId, UnexpectedInteractiveTxMessage(channelId, cmd.message).message))))
}
}
is InteractiveTxConstructionMessage -> when (rbfStatus) {
is RbfStatus.InProgress -> {
val (rbfSession1, interactiveTxAction) = rbfStatus.rbfSession.receive(cmd.message)
when (interactiveTxAction) {
is InteractiveTxSessionAction.SendMessage -> Pair([email protected](rbfStatus = rbfStatus.copy(rbfSession1)), listOf(ChannelAction.Message.Send(interactiveTxAction.msg)))
is InteractiveTxSessionAction.SignSharedTx -> {
val replacedCommitment = commitments.latest
val signingSession = InteractiveTxSigningSession.create(
rbfSession1,
keyManager,
commitments.params,
rbfSession1.fundingParams,
fundingTxIndex = replacedCommitment.fundingTxIndex,
interactiveTxAction.sharedTx,
localPushAmount,
remotePushAmount,
liquidityLease = null,
localCommitmentIndex = replacedCommitment.localCommit.index,
remoteCommitmentIndex = replacedCommitment.remoteCommit.index,
replacedCommitment.localCommit.spec.feerate,
replacedCommitment.remoteCommit.remotePerCommitmentPoint,
replacedCommitment.localCommit.spec.htlcs
)
when (signingSession) {
is Either.Left -> {
logger.error(signingSession.value) { "cannot initiate interactive-tx rbf signing session" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, signingSession.value.message))))
}
is Either.Right -> {
val (session, commitSig) = signingSession.value
val nextState = [email protected](rbfStatus = RbfStatus.WaitingForSigs(session))
val actions = buildList {
interactiveTxAction.txComplete?.let { add(ChannelAction.Message.Send(it)) }
add(ChannelAction.Storage.StoreState(nextState))
add(ChannelAction.Message.Send(commitSig))
}
Pair(nextState, actions)
}
}
}
is InteractiveTxSessionAction.RemoteFailure -> {
logger.warning { "rbf attempt failed: $interactiveTxAction" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
}
}
else -> {
logger.info { "ignoring unexpected interactive-tx message: ${cmd.message::class}" }
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(Warning(channelId, UnexpectedInteractiveTxMessage(channelId, cmd.message).message))))
}
}
is CommitSig -> when (rbfStatus) {
is RbfStatus.WaitingForSigs -> {
val (signingSession1, action) = rbfStatus.session.receiveCommitSig(channelKeys(), commitments.params, cmd.message, currentBlockHeight.toLong(), logger)
when (action) {
is InteractiveTxSigningSessionAction.AbortFundingAttempt -> {
logger.warning { "rbf attempt failed: ${action.reason.message}" }
Pair([email protected](rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, action.reason.message))))
}
// No need to store their commit_sig, they will re-send it if we disconnect.
InteractiveTxSigningSessionAction.WaitForTxSigs -> Pair([email protected](rbfStatus = rbfStatus.copy(session = signingSession1)), listOf())
is InteractiveTxSigningSessionAction.SendTxSigs -> sendRbfTxSigs(action, cmd.message.channelData)
}
}
else -> {
logger.info { "ignoring redundant commit_sig" }
Pair(this@WaitForFundingConfirmed, listOf())
}
}
is TxAbort -> when (rbfStatus) {
RbfStatus.None -> {
logger.info { "our peer wants to abort the funding attempt, but we've already negotiated a funding transaction: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data.toHex()}" }
// We ack their tx_abort but we keep monitoring the funding transaction until it's confirmed or double-spent.
Pair(this@WaitForFundingConfirmed, listOf(ChannelAction.Message.Send(TxAbort(channelId, DualFundingAborted(channelId, "requested by remote").message))))
}
RbfStatus.RbfAborted -> {
logger.info { "our peer acked our previous tx_abort" }
Pair([email protected](rbfStatus = RbfStatus.None), listOf())
}
else -> {
logger.info { "our peer aborted the rbf attempt: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data.toHex()}" }
Pair([email protected](rbfStatus = RbfStatus.None), listOf(ChannelAction.Message.Send(TxAbort(channelId, RbfAttemptAborted(channelId).message))))
}
}
is ChannelReady -> Pair([email protected](deferred = cmd.message), listOf())
is Error -> handleRemoteError(cmd.message)
else -> unhandled(cmd)
}
is ChannelCommand.WatchReceived -> when (cmd.watch) {
is WatchEventConfirmed -> when (val res = acceptFundingTxConfirmed(cmd.watch)) {
is Either.Left -> Pair(this@WaitForFundingConfirmed, listOf())
is Either.Right -> {
val (commitments1, commitment, actions) = res.value
val nextPerCommitmentPoint = channelKeys().commitmentPoint(1)
val channelReady = ChannelReady(channelId, nextPerCommitmentPoint, TlvStream(ChannelReadyTlv.ShortChannelIdTlv(ShortChannelId.peerId(staticParams.nodeParams.nodeId))))
// this is the temporary channel id that we will use in our channel_update message, the goal is to be able to use our channel
// as soon as it reaches NORMAL state, and before it is announced on the network
// (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime)
val shortChannelId = ShortChannelId(cmd.watch.blockHeight, cmd.watch.txIndex, commitment.commitInput.outPoint.index.toInt())
val nextState = WaitForChannelReady(commitments1, shortChannelId, channelReady)
val actions1 = buildList {
if (rbfStatus != RbfStatus.None) add(ChannelAction.Message.Send(TxAbort(channelId, InvalidRbfTxConfirmed(channelId, cmd.watch.tx.txid).message)))
add(ChannelAction.Message.Send(channelReady))
add(ChannelAction.Storage.StoreState(nextState))
}
if (deferred != null) {
logger.info { "channel_ready has already been received" }
val (nextState1, actions2) = nextState.run { process(ChannelCommand.MessageReceived(deferred)) }
Pair(nextState1, actions + actions1 + actions2)
} else {
Pair(nextState, actions + actions1)
}
}
}
else -> unhandled(cmd)
}
is ChannelCommand.Funding.BumpFundingFee -> when {
!latestFundingTx.fundingParams.isInitiator -> {
logger.warning { "cannot initiate rbf, we're not the initiator" }
Pair(this@WaitForFundingConfirmed, listOf())
}
rbfStatus != RbfStatus.None -> {
logger.warning { "cannot initiate rbf, another one is already in progress" }
Pair(this@WaitForFundingConfirmed, listOf())
}
else -> {
logger.info { "initiating rbf (current feerate = ${latestFundingTx.fundingParams.targetFeerate}, next feerate = ${cmd.targetFeerate})" }
val txInitRbf = TxInitRbf(channelId, cmd.lockTime, cmd.targetFeerate, cmd.fundingAmount)
Pair([email protected](rbfStatus = RbfStatus.RbfRequested(cmd)), listOf(ChannelAction.Message.Send(txInitRbf)))
}
}
is ChannelCommand.Close.MutualClose -> Pair(
this@WaitForFundingConfirmed,
listOf(ChannelAction.ProcessCmdRes.NotExecuted(cmd, CommandUnavailableInThisState(channelId, stateName)))
)
is ChannelCommand.Close.ForceClose -> handleLocalError(cmd, ForcedLocalCommit(channelId))
is ChannelCommand.Commitment.CheckHtlcTimeout -> Pair(this@WaitForFundingConfirmed, listOf())
is ChannelCommand.Commitment -> unhandled(cmd)
is ChannelCommand.Htlc -> unhandled(cmd)
is ChannelCommand.Init -> unhandled(cmd)
is ChannelCommand.Closing -> unhandled(cmd)
is ChannelCommand.Connected -> unhandled(cmd)
is ChannelCommand.Disconnected -> {
val rbfStatus1 = when (rbfStatus) {
// We keep track of the RBF status: we should be able to complete the signature steps on reconnection.
is RbfStatus.WaitingForSigs -> rbfStatus
else -> RbfStatus.None
}
Pair(Offline([email protected](rbfStatus = rbfStatus1)), listOf())
}
}
}
private fun ChannelContext.sendRbfTxSigs(action: InteractiveTxSigningSessionAction.SendTxSigs, remoteChannelData: EncryptedChannelData): Pair> {
logger.info { "rbf funding tx created with txId=${action.fundingTx.txId}, ${action.fundingTx.sharedTx.tx.localInputs.size} local inputs, ${action.fundingTx.sharedTx.tx.remoteInputs.size} remote inputs, ${action.fundingTx.sharedTx.tx.localOutputs.size} local outputs and ${action.fundingTx.sharedTx.tx.remoteOutputs.size} remote outputs" }
val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount)
logger.info { "will wait for $fundingMinDepth confirmations" }
val watchConfirmed = WatchConfirmed(channelId, action.commitment.fundingTxId, action.commitment.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK)
val nextState = WaitForFundingConfirmed(
commitments.add(action.commitment).copy(remoteChannelData = remoteChannelData),
localPushAmount,
remotePushAmount,
waitingSinceBlock,
deferred,
RbfStatus.None
)
val actions = buildList {
add(ChannelAction.Storage.StoreState(nextState))
action.fundingTx.signedTx?.let { add(ChannelAction.Blockchain.PublishTx(it, ChannelAction.Blockchain.PublishTx.Type.FundingTx)) }
add(ChannelAction.Blockchain.SendWatch(watchConfirmed))
add(ChannelAction.Message.Send(action.localSigs))
}
return Pair(nextState, actions)
}
/** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */
fun getUnsignedFundingTxId(): TxId? {
return when (rbfStatus) {
is RbfStatus.WaitingForSigs -> rbfStatus.session.fundingTx.txId
else -> when (latestFundingTx.sharedTx) {
is PartiallySignedSharedTransaction -> latestFundingTx.txId
is FullySignedSharedTransaction -> null
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy