diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index ebef13b46..d601dcb10 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -249,7 +249,7 @@ sealed class FundingContributionFailure { data class FundingContributions(val inputs: List, val outputs: List) { companion object { /** Compute our local splice contribution using all the funds available in our wallet. */ - fun computeSpliceContribution(isInitiator: Boolean, commitment: Commitment, walletInputs: List, localOutputs: List, targetFeerate: FeeratePerKw): Satoshi { + fun computeSpliceContribution(isInitiator: Boolean, commitment: Commitment, walletInputs: List, localOutputs: List, isLiquidityPurchase: Boolean, targetFeerate: FeeratePerKw): Satoshi { val weight = computeWeightPaid(isInitiator, commitment, walletInputs, localOutputs) val fees = Transactions.weight2fee(targetFeerate, weight) return when { @@ -257,7 +257,7 @@ data class FundingContributions(val inputs: List, v // The maximum amount we can use for on-chain fees is our current balance, which is fine because: // - this will simply result in a splice transaction with a lower feerate than expected // - liquidity fees will be paid later from future HTLCs relayed to us - walletInputs.isEmpty() && localOutputs.isEmpty() -> -(fees.min(commitment.localCommit.spec.toLocal.truncateToSatoshi())) + walletInputs.isEmpty() && localOutputs.isEmpty() && isLiquidityPurchase -> -(fees.min(commitment.localCommit.spec.toLocal.truncateToSatoshi())) else -> walletInputs.map { it.amount }.sum() - localOutputs.map { it.amount }.sum() - fees } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index aa9074126..482bd14e6 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -383,6 +383,7 @@ data class Normal( commitment = parentCommitment, walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), localOutputs = spliceStatus.command.spliceOutputs, + isLiquidityPurchase = spliceStatus.command.requestRemoteFunding != null, targetFeerate = spliceStatus.command.feerate ) val commitTxFees = when { @@ -543,7 +544,14 @@ data class Normal( channelKeys = channelKeys(), swapInKeys = keyManager.swapInOnChainWallet, params = fundingParams, - sharedUtxo = Pair(sharedInput, SharedFundingInputBalances(toLocal = parentCommitment.localCommit.spec.toLocal, toRemote = parentCommitment.localCommit.spec.toRemote, toHtlcs = parentCommitment.localCommit.spec.htlcs.map { it.add.amountMsat }.sum())), + sharedUtxo = Pair( + sharedInput, + SharedFundingInputBalances( + toLocal = parentCommitment.localCommit.spec.toLocal, + toRemote = parentCommitment.localCommit.spec.toRemote, + toHtlcs = parentCommitment.localCommit.spec.htlcs.map { it.add.amountMsat }.sum() + ) + ), walletInputs = spliceStatus.command.spliceIn?.walletInputs ?: emptyList(), localOutputs = spliceStatus.command.spliceOutputs, liquidityPurchase = liquidityPurchase.value, diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt index fe3bbe8e9..6f1f8a352 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/states/SpliceTestsCommon.kt @@ -186,6 +186,34 @@ class SpliceTestsCommon : LightningTestSuite() { assertEquals(alice5.state.commitments.latest.localCommit.spec.toRemote, TestConstants.bobFundingAmount.toMilliSatoshi() + 15_000_000.msat) } + @Test + fun `splice cpfp -- not enough funds`() { + val (alice, bob) = reachNormal(channelType = ChannelType.SupportedChannelType.AnchorOutputsZeroReserve, aliceFundingAmount = 75_000.sat, bobFundingAmount = 25_000.sat) + val (alice1, bob1) = spliceOut(alice, bob, 65_000.sat) + // After the splice-out, Alice doesn't have enough funds to pay the mining fees to CPFP. + val spliceCpfp = ChannelCommand.Commitment.Splice.Request( + replyTo = CompletableDeferred(), + spliceIn = null, + spliceOut = null, + requestRemoteFunding = null, + currentFeeCredit = 0.msat, + feerate = FeeratePerKw(20_000.sat), + origins = listOf(), + ) + val (alice2, actionsAlice2) = alice1.process(spliceCpfp) + val aliceStfu = actionsAlice2.findOutgoingMessage() + val (_, actionsBob2) = bob1.process(ChannelCommand.MessageReceived(aliceStfu)) + val bobStfu = actionsBob2.findOutgoingMessage() + val (alice3, actionsAlice3) = alice2.process(ChannelCommand.MessageReceived(bobStfu)) + actionsAlice3.hasOutgoingMessage() + assertIs(alice3.state) + assertEquals(SpliceStatus.Aborted, alice3.state.spliceStatus) + runBlocking { + val response = spliceCpfp.replyTo.await() + assertIs(response) + } + } + @Test fun `splice to purchase inbound liquidity`() { val (alice, bob) = reachNormal()