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
/** 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
/** 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 */
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
/** Means we have spent this utxo, and it is fully confirmed */
final case object ConfirmedSpent extends SpentState
val pendingConfStates: Set[TxoState] =
Set(TxoState.ImmatureCoinbase,
TxoState.PendingConfirmationsReceived,
TxoState.PendingConfirmationsSpent)
Set(BroadcastSpent,
BroadcastReceived,
ImmatureCoinbase,
PendingConfirmationsReceived,
PendingConfirmationsSpent)
val confirmedStates: Set[TxoState] =
Set(TxoState.ConfirmedReceived, TxoState.ConfirmedSpent)
val receivedStates: Set[TxoState] =
Set(PendingConfirmationsReceived, ConfirmedReceived)
Set(PendingConfirmationsReceived, ConfirmedReceived, BroadcastReceived)
val spentStates: Set[TxoState] =
Set(PendingConfirmationsSpent, TxoState.ConfirmedSpent, Reserved)
Set(PendingConfirmationsSpent,
TxoState.ConfirmedSpent,
Reserved,
BroadcastSpent)
val all: Vector[TxoState] = Vector(DoesNotExist,
ImmatureCoinbase,
PendingConfirmationsReceived,
ConfirmedReceived,
Reserved,
PendingConfirmationsSpent,
ConfirmedSpent)
val broadcastStates: Set[TxoState] = Set(BroadcastReceived, BroadcastSpent)
val all: Vector[TxoState] = Vector(
DoesNotExist,
ImmatureCoinbase,
BroadcastReceived,
PendingConfirmationsReceived,
ConfirmedReceived,
Reserved,
BroadcastSpent,
PendingConfirmationsSpent,
ConfirmedSpent
)
override def fromStringOpt(str: String): Option[TxoState] = {
all.find(state => str.toLowerCase() == state.toString.toLowerCase)

View File

@ -25,7 +25,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
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
for {
@ -35,7 +35,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions()
} yield {
assert(updatedCoins.forall(_.state == TxoState.PendingConfirmationsSpent))
assert(updatedCoins.forall(_.state == TxoState.BroadcastSpent))
assert(updatedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx))
@ -51,7 +51,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions()
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent))
_ = assert(updatedCoins.forall(_.state == BroadcastSpent))
_ = assert(!oldTransactions.map(_.transaction).contains(tx))
_ = assert(newTransactions.map(_.transaction).contains(tx))
@ -59,8 +59,8 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
hash <- bitcoind.getBestBlockHash
_ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates()
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block
@ -85,14 +85,14 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
Some(SatoshisPerByte.one))
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(coins.forall(_.state == PendingConfirmationsSpent))
_ = assert(coins.forall(_.state == BroadcastSpent))
_ = assert(coins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
rbf <- wallet.bumpFeeRBF(tx.txIdBE, SatoshisPerByte.fromLong(3))
_ <- wallet.processTransaction(rbf, None)
rbfCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(rbf)
} yield {
assert(rbfCoins.forall(_.state == PendingConfirmationsSpent))
assert(rbfCoins.forall(_.state == BroadcastSpent))
assert(rbfCoins.forall(_.spendingTxIdOpt.contains(rbf.txIdBE)))
}
}
@ -130,7 +130,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions()
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent))
_ = assert(updatedCoins.forall(_.state == BroadcastSpent))
_ = assert(!oldTransactions.map(_.transaction).contains(tx))
_ = assert(newTransactions.map(_.transaction).contains(tx))
@ -138,8 +138,8 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
hash <- bitcoind.getBestBlockHash
_ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates()
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block
@ -191,7 +191,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
} 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
for {
@ -210,9 +210,44 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
updatedCoin <-
wallet.spendingInfoDAO.findByScriptPubKey(addr.scriptPubKey)
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 {
assert(
updatedCoin.forall(_.state == TxoState.PendingConfirmationsReceived))
pendingCoins.forall(_.state == TxoState.PendingConfirmationsReceived))
assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx))
}
@ -376,5 +411,4 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
assert(newTransactions.map(_.transaction).contains(tx))
}
}
}

View File

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

View File

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

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.util.TimeUtil
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.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.wallet._
@ -267,10 +268,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
out: SpendingInfoDb,
spendingTxId: DoubleSha256DigestBE): Future[Option[SpendingInfoDb]] = {
out.state match {
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived =>
case ConfirmedReceived | PendingConfirmationsReceived |
BroadcastReceived =>
val updated =
out
.copyWithState(state = TxoState.PendingConfirmationsSpent)
.copyWithState(state = BroadcastSpent)
.copyWithSpendingTxId(spendingTxId)
val updatedF =
spendingInfoDAO.update(updated)
@ -278,7 +280,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
logger.debug(
s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}"))
updatedF.map(Some(_))
case TxoState.Reserved | TxoState.PendingConfirmationsSpent =>
case TxoState.Reserved | TxoState.PendingConfirmationsSpent |
BroadcastSpent =>
val updated =
out.copyWithSpendingTxId(spendingTxId)
val updatedF =
@ -345,7 +348,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
case TxoState.PendingConfirmationsReceived |
TxoState.ConfirmedReceived |
TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent |
TxoState.DoesNotExist | TxoState.ImmatureCoinbase =>
TxoState.DoesNotExist | TxoState.ImmatureCoinbase |
BroadcastReceived | BroadcastSpent =>
foundTxo
}
@ -377,7 +381,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
Seq[SpendingInfoDb]] = {
val stateF: Future[TxoState] = blockHashOpt match {
case None =>
Future.successful(TxoState.PendingConfirmationsReceived)
Future.successful(TxoState.BroadcastReceived)
case Some(blockHash) =>
chainQueryApi.getNumberOfConfirmations(blockHash).map {
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 blockHashAndDb = spendingInfoDbs.map { txo =>
val txToUse = txo.state match {
case _: ReceivedState | DoesNotExist | ImmatureCoinbase | Reserved =>
case _: ReceivedState | DoesNotExist | ImmatureCoinbase |
Reserved | BroadcastReceived =>
txo.txid
case PendingConfirmationsSpent | ConfirmedSpent =>
case PendingConfirmationsSpent | ConfirmedSpent | BroadcastSpent =>
txo.spendingTxIdOpt.get
}
(blockHashMap(txToUse), txo)
@ -143,14 +144,14 @@ private[wallet] trait UtxoHandling extends WalletLogger {
else
txo.copyWithState(TxoState.PendingConfirmationsReceived)
} else txo
case TxoState.PendingConfirmationsReceived =>
case TxoState.PendingConfirmationsReceived | BroadcastReceived =>
if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedReceived)
else txo
case TxoState.PendingConfirmationsSpent =>
else txo.copyWithState(PendingConfirmationsReceived)
case TxoState.PendingConfirmationsSpent | BroadcastSpent =>
if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedSpent)
else txo
else txo.copyWithState(PendingConfirmationsSpent)
case TxoState.Reserved =>
// We should keep the utxo as reserved so it is not used in
// a future transaction that it should not be in