Add Broadcast TxoState (#2735)

* Add broadcast TxoState

* Update scaladoc
This commit is contained in:
benthecarman 2021-03-18 14:16:53 -05:00 committed by GitHub
parent 1daba85ddf
commit 12bff309c2
6 changed files with 110 additions and 50 deletions

View File

@ -19,7 +19,12 @@ object TxoState extends StringFactory[TxoState] {
*/ */
final case object ImmatureCoinbase extends TxoState final case object ImmatureCoinbase extends TxoState
/** Means we have received funds to this utxo, but they are not confirmed */ /** Means we have received funds to this utxo, and they have not been confirmed in a block */
final case object BroadcastReceived extends ReceivedState
/** Means we have received funds to this utxo, and they have some confirmations but
* have not reached our confirmation threshold
*/
final case object PendingConfirmationsReceived extends ReceivedState final case object PendingConfirmationsReceived extends ReceivedState
/** Means we have received funds and they are fully confirmed for this utxo */ /** Means we have received funds and they are fully confirmed for this utxo */
@ -28,33 +33,49 @@ object TxoState extends StringFactory[TxoState] {
/** Means we have not spent this utxo yet, but will be used in a future transaction */ /** Means we have not spent this utxo yet, but will be used in a future transaction */
final case object Reserved extends SpentState final case object Reserved extends SpentState
/** Means we have spent this utxo, but it is not fully confirmed */ /** Means we have spent this utxo, and they have not been confirmed in a block */
final case object BroadcastSpent extends SpentState
/** Means we have spent this utxo, and they have some confirmations but
* have not reached our confirmation threshold
*/
final case object PendingConfirmationsSpent extends SpentState final case object PendingConfirmationsSpent extends SpentState
/** Means we have spent this utxo, and it is fully confirmed */ /** Means we have spent this utxo, and it is fully confirmed */
final case object ConfirmedSpent extends SpentState final case object ConfirmedSpent extends SpentState
val pendingConfStates: Set[TxoState] = val pendingConfStates: Set[TxoState] =
Set(TxoState.ImmatureCoinbase, Set(BroadcastSpent,
TxoState.PendingConfirmationsReceived, BroadcastReceived,
TxoState.PendingConfirmationsSpent) ImmatureCoinbase,
PendingConfirmationsReceived,
PendingConfirmationsSpent)
val confirmedStates: Set[TxoState] = val confirmedStates: Set[TxoState] =
Set(TxoState.ConfirmedReceived, TxoState.ConfirmedSpent) Set(TxoState.ConfirmedReceived, TxoState.ConfirmedSpent)
val receivedStates: Set[TxoState] = val receivedStates: Set[TxoState] =
Set(PendingConfirmationsReceived, ConfirmedReceived) Set(PendingConfirmationsReceived, ConfirmedReceived, BroadcastReceived)
val spentStates: Set[TxoState] = val spentStates: Set[TxoState] =
Set(PendingConfirmationsSpent, TxoState.ConfirmedSpent, Reserved) Set(PendingConfirmationsSpent,
TxoState.ConfirmedSpent,
Reserved,
BroadcastSpent)
val all: Vector[TxoState] = Vector(DoesNotExist, val broadcastStates: Set[TxoState] = Set(BroadcastReceived, BroadcastSpent)
ImmatureCoinbase,
PendingConfirmationsReceived, val all: Vector[TxoState] = Vector(
ConfirmedReceived, DoesNotExist,
Reserved, ImmatureCoinbase,
PendingConfirmationsSpent, BroadcastReceived,
ConfirmedSpent) PendingConfirmationsReceived,
ConfirmedReceived,
Reserved,
BroadcastSpent,
PendingConfirmationsSpent,
ConfirmedSpent
)
override def fromStringOpt(str: String): Option[TxoState] = { override def fromStringOpt(str: String): Option[TxoState] = {
all.find(state => str.toLowerCase() == state.toString.toLowerCase) all.find(state => str.toLowerCase() == state.toString.toLowerCase)

View File

@ -25,7 +25,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
withFundedWalletAndBitcoind(test, getBIP39PasswordOpt()) withFundedWalletAndBitcoind(test, getBIP39PasswordOpt())
} }
it should "track a utxo state change to pending spent" in { param => it should "track a utxo state change to broadcast spent" in { param =>
val WalletWithBitcoindRpc(wallet, _) = param val WalletWithBitcoindRpc(wallet, _) = param
for { for {
@ -35,7 +35,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions() newTransactions <- wallet.listTransactions()
} yield { } yield {
assert(updatedCoins.forall(_.state == TxoState.PendingConfirmationsSpent)) assert(updatedCoins.forall(_.state == TxoState.BroadcastSpent))
assert(updatedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE))) assert(updatedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
assert(!oldTransactions.map(_.transaction).contains(tx)) assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx)) assert(newTransactions.map(_.transaction).contains(tx))
@ -51,7 +51,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions() newTransactions <- wallet.listTransactions()
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent)) _ = assert(updatedCoins.forall(_.state == BroadcastSpent))
_ = assert(!oldTransactions.map(_.transaction).contains(tx)) _ = assert(!oldTransactions.map(_.transaction).contains(tx))
_ = assert(newTransactions.map(_.transaction).contains(tx)) _ = assert(newTransactions.map(_.transaction).contains(tx))
@ -59,8 +59,8 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
hash <- bitcoind.getBestBlockHash hash <- bitcoind.getBestBlockHash
_ <- wallet.processTransaction(tx, Some(hash)) _ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates() _ <- wallet.updateUtxoPendingStates()
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent)) _ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block // Put confirmations on top of the tx's block
@ -85,14 +85,14 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
Some(SatoshisPerByte.one)) Some(SatoshisPerByte.one))
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(coins.forall(_.state == PendingConfirmationsSpent)) _ = assert(coins.forall(_.state == BroadcastSpent))
_ = assert(coins.forall(_.spendingTxIdOpt.contains(tx.txIdBE))) _ = assert(coins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
rbf <- wallet.bumpFeeRBF(tx.txIdBE, SatoshisPerByte.fromLong(3)) rbf <- wallet.bumpFeeRBF(tx.txIdBE, SatoshisPerByte.fromLong(3))
_ <- wallet.processTransaction(rbf, None) _ <- wallet.processTransaction(rbf, None)
rbfCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(rbf) rbfCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(rbf)
} yield { } yield {
assert(rbfCoins.forall(_.state == PendingConfirmationsSpent)) assert(rbfCoins.forall(_.state == BroadcastSpent))
assert(rbfCoins.forall(_.spendingTxIdOpt.contains(rbf.txIdBE))) assert(rbfCoins.forall(_.spendingTxIdOpt.contains(rbf.txIdBE)))
} }
} }
@ -130,7 +130,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx) updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions() newTransactions <- wallet.listTransactions()
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent)) _ = assert(updatedCoins.forall(_.state == BroadcastSpent))
_ = assert(!oldTransactions.map(_.transaction).contains(tx)) _ = assert(!oldTransactions.map(_.transaction).contains(tx))
_ = assert(newTransactions.map(_.transaction).contains(tx)) _ = assert(newTransactions.map(_.transaction).contains(tx))
@ -138,8 +138,8 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
hash <- bitcoind.getBestBlockHash hash <- bitcoind.getBestBlockHash
_ <- wallet.processTransaction(tx, Some(hash)) _ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates() _ <- wallet.updateUtxoPendingStates()
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent)) _ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block // Put confirmations on top of the tx's block
@ -191,7 +191,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
} yield res } yield res
} }
it should "track a utxo state change to pending received" in { param => it should "track a utxo state change to broadcast received" in { param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param val WalletWithBitcoindRpc(wallet, bitcoind) = param
for { for {
@ -210,9 +210,44 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoin <- updatedCoin <-
wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey) wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey)
newTransactions <- wallet.listTransactions() newTransactions <- wallet.listTransactions()
} yield {
assert(updatedCoin.forall(_.state == TxoState.BroadcastReceived))
assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx))
}
}
it should "track a utxo state change to pending received" in { param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param
for {
oldTransactions <- wallet.listTransactions()
addr <- wallet.getNewAddress()
txId <- bitcoind.sendToAddress(addr, Satoshis(3000))
tx <- bitcoind.getRawTransactionRaw(txId)
_ <- wallet.processOurTransaction(transaction = tx,
feeRate = SatoshisPerByte(Satoshis(3)),
inputAmount = Satoshis(4000),
sentAmount = Satoshis(3000),
blockHashOpt = None,
newTags = Vector.empty)
updatedCoin <-
wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey)
newTransactions <- wallet.listTransactions()
_ = assert(updatedCoin.forall(_.state == TxoState.BroadcastReceived))
hash <- bitcoind.getNewAddress
.flatMap(bitcoind.generateToAddress(1, _))
.map(_.head)
_ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <-
wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey)
} yield { } yield {
assert( assert(
updatedCoin.forall(_.state == TxoState.PendingConfirmationsReceived)) pendingCoins.forall(_.state == TxoState.PendingConfirmationsReceived))
assert(!oldTransactions.map(_.transaction).contains(tx)) assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx)) assert(newTransactions.map(_.transaction).contains(tx))
} }
@ -376,5 +411,4 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
assert(newTransactions.map(_.transaction).contains(tx)) assert(newTransactions.map(_.transaction).contains(tx))
} }
} }
} }

