From 0a455f0b41385f10fce5f022a65467684854a495 Mon Sep 17 00:00:00 2001 From: t-bast Date: Mon, 20 Nov 2023 14:06:55 +0100 Subject: [PATCH] Don't double-spend unspendable inputs When filtering inputs that can be used for swaps, we filter out all the inputs that are already used in pending funding transactions. However, we ignored previously confirmed funding transactions, assuming that our electrum server would have already filtered them from our wallet state. Unfortunately, we cannot assume that our electrum server is up-to-date (especially if we connect to a different one after a restart) and behaving correctly, so we must also exclude inputs use in the latest confirmed funding transaction. --- .../blockchain/electrum/SwapInManager.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) 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