Fix bug where we didn't set spendingTxId when transitioning from Reserved -> PendingConfirmationsSpent (#3909)

* Fix bug where we didn't set spendingTxId when transitioning from Reserved -> PendingConfirmationsSpent

* Try to add unit test

* WIP

* WIP2

* Get reserved state working

* Fix assertion, cleanup logs

* Cleanup test case
This commit is contained in:
Chris Stewart 2022-01-28 06:50:24 -06:00 committed by GitHub
parent 08941206fc
commit cf16d93648
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 19 deletions

View file

@ -1,7 +1,8 @@
package org.bitcoins.wallet package org.bitcoins.wallet
import grizzled.slf4j.Logging
import org.bitcoins.core.api.wallet.db.SpendingInfoDb import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.currency.Satoshis import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.number._ import org.bitcoins.core.number._
import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script._ import org.bitcoins.core.protocol.script._
@ -21,7 +22,9 @@ import org.scalatest.{FutureOutcome, Outcome}
import scala.concurrent.Future import scala.concurrent.Future
class UTXOLifeCycleTest extends BitcoinSWalletTestCachedBitcoindNewest { class UTXOLifeCycleTest
extends BitcoinSWalletTestCachedBitcoindNewest
with Logging {
behavior of "Wallet Txo States" behavior of "Wallet Txo States"
@ -514,4 +517,90 @@ class UTXOLifeCycleTest extends BitcoinSWalletTestCachedBitcoindNewest {
assert(reserved.outPoint == utxos.head.outPoint) assert(reserved.outPoint == utxos.head.outPoint)
} }
} }
it must "mark a utxo as reserved that is still receiving confirmations and not unreserve the utxo" in {
param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param
val addressF = wallet.getNewAddress()
val txIdF =
addressF.flatMap(addr => bitcoind.sendToAddress(addr, Bitcoins.one))
val throwAwayAddrF = bitcoind.getNewAddress
for {
txId <- txIdF
//generate a few blocks to make the utxo pending confirmations received
throwAwayAddr <- throwAwayAddrF
hashes <- bitcoind.generateToAddress(blocks = 1, throwAwayAddr)
block <- bitcoind.getBlockRaw(hashes.head)
_ <- wallet.processBlock(block)
//make sure the utxo is pending confirmations received
utxos <- wallet.listUtxos(TxoState.PendingConfirmationsReceived)
_ = assert(utxos.length == 1)
utxo = utxos.head
_ = assert(utxo.txid == txId)
_ = assert(utxo.state == TxoState.PendingConfirmationsReceived)
//now mark the utxo as reserved
_ <- wallet.markUTXOsAsReserved(Vector(utxo))
//confirm it is reserved
_ <- wallet
.listUtxos(TxoState.Reserved)
.map(utxos =>
assert(utxos.contains(utxo.copyWithState(TxoState.Reserved))))
//now process another block
hashes2 <- bitcoind.generateToAddress(blocks = 1, throwAwayAddr)
block2 <- bitcoind.getBlockRaw(hashes2.head)
_ <- wallet.processBlock(block2)
//the utxo should still be reserved
reservedUtxos <- wallet.listUtxos(TxoState.Reserved)
reservedUtxo = reservedUtxos.head
} yield {
assert(reservedUtxo.txid == txId)
assert(reservedUtxo.state == TxoState.Reserved)
}
}
it must "transition a reserved utxo to spent when we are offline" in {
param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param
val bitcoindAddrF = bitcoind.getNewAddress
val amt = Satoshis(100000)
val utxoCountF = wallet.listUtxos()
for {
bitcoindAdr <- bitcoindAddrF
utxoCount <- utxoCountF
//build a spending transaction
tx <- wallet.sendToAddress(bitcoindAdr, amt, SatoshisPerVirtualByte.one)
c <- wallet.listUtxos()
_ = assert(c.length == utxoCount.length)
txIdBE <- bitcoind.sendRawTransaction(tx)
//find all utxos that we can use to fund a transaction
utxos <- wallet
.listUtxos()
.map(_.filter(u => TxoState.receivedStates.contains(u.state)))
broadcastReceived <- wallet.listUtxos(TxoState.BroadcastReceived)
_ = assert(broadcastReceived.length == 1) //change output
//mark all utxos as reserved
_ <- wallet.markUTXOsAsReserved(utxos)
newReservedUtxos <- wallet.listUtxos(TxoState.Reserved)
//make sure all utxos are reserved
_ = assert(newReservedUtxos.length == utxoCount.length)
blockHash <- bitcoind.generateToAddress(1, bitcoindAdr).map(_.head)
block <- bitcoind.getBlockRaw(blockHash)
_ <- wallet.processBlock(block)
broadcastSpentUtxo <- wallet.listUtxos(
TxoState.PendingConfirmationsSpent)
finalReservedUtxos <- wallet.listUtxos(TxoState.Reserved)
} yield {
assert(broadcastSpentUtxo.length == 1)
//make sure spendingTxId got set correctly
assert(broadcastSpentUtxo.head.spendingTxIdOpt.get == txIdBE)
//make sure no utxos get unreserved when processing the block
assert(finalReservedUtxos.length == newReservedUtxos.length)
}
}
} }