View File

@ -333,7 +333,7 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
_ <- wallet.processTransaction(signedTx, None) _ <- wallet.processTransaction(signedTx, None)
newCoinbaseUtxos <- wallet.listUtxos(TxoState.ImmatureCoinbase) newCoinbaseUtxos <- wallet.listUtxos(TxoState.ImmatureCoinbase)
_ = assert(newCoinbaseUtxos.isEmpty) _ = assert(newCoinbaseUtxos.isEmpty)
spentUtxos <- wallet.listUtxos(TxoState.PendingConfirmationsSpent) spentUtxos <- wallet.listUtxos(TxoState.BroadcastSpent)
_ = assert(spentUtxos.size == 1) _ = assert(spentUtxos.size == 1)
// Assert spending tx valid to bitcoind // Assert spending tx valid to bitcoind

View File

@ -31,10 +31,7 @@ import org.bitcoins.core.wallet.keymanagement.{
KeyManagerParams, KeyManagerParams,
KeyManagerUnlockError KeyManagerUnlockError
} }
import org.bitcoins.core.wallet.utxo.TxoState.{ import org.bitcoins.core.wallet.utxo.TxoState._
ConfirmedReceived,
PendingConfirmationsReceived
}
import org.bitcoins.core.wallet.utxo._ import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._ import org.bitcoins.crypto._
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager} import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
@ -267,11 +264,11 @@ abstract class Wallet
.map { txo => .map { txo =>
txo.state match { txo.state match {
case TxoState.PendingConfirmationsReceived | case TxoState.PendingConfirmationsReceived |
TxoState.ConfirmedReceived => TxoState.ConfirmedReceived | TxoState.BroadcastReceived =>
txo.output.value txo.output.value
case TxoState.Reserved | TxoState.PendingConfirmationsSpent | case TxoState.Reserved | TxoState.PendingConfirmationsSpent |
TxoState.ConfirmedSpent | TxoState.DoesNotExist | TxoState.ConfirmedSpent | TxoState.BroadcastSpent |
TxoState.ImmatureCoinbase => TxoState.DoesNotExist | TxoState.ImmatureCoinbase =>
CurrencyUnits.zero CurrencyUnits.zero
} }
} }
@ -307,10 +304,12 @@ abstract class Wallet
} }
override def getUnconfirmedBalance(): Future[CurrencyUnit] = { override def getUnconfirmedBalance(): Future[CurrencyUnit] = {
filterThenSum(_.state == PendingConfirmationsReceived).map { balance => filterThenSum(utxo =>
logger.trace(s"Unconfirmed balance=${balance.satoshis}") utxo.state == PendingConfirmationsReceived || utxo.state == BroadcastReceived)
balance .map { balance =>
} logger.trace(s"Unconfirmed balance=${balance.satoshis}")
balance
}
} }
override def getUnconfirmedBalance( override def getUnconfirmedBalance(
@ -320,7 +319,7 @@ abstract class Wallet
} yield { } yield {
val confirmedUtxos = allUnspent.filter { utxo => val confirmedUtxos = allUnspent.filter { utxo =>
HDAccount.isSameAccount(utxo.privKeyPath.path, account) && HDAccount.isSameAccount(utxo.privKeyPath.path, account) &&
utxo.state == PendingConfirmationsReceived utxo.state == PendingConfirmationsReceived || utxo.state == BroadcastReceived
} }
confirmedUtxos.foldLeft(CurrencyUnits.zero)(_ + _.output.value) confirmedUtxos.foldLeft(CurrencyUnits.zero)(_ + _.output.value)
} }
@ -328,7 +327,8 @@ abstract class Wallet
override def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit] = { override def getUnconfirmedBalance(tag: AddressTag): Future[CurrencyUnit] = {
spendingInfoDAO.findAllUnspentForTag(tag).map { allUnspent => spendingInfoDAO.findAllUnspentForTag(tag).map { allUnspent =>
val confirmed = allUnspent.filter(_.state == PendingConfirmationsReceived) val confirmed = allUnspent.filter(utxo =>
utxo.state == PendingConfirmationsReceived || utxo.state == BroadcastReceived)
confirmed.foldLeft(CurrencyUnits.zero)(_ + _.output.value) confirmed.foldLeft(CurrencyUnits.zero)(_ + _.output.value)
} }
} }

