diff --git a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt index ecbefa638..d1897c2fe 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManager.kt @@ -57,31 +57,34 @@ class SwapInManager(private var reservedUtxos: Set, private val logger companion object { /** - * Return the list of wallet inputs used in pending unconfirmed channel funding attempts. + * Return the list of wallet inputs already used in channel funding attempts. * These inputs should not be reused in other funding attempts, otherwise we would double-spend ourselves. */ fun reservedWalletInputs(channels: List): Set { - val unconfirmedFundingTxs: List = buildList { + return buildSet { for (channel in channels) { // Add all unsigned inputs currently used to build a funding tx that isn't broadcast yet (creation, rbf, splice). when { - channel is WaitForFundingSigned -> add(channel.signingSession.fundingTx) - channel is WaitForFundingConfirmed && channel.rbfStatus is RbfStatus.WaitingForSigs -> add(channel.rbfStatus.session.fundingTx) - channel is Normal && channel.spliceStatus is SpliceStatus.WaitingForSigs -> add(channel.spliceStatus.session.fundingTx) + channel is WaitForFundingSigned -> addAll(channel.signingSession.fundingTx.tx.localInputs.map { it.outPoint }) + channel is WaitForFundingConfirmed && channel.rbfStatus is RbfStatus.WaitingForSigs -> addAll(channel.rbfStatus.session.fundingTx.tx.localInputs.map { it.outPoint }) + channel is Normal && channel.spliceStatus is SpliceStatus.WaitingForSigs -> addAll(channel.spliceStatus.session.fundingTx.tx.localInputs.map { it.outPoint }) else -> {} } - // Add all inputs in unconfirmed funding txs (utxos spent by confirmed transactions will never appear in our wallet). + // Add all inputs from previously broadcast funding txs. + // We include confirmed transactions as well, in case our electrum server (and thus our wallet) isn't up-to-date. when (channel) { is ChannelStateWithCommitments -> channel.commitments.all .map { it.localFundingStatus } - .filterIsInstance() - .forEach { add(it.sharedTx) } + .forEach { fundingStatus -> + when (fundingStatus) { + is LocalFundingStatus.UnconfirmedFundingTx -> addAll(fundingStatus.sharedTx.tx.localInputs.map { it.outPoint }) + is LocalFundingStatus.ConfirmedFundingTx -> addAll(fundingStatus.signedTx.txIn.map { it.outPoint }) + } + } else -> {} } } } - val localInputs = unconfirmedFundingTxs.flatMap { fundingTx -> fundingTx.tx.localInputs.map { it.outPoint } } - return localInputs.toSet() } } } \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt index a92a4f392..56111db03 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInManagerTestsCommon.kt @@ -4,19 +4,22 @@ import fr.acinq.bitcoin.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.NodeParams import fr.acinq.lightning.SwapInParams +import fr.acinq.lightning.blockchain.BITCOIN_FUNDING_DEPTHOK +import fr.acinq.lightning.blockchain.WatchEventConfirmed +import fr.acinq.lightning.channel.ChannelCommand +import fr.acinq.lightning.channel.LNChannel import fr.acinq.lightning.channel.LocalFundingStatus import fr.acinq.lightning.channel.TestsHelper +import fr.acinq.lightning.channel.states.Normal import fr.acinq.lightning.channel.states.SpliceTestsCommon import fr.acinq.lightning.channel.states.WaitForFundingSignedTestsCommon import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.utils.MDCLogger import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.wire.SpliceLocked import org.kodein.log.LoggerFactory import org.kodein.log.newLogger -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.test.* class SwapInManagerTestsCommon : LightningTestSuite() { @@ -170,4 +173,28 @@ class SwapInManagerTestsCommon : LightningTestSuite() { } } + @Test + fun `swap funds -- ignore inputs from confirmed splice`() { + val (alice, bob) = TestsHelper.reachNormal(zeroConf = true) + val (alice1, _) = SpliceTestsCommon.spliceIn(alice, bob, listOf(50_000.sat, 75_000.sat)) + assertEquals(2, alice1.commitments.active.size) + assertIs(alice1.commitments.latest.localFundingStatus) + val inputs = (alice1.commitments.latest.localFundingStatus as LocalFundingStatus.UnconfirmedFundingTx).sharedTx.tx.localInputs + assertEquals(2, inputs.size) // 2 splice inputs + val spliceTx = alice1.commitments.latest.localFundingStatus.signedTx!! + val (alice2, _) = alice1.process(ChannelCommand.WatchReceived(WatchEventConfirmed(alice.channelId, BITCOIN_FUNDING_DEPTHOK, 100, 2, spliceTx))) + val (alice3, _) = alice2.process(ChannelCommand.MessageReceived(SpliceLocked(alice.channelId, spliceTx.hash))) + assertIs>(alice3) + assertEquals(1, alice3.commitments.all.size) + assertIs(alice3.commitments.latest.localFundingStatus) + val wallet = run { + val parentTxs = inputs.map { it.previousTx } + val unspent = inputs.map { i -> UnspentItem(i.outPoint.txid, i.outPoint.index.toInt(), i.txOut.amount.toLong(), 100) } + WalletState(mapOf(dummyAddress to unspent), parentTxs.associateBy { it.txid }) + } + val mgr = SwapInManager(listOf(alice3.state), logger) + val cmd = SwapInCommand.TrySwapIn(currentBlockHeight = 150, wallet = wallet, swapInParams = SwapInParams(minConfirmations = 5, maxConfirmations = 720, refundDelay = 900), trustedTxs = emptySet()) + mgr.process(cmd).also { assertNull(it) } + } + } \ No newline at end of file