View file

@ -408,7 +408,6 @@ private[bitcoins] trait TransactionProcessing extends WalletLogger {
out out
.copyWithSpendingTxId(spendingTxId) .copyWithSpendingTxId(spendingTxId)
.copyWithState(state = BroadcastSpent) .copyWithState(state = BroadcastSpent)
Some(updated) Some(updated)
case TxoState.BroadcastSpent => case TxoState.BroadcastSpent =>
if (!out.spendingTxIdOpt.contains(spendingTxId)) { if (!out.spendingTxIdOpt.contains(spendingTxId)) {
@ -483,31 +482,18 @@ private[bitcoins] trait TransactionProcessing extends WalletLogger {
logger.debug( logger.debug(
s"Updating block_hash of txo=${transaction.txIdBE.hex}, new block hash=${blockHash.hex}") s"Updating block_hash of txo=${transaction.txIdBE.hex}, new block hash=${blockHash.hex}")
// If the utxo was marked reserved we want to update it to spent now
// since it has been included in a block
val unreservedTxo = foundTxo.state match {
case TxoState.Reserved =>
foundTxo.copyWithState(TxoState.PendingConfirmationsSpent)
case TxoState.PendingConfirmationsReceived |
TxoState.ConfirmedReceived |
TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent |
TxoState.DoesNotExist | TxoState.ImmatureCoinbase |
BroadcastReceived | BroadcastSpent =>
foundTxo
}
val updateTxDbF = insertTransaction(transaction, blockHashOpt) val updateTxDbF = insertTransaction(transaction, blockHashOpt)
// Update Txo State // Update Txo State
updateTxDbF.flatMap(_ => updateTxDbF.flatMap(_ =>
updateUtxoConfirmedState(unreservedTxo).flatMap { updateUtxoConfirmedState(foundTxo).flatMap {
case Some(txo) => case Some(txo) =>
logger.debug( logger.debug(
s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}") s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}")
Future.successful(txo) Future.successful(txo)
case None => case None =>
// State was not updated so we need to update it so it's block hash is in the database // State was not updated so we need to update it so it's block hash is in the database
spendingInfoDAO.update(unreservedTxo) spendingInfoDAO.update(foundTxo)
}) })
case None => case None =>
logger.debug( logger.debug(

View file

@ -315,6 +315,8 @@ private[wallet] trait UtxoHandling extends WalletLogger {
override def markUTXOsAsReserved( override def markUTXOsAsReserved(
utxos: Vector[SpendingInfoDb]): Future[Vector[SpendingInfoDb]] = { utxos: Vector[SpendingInfoDb]): Future[Vector[SpendingInfoDb]] = {
val outPoints = utxos.map(_.outPoint)
logger.info(s"Reserving utxos=$outPoints")
val updated = utxos.map(_.copyWithState(TxoState.Reserved)) val updated = utxos.map(_.copyWithState(TxoState.Reserved))
for { for {
utxos <- spendingInfoDAO.markAsReserved(updated) utxos <- spendingInfoDAO.markAsReserved(updated)
@ -327,7 +329,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
tx: Transaction): Future[Vector[SpendingInfoDb]] = { tx: Transaction): Future[Vector[SpendingInfoDb]] = {
for { for {
utxos <- spendingInfoDAO.findOutputsBeingSpent(tx) utxos <- spendingInfoDAO.findOutputsBeingSpent(tx)
reserved <- markUTXOsAsReserved(utxos.toVector) reserved <- markUTXOsAsReserved(utxos)
} yield reserved } yield reserved
} }