View File

@ -10,6 +10,7 @@ import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput} import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.util.TimeUtil import org.bitcoins.core.util.TimeUtil
import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.TxoState._
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState} import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE} import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.wallet._ import org.bitcoins.wallet._
@ -267,10 +268,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
out: SpendingInfoDb, out: SpendingInfoDb,
spendingTxId: DoubleSha256DigestBE): Future[Option[SpendingInfoDb]] = { spendingTxId: DoubleSha256DigestBE): Future[Option[SpendingInfoDb]] = {
out.state match { out.state match {
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived => case ConfirmedReceived | PendingConfirmationsReceived |
BroadcastReceived =>
val updated = val updated =
out out
.copyWithState(state = TxoState.PendingConfirmationsSpent) .copyWithState(state = BroadcastSpent)
.copyWithSpendingTxId(spendingTxId) .copyWithSpendingTxId(spendingTxId)
val updatedF = val updatedF =
spendingInfoDAO.update(updated) spendingInfoDAO.update(updated)
@ -278,7 +280,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
logger.debug( logger.debug(
s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}")) s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}"))
updatedF.map(Some(_)) updatedF.map(Some(_))
case TxoState.Reserved | TxoState.PendingConfirmationsSpent => case TxoState.Reserved | TxoState.PendingConfirmationsSpent |
BroadcastSpent =>
val updated = val updated =
out.copyWithSpendingTxId(spendingTxId) out.copyWithSpendingTxId(spendingTxId)
val updatedF = val updatedF =
@ -345,7 +348,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
case TxoState.PendingConfirmationsReceived | case TxoState.PendingConfirmationsReceived |
TxoState.ConfirmedReceived | TxoState.ConfirmedReceived |
TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent | TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent |
TxoState.DoesNotExist | TxoState.ImmatureCoinbase => TxoState.DoesNotExist | TxoState.ImmatureCoinbase |
BroadcastReceived | BroadcastSpent =>
foundTxo foundTxo
} }
@ -377,7 +381,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
Seq[SpendingInfoDb]] = { Seq[SpendingInfoDb]] = {
val stateF: Future[TxoState] = blockHashOpt match { val stateF: Future[TxoState] = blockHashOpt match {
case None => case None =>
Future.successful(TxoState.PendingConfirmationsReceived) Future.successful(TxoState.BroadcastReceived)
case Some(blockHash) => case Some(blockHash) =>
chainQueryApi.getNumberOfConfirmations(blockHash).map { chainQueryApi.getNumberOfConfirmations(blockHash).map {
case None => case None =>

View File

@ -116,9 +116,10 @@ private[wallet] trait UtxoHandling extends WalletLogger {
val blockHashMap = txDbs.map(db => db.txIdBE -> db.blockHashOpt).toMap val blockHashMap = txDbs.map(db => db.txIdBE -> db.blockHashOpt).toMap
val blockHashAndDb = spendingInfoDbs.map { txo => val blockHashAndDb = spendingInfoDbs.map { txo =>
val txToUse = txo.state match { val txToUse = txo.state match {
case _: ReceivedState | DoesNotExist | ImmatureCoinbase | Reserved => case _: ReceivedState | DoesNotExist | ImmatureCoinbase |
Reserved | BroadcastReceived =>
txo.txid txo.txid
case PendingConfirmationsSpent | ConfirmedSpent => case PendingConfirmationsSpent | ConfirmedSpent | BroadcastSpent =>
txo.spendingTxIdOpt.get txo.spendingTxIdOpt.get
} }
(blockHashMap(txToUse), txo) (blockHashMap(txToUse), txo)
@ -143,14 +144,14 @@ private[wallet] trait UtxoHandling extends WalletLogger {
else else
txo.copyWithState(TxoState.PendingConfirmationsReceived) txo.copyWithState(TxoState.PendingConfirmationsReceived)
} else txo } else txo
case TxoState.PendingConfirmationsReceived => case TxoState.PendingConfirmationsReceived | BroadcastReceived =>
if (confs >= walletConfig.requiredConfirmations) if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedReceived) txo.copyWithState(TxoState.ConfirmedReceived)
else txo else txo.copyWithState(PendingConfirmationsReceived)
case TxoState.PendingConfirmationsSpent => case TxoState.PendingConfirmationsSpent | BroadcastSpent =>
if (confs >= walletConfig.requiredConfirmations) if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedSpent) txo.copyWithState(TxoState.ConfirmedSpent)
else txo else txo.copyWithState(PendingConfirmationsSpent)
case TxoState.Reserved => case TxoState.Reserved =>
// We should keep the utxo as reserved so it is not used in // We should keep the utxo as reserved so it is not used in
// a future transaction that it should not be in // a future transaction that it should not be in