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

Unlock swap-in utxos if initial channel creation fails #689

Merged
merged 1 commit into from
Sep 26, 2024
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
66 changes: 35 additions & 31 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ sealed class ChannelCommand {
data object Disconnected : ChannelCommand()
sealed class Init : ChannelCommand() {
data class Initiator(
val replyTo: CompletableDeferred<ChannelFundingResponse>,
val fundingAmount: Satoshi,
val pushAmount: MilliSatoshi,
val walletInputs: List<WalletState.Utxo>,
Expand All @@ -42,6 +43,7 @@ sealed class ChannelCommand {
}

data class NonInitiator(
val replyTo: CompletableDeferred<ChannelFundingResponse>,
val temporaryChannelId: ByteVector32,
val fundingAmount: Satoshi,
val pushAmount: MilliSatoshi,
Expand Down Expand Up @@ -88,7 +90,7 @@ sealed class ChannelCommand {
data object CheckHtlcTimeout : Commitment()
sealed class Splice : Commitment() {
data class Request(
val replyTo: CompletableDeferred<Response>,
val replyTo: CompletableDeferred<ChannelFundingResponse>,
val spliceIn: SpliceIn?,
val spliceOut: SpliceOut?,
val requestRemoteFunding: LiquidityAds.RequestFunding?,
Expand All @@ -102,36 +104,6 @@ sealed class ChannelCommand {
data class SpliceIn(val walletInputs: List<WalletState.Utxo>, val pushAmount: MilliSatoshi = 0.msat)
data class SpliceOut(val amount: Satoshi, val scriptPubKey: ByteVector)
}

sealed class Response {
/**
* This response doesn't fully guarantee that the splice will confirm, because our peer may potentially double-spend
* the splice transaction. Callers should wait for on-chain confirmations and handle double-spend events.
*/
data class Created(
val channelId: ByteVector32,
val fundingTxIndex: Long,
val fundingTxId: TxId,
val capacity: Satoshi,
val balance: MilliSatoshi,
val liquidityPurchase: LiquidityAds.Purchase?,
) : Response()

sealed class Failure : Response() {
data class InsufficientFunds(val balanceAfterFees: MilliSatoshi, val liquidityFees: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Failure()
data object InvalidSpliceOutPubKeyScript : Failure()
data object SpliceAlreadyInProgress : Failure()
data object ConcurrentRemoteSplice : Failure()
data object ChannelNotQuiescent : Failure()
data class InvalidLiquidityAds(val reason: ChannelException) : Failure()
data class FundingFailure(val reason: FundingContributionFailure) : Failure()
data object CannotStartSession : Failure()
data class InteractiveTxSessionFailed(val reason: InteractiveTxSessionAction.RemoteFailure) : Failure()
data class CannotCreateCommitTx(val reason: ChannelException) : Failure()
data class AbortedByPeer(val reason: String) : Failure()
data object Disconnected : Failure()
}
}
}
}

Expand All @@ -144,4 +116,36 @@ sealed class ChannelCommand {
data class GetHtlcInfosResponse(val revokedCommitTxId: TxId, val htlcInfos: List<ChannelAction.Storage.HtlcInfo>) : Closing()
}
// @formatter:on
}

sealed class ChannelFundingResponse {
/**
* This response doesn't fully guarantee that the channel transaction will confirm, because our peer may potentially double-spend it.
* Callers should wait for on-chain confirmations and handle double-spend events.
*/
data class Success(
val channelId: ByteVector32,
val fundingTxIndex: Long,
val fundingTxId: TxId,
val capacity: Satoshi,
val balance: MilliSatoshi,
val liquidityPurchase: LiquidityAds.Purchase?,
) : ChannelFundingResponse()

sealed class Failure : ChannelFundingResponse() {
data class InsufficientFunds(val balanceAfterFees: MilliSatoshi, val liquidityFees: MilliSatoshi, val currentFeeCredit: MilliSatoshi) : Failure()
data object InvalidSpliceOutPubKeyScript : ChannelFundingResponse.Failure()
data object SpliceAlreadyInProgress : Failure()
data object ConcurrentRemoteSplice : Failure()
data object ChannelNotQuiescent : Failure()
data class InvalidChannelParameters(val reason: ChannelException) : Failure()
data class InvalidLiquidityAds(val reason: ChannelException) : Failure()
data class FundingFailure(val reason: FundingContributionFailure) : Failure()
data object CannotStartSession : Failure()
data class InteractiveTxSessionFailed(val reason: InteractiveTxSessionAction.RemoteFailure) : Failure()
data class CannotCreateCommitTx(val reason: ChannelException) : Failure()
data class AbortedByPeer(val reason: String) : Failure()
data class UnexpectedMessage(val msg: LightningMessage) : Failure()
data object Disconnected : Failure()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,7 @@ sealed class SpliceStatus {
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : QuiescentSpliceStatus()
/** We both agreed to splice and are building the splice transaction. */
data class InProgress(
val replyTo: CompletableDeferred<ChannelCommand.Commitment.Splice.Response>?,
val replyTo: CompletableDeferred<ChannelFundingResponse>?,
val spliceSession: InteractiveTxSession,
val localPushAmount: MilliSatoshi,
val remotePushAmount: MilliSatoshi,
Expand Down
36 changes: 18 additions & 18 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ data class Normal(
}
else -> {
logger.warning { "cannot initiate splice, another splice is already in progress" }
cmd.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.SpliceAlreadyInProgress)
cmd.replyTo.complete(ChannelFundingResponse.Failure.SpliceAlreadyInProgress)
Pair(this@Normal, emptyList())
}
}
Expand Down Expand Up @@ -370,7 +370,7 @@ data class Normal(
// We could keep track of our splice attempt and merge it with the remote splice instead of cancelling it.
// But this is an edge case that should rarely occur, so it's probably not worth the additional complexity.
logger.warning { "our peer initiated quiescence before us, cancelling our splice attempt" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.ConcurrentRemoteSplice)
Pair([email protected](spliceStatus = SpliceStatus.ReceivedStfu(cmd.message)), emptyList())
}
is SpliceStatus.InitiatorQuiescent -> {
Expand Down Expand Up @@ -404,12 +404,12 @@ data class Normal(
val balanceAfterFees = parentCommitment.localCommit.spec.toLocal + fundingContribution.toMilliSatoshi() - liquidityFeesOwed
if (balanceAfterFees < parentCommitment.localChannelReserve(commitments.params).max(commitTxFees)) {
logger.warning { "cannot do splice: insufficient funds (balanceAfterFees=$balanceAfterFees, liquidityFees=$liquidityFees, feeCredit=${spliceStatus.command.currentFeeCredit})" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InsufficientFunds(balanceAfterFees, liquidityFees, spliceStatus.command.currentFeeCredit))
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InsufficientFunds(balanceAfterFees, liquidityFees, spliceStatus.command.currentFeeCredit))
val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message)))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), action)
} else if (spliceStatus.command.spliceOut?.scriptPubKey?.let { Helpers.Closing.isValidFinalScriptPubkey(it, allowAnySegwit = true) } == false) {
logger.warning { "cannot do splice: invalid splice-out script" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidSpliceOutPubKeyScript)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InvalidSpliceOutPubKeyScript)
val action = listOf(ChannelAction.Message.Send(TxAbort(channelId, InvalidSpliceRequest(channelId).message)))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), action)
} else {
Expand All @@ -427,7 +427,7 @@ data class Normal(
}
} else {
logger.warning { "cannot initiate splice, channel not quiescent" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ChannelNotQuiescent)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.ChannelNotQuiescent)
val actions = buildList {
add(ChannelAction.Message.Send(Warning(channelId, InvalidSpliceNotQuiescent(channelId).message)))
add(ChannelAction.Disconnect)
Expand All @@ -436,7 +436,7 @@ data class Normal(
}
} else {
logger.warning { "concurrent stfu received and our peer is the channel initiator, cancelling our splice attempt" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.ConcurrentRemoteSplice)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.ConcurrentRemoteSplice)
Pair([email protected](spliceStatus = SpliceStatus.NonInitiatorQuiescent), emptyList())
}
}
Expand Down Expand Up @@ -525,7 +525,7 @@ data class Normal(
)) {
is Either.Left<ChannelException> -> {
logger.error { "rejecting liquidity proposal: ${liquidityPurchase.value.message}" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.InvalidLiquidityAds(liquidityPurchase.value))
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.InvalidLiquidityAds(liquidityPurchase.value))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, liquidityPurchase.value.message))))
}
is Either.Right<LiquidityAds.Purchase?> -> {
Expand Down Expand Up @@ -557,7 +557,7 @@ data class Normal(
)) {
is Either.Left -> {
logger.error { "could not create splice contributions: ${fundingContributions.value}" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.FundingFailure(fundingContributions.value))
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.FundingFailure(fundingContributions.value))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
is Either.Right -> {
Expand Down Expand Up @@ -589,7 +589,7 @@ data class Normal(
}
else -> {
logger.error { "could not start interactive-tx session: $interactiveTxAction" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.CannotStartSession)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.CannotStartSession)
Pair([email protected](spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
}
Expand Down Expand Up @@ -629,7 +629,7 @@ data class Normal(
when (signingSession) {
is Either.Left -> {
logger.error(signingSession.value) { "cannot initiate interactive-tx splice signing session" }
spliceStatus.replyTo?.complete(ChannelCommand.Commitment.Splice.Response.Failure.CannotCreateCommitTx(signingSession.value))
spliceStatus.replyTo?.complete(ChannelFundingResponse.Failure.CannotCreateCommitTx(signingSession.value))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, signingSession.value.message))))
}
is Either.Right -> {
Expand All @@ -639,7 +639,7 @@ data class Normal(
// It is likely that we will restart before the transaction is confirmed, in which case we will lose the replyTo and the ability to notify the caller.
// We should be able to resume the signing steps and complete the splice if we disconnect, so we optimistically notify the caller now.
spliceStatus.replyTo?.complete(
ChannelCommand.Commitment.Splice.Response.Created(
ChannelFundingResponse.Success(
channelId = channelId,
fundingTxIndex = session.fundingTxIndex,
fundingTxId = session.fundingTx.txId,
Expand All @@ -660,7 +660,7 @@ data class Normal(
}
is InteractiveTxSessionAction.RemoteFailure -> {
logger.warning { "interactive-tx failed: $interactiveTxAction" }
spliceStatus.replyTo?.complete(ChannelCommand.Commitment.Splice.Response.Failure.InteractiveTxSessionFailed(interactiveTxAction))
spliceStatus.replyTo?.complete(ChannelFundingResponse.Failure.InteractiveTxSessionFailed(interactiveTxAction))
Pair([email protected](spliceStatus = SpliceStatus.Aborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, interactiveTxAction.toString()))))
}
}
Expand Down Expand Up @@ -723,7 +723,7 @@ data class Normal(
is TxAbort -> when (spliceStatus) {
is SpliceStatus.Requested -> {
logger.info { "our peer rejected our splice request: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer(cmd.message.toAscii()))
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.AbortedByPeer(cmd.message.toAscii()))
val actions = buildList {
add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message)))
addAll(endQuiescence())
Expand All @@ -732,7 +732,7 @@ data class Normal(
}
is SpliceStatus.InProgress -> {
logger.info { "our peer aborted the splice attempt: ascii='${cmd.message.toAscii()}' bin=${cmd.message.data}" }
spliceStatus.replyTo?.complete(ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer(cmd.message.toAscii()))
spliceStatus.replyTo?.complete(ChannelFundingResponse.Failure.AbortedByPeer(cmd.message.toAscii()))
val actions = buildList {
add(ChannelAction.Message.Send(TxAbort(channelId, SpliceAborted(channelId).message)))
addAll(endQuiescence())
Expand Down Expand Up @@ -778,7 +778,7 @@ data class Normal(
is CancelOnTheFlyFunding -> when (spliceStatus) {
is SpliceStatus.Requested -> {
logger.info { "our peer rejected our on-the-fly splice request: ascii='${cmd.message.toAscii()}'" }
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.AbortedByPeer(cmd.message.toAscii()))
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.AbortedByPeer(cmd.message.toAscii()))
Pair([email protected](spliceStatus = SpliceStatus.None), endQuiescence())
}
else -> {
Expand Down Expand Up @@ -832,18 +832,18 @@ data class Normal(
is SpliceStatus.None -> SpliceStatus.None
is SpliceStatus.Aborted -> SpliceStatus.None
is SpliceStatus.Requested -> {
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.Disconnected)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.Disconnected)
SpliceStatus.None
}
is SpliceStatus.InProgress -> {
spliceStatus.replyTo?.complete(ChannelCommand.Commitment.Splice.Response.Failure.Disconnected)
spliceStatus.replyTo?.complete(ChannelFundingResponse.Failure.Disconnected)
SpliceStatus.None
}
is SpliceStatus.WaitingForSigs -> spliceStatus
is SpliceStatus.NonInitiatorQuiescent -> SpliceStatus.None
is QuiescenceNegotiation.NonInitiator -> SpliceStatus.None
is QuiescenceNegotiation.Initiator -> {
spliceStatus.command.replyTo.complete(ChannelCommand.Commitment.Splice.Response.Failure.Disconnected)
spliceStatus.command.replyTo.complete(ChannelFundingResponse.Failure.Disconnected)
SpliceStatus.None
}
}
Expand Down
Loading