Move blockhash to tx table from spending info table (#2744)

* Move blockhash to tx table from spending info table

* Add test, fix spending tx id col name

* Scaladocs, add test

* Add more unit test

* Make id not comparable in process tx test

* Fix tests

* attempt to fix

* Add mempool comment to scaladoc

* Deparallelize process inputs & outputs
This commit is contained in:
benthecarman 2021-03-16 10:05:29 -05:00 committed by GitHub
parent 7e23eecb20
commit 9494eec1b8
28 changed files with 622 additions and 314 deletions

View File

@ -374,7 +374,6 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
EmptyScriptWitness,
DoubleSha256DigestBE.empty,
TxoState.PendingConfirmationsSpent,
None,
None
)
@ -775,7 +774,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
val tx = Transaction(
"020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000")
val txDb = TransactionDbHelper.fromTransaction(tx)
val txDb = TransactionDbHelper.fromTransaction(tx, None)
(mockWalletApi
.findTransaction(_: DoubleSha256DigestBE))

View File

@ -58,17 +58,17 @@ object BitcoindRpcBackendUtil extends BitcoinSLogger {
_ <- walletStateOpt match {
case None =>
for {
utxos <- wallet.listUtxos()
lastConfirmedOpt = utxos.filter(_.blockHash.isDefined).lastOption
txDbs <- wallet.listTransactions()
lastConfirmedOpt = txDbs.filter(_.blockHashOpt.isDefined).lastOption
_ <- lastConfirmedOpt match {
case None => Future.unit
case Some(utxo) =>
case Some(txDb) =>
for {
heightOpt <- bitcoind.getBlockHeight(utxo.blockHash.get)
heightOpt <- bitcoind.getBlockHeight(txDb.blockHashOpt.get)
_ <- heightOpt match {
case Some(height) =>
logger.info(
s"Last utxo occurred at block $height, syncing from there")
s"Last tx occurred at block $height, syncing from there")
doSync(height, bitcoindHeight)
case None => Future.unit
}

View File

@ -2,13 +2,7 @@ package org.bitcoins.core.api.wallet.db
import org.bitcoins.core.api.db.DbRowAutoInc
import org.bitcoins.core.api.keymanager.BIP39KeyManagerApi
import org.bitcoins.core.hd.{
HDChainType,
HDPath,
LegacyHDPath,
NestedSegWitHDPath,
SegWitHDPath
}
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.script.{ScriptPubKey, ScriptWitness}
import org.bitcoins.core.protocol.transaction.{
Transaction,
@ -34,8 +28,8 @@ case class SegwitV0SpendingInfo(
scriptWitness: ScriptWitness,
txid: DoubleSha256DigestBE,
state: TxoState,
id: Option[Long] = None,
blockHash: Option[DoubleSha256DigestBE]
spendingTxIdOpt: Option[DoubleSha256DigestBE],
id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
@ -49,10 +43,10 @@ case class SegwitV0SpendingInfo(
override def copyWithId(id: Long): SegwitV0SpendingInfo =
copy(id = Some(id))
/** Updates the `blockHash` field */
override def copyWithBlockHash(
blockHash: DoubleSha256DigestBE): SegwitV0SpendingInfo =
copy(blockHash = Some(blockHash))
/** Updates the `spendingTxId` field */
override def copyWithSpendingTxId(
txId: DoubleSha256DigestBE): SegwitV0SpendingInfo =
copy(spendingTxIdOpt = Some(txId))
}
/** DB representation of a legacy UTXO
@ -63,10 +57,11 @@ case class LegacySpendingInfo(
privKeyPath: LegacyHDPath,
state: TxoState,
txid: DoubleSha256DigestBE,
blockHash: Option[DoubleSha256DigestBE],
spendingTxIdOpt: Option[DoubleSha256DigestBE],
id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath
@ -78,9 +73,10 @@ case class LegacySpendingInfo(
override def copyWithState(state: TxoState): LegacySpendingInfo =
copy(state = state)
override def copyWithBlockHash(
blockHash: DoubleSha256DigestBE): LegacySpendingInfo =
copy(blockHash = Some(blockHash))
/** Updates the `spendingTxId` field */
override def copyWithSpendingTxId(
txId: DoubleSha256DigestBE): LegacySpendingInfo =
copy(spendingTxIdOpt = Some(txId))
}
/** DB representation of a nested segwit V0
@ -94,7 +90,7 @@ case class NestedSegwitV0SpendingInfo(
scriptWitness: ScriptWitness,
txid: DoubleSha256DigestBE,
state: TxoState,
blockHash: Option[DoubleSha256DigestBE],
spendingTxIdOpt: Option[DoubleSha256DigestBE],
id: Option[Long] = None
) extends SpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = Some(redeemScript)
@ -109,10 +105,10 @@ case class NestedSegwitV0SpendingInfo(
override def copyWithId(id: Long): NestedSegwitV0SpendingInfo =
copy(id = Some(id))
/** Updates the `blockHash` field */
override def copyWithBlockHash(
blockHash: DoubleSha256DigestBE): NestedSegwitV0SpendingInfo =
copy(blockHash = Some(blockHash))
/** Updates the `spendingTxId` field */
override def copyWithSpendingTxId(
txId: DoubleSha256DigestBE): NestedSegwitV0SpendingInfo =
copy(spendingTxIdOpt = Some(txId))
}
/** The database level representation of a UTXO.
@ -124,16 +120,6 @@ case class NestedSegwitV0SpendingInfo(
*/
sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
state match {
case TxoState.ConfirmedSpent | TxoState.ConfirmedReceived |
TxoState.ImmatureCoinbase =>
require(blockHash.isDefined,
"Transaction cannot be confirmed without a blockHash")
case TxoState.DoesNotExist | TxoState.PendingConfirmationsSpent |
TxoState.PendingConfirmationsReceived | TxoState.Reserved =>
()
}
protected type PathType <: HDPath
/** This type is here to ensure copyWithSpent returns the same
@ -158,8 +144,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
/** The TXID of the transaction this output was received in */
def txid: DoubleSha256DigestBE
/** The hash of the block in which the transaction was included */
def blockHash: Option[DoubleSha256DigestBE]
/** TxId of the transaction that this output was spent by */
def spendingTxIdOpt: Option[DoubleSha256DigestBE]
/** Converts the UTXO to the canonical `txid:vout` format */
def toHumanReadableString: String =
@ -168,8 +154,8 @@ sealed trait SpendingInfoDb extends DbRowAutoInc[SpendingInfoDb] {
/** Updates the `spent` field */
def copyWithState(state: TxoState): SpendingInfoType
/** Updates the `blockHash` field */
def copyWithBlockHash(blockHash: DoubleSha256DigestBE): SpendingInfoType
/** Updates the `spendingTxId` field */
def copyWithSpendingTxId(txId: DoubleSha256DigestBE): SpendingInfoType
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO

View File

@ -30,7 +30,8 @@ case class TransactionDb(
totalOutput: CurrencyUnit,
numInputs: Int,
numOutputs: Int,
lockTime: UInt32)
lockTime: UInt32,
blockHashOpt: Option[DoubleSha256DigestBE])
extends TxDB {
require(unsignedTx.inputs.forall(_.scriptSignature == EmptyScriptSignature),
s"All ScriptSignatures must be empty, got $unsignedTx")
@ -42,7 +43,9 @@ case class TransactionDb(
object TransactionDbHelper {
def fromTransaction(tx: Transaction): TransactionDb = {
def fromTransaction(
tx: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): TransactionDb = {
val (unsignedTx, wTxIdBEOpt) = tx match {
case btx: NonWitnessTransaction =>
val unsignedInputs = btx.inputs.map(input =>
@ -76,6 +79,7 @@ object TransactionDbHelper {
totalOutput,
tx.inputs.size,
tx.outputs.size,
tx.lockTime)
tx.lockTime,
blockHashOpt)
}
}

View File

@ -29,7 +29,7 @@ case class UTXORecord(
path: HDPath,
redeemScript: Option[ScriptPubKey], // RedeemScript
scriptWitness: Option[ScriptWitness],
blockHash: Option[DoubleSha256DigestBE], // block hash
spendingTxIdOpt: Option[DoubleSha256DigestBE],
id: Option[Long] = None
) extends DbRowAutoInc[UTXORecord] {
override def copyWithId(id: Long): UTXORecord = copy(id = Option(id))
@ -47,7 +47,7 @@ case class UTXORecord(
id = id,
state = state,
txid = txid,
blockHash = blockHash
spendingTxIdOpt = spendingTxIdOpt
)
case (path: LegacyHDPath, None, None) =>
@ -56,20 +56,22 @@ case class UTXORecord(
privKeyPath = path,
id = id,
state = state,
txid = txid,
blockHash = blockHash)
spendingTxIdOpt = spendingTxIdOpt,
txid = txid)
case (path: NestedSegWitHDPath, Some(redeemScript), Some(scriptWitness))
if WitnessScriptPubKey.isValidAsm(redeemScript.asm) =>
NestedSegwitV0SpendingInfo(outpoint,
TransactionOutput(value, scriptPubKey),
path,
redeemScript,
scriptWitness,
txid,
state,
blockHash,
id)
NestedSegwitV0SpendingInfo(
outPoint = outpoint,
output = TransactionOutput(value, scriptPubKey),
privKeyPath = path,
redeemScript = redeemScript,
scriptWitness = scriptWitness,
txid = txid,
state = state,
spendingTxIdOpt = spendingTxIdOpt,
id = id
)
case _ =>
throw new IllegalArgumentException(
@ -91,7 +93,7 @@ object UTXORecord {
spendingInfoDb.privKeyPath,
spendingInfoDb.redeemScriptOpt, // ReedemScript
spendingInfoDb.scriptWitnessOpt,
spendingInfoDb.blockHash, // block hash
spendingInfoDb.spendingTxIdOpt,
spendingInfoDb.id
)
}

View File

@ -35,7 +35,8 @@ object TxoState extends StringFactory[TxoState] {
final case object ConfirmedSpent extends SpentState
val pendingConfStates: Set[TxoState] =
Set(TxoState.PendingConfirmationsReceived,
Set(TxoState.ImmatureCoinbase,
TxoState.PendingConfirmationsReceived,
TxoState.PendingConfirmationsSpent)
val confirmedStates: Set[TxoState] =

View File

@ -74,13 +74,13 @@ class DbManagementTest extends BitcoinSAsyncTest with EmbeddedPg {
val result = walletDbManagement.migrate()
walletAppConfig.driver match {
case SQLite =>
val expected = 9
val expected = 10
assert(result == expected)
val flywayInfo = walletDbManagement.info()
assert(flywayInfo.applied().length == expected)
assert(flywayInfo.pending().length == 0)
case PostgreSQL =>
val expected = 7
val expected = 8
assert(result == expected)
val flywayInfo = walletDbManagement.info()

View File

@ -9,7 +9,7 @@ import org.bitcoins.core.api.feeprovider.FeeRateApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.currency._
import org.bitcoins.core.gcs.BlockFilter
import org.bitcoins.core.protocol.BlockStamp
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee._
@ -66,6 +66,9 @@ trait BitcoinSWalletTest extends BitcoinSFixture with EmbeddedPg {
def nodeApi: NodeApi = MockNodeApi
val testAddr: BitcoinAddress =
BitcoinAddress.fromString("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq")
val legacyWalletConf: Config =
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = legacy")

View File

@ -1,6 +1,5 @@
package org.bitcoins.testkit.wallet
import org.bitcoins.core.api.wallet.db
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto._
@ -125,14 +124,14 @@ object WalletTestUtil {
TransactionOutput(1.bitcoin, spk)
val scriptWitness = randomScriptWitness
val privkeyPath = WalletTestUtil.sampleSegwitPath
db.SegwitV0SpendingInfo(
SegwitV0SpendingInfo(
state = randomState,
txid = randomTXID,
outPoint = outpoint,
output = output,
privKeyPath = privkeyPath,
scriptWitness = scriptWitness,
blockHash = Some(randomBlockHash)
spendingTxIdOpt = None
)
}
@ -142,12 +141,12 @@ object WalletTestUtil {
val output =
TransactionOutput(1.bitcoin, spk)
val privKeyPath = WalletTestUtil.sampleLegacyPath
db.LegacySpendingInfo(state = randomState,
txid = randomTXID,
outPoint = outpoint,
output = output,
privKeyPath = privKeyPath,
blockHash = Some(randomBlockHash))
LegacySpendingInfo(state = randomState,
txid = randomTXID,
outPoint = outpoint,
output = output,
privKeyPath = privKeyPath,
spendingTxIdOpt = None)
}
def sampleNestedSegwitUTXO(
@ -158,7 +157,7 @@ object WalletTestUtil {
TransactionOutput(1.bitcoin, P2SHScriptPubKey(wpkh))
val scriptWitness = randomScriptWitness
val privkeyPath = WalletTestUtil.sampleNestedSegwitPath
db.NestedSegwitV0SpendingInfo(
NestedSegwitV0SpendingInfo(
state = randomState,
txid = randomTXID,
outPoint = outpoint,
@ -166,7 +165,7 @@ object WalletTestUtil {
privKeyPath = privkeyPath,
redeemScript = wpkh,
scriptWitness = scriptWitness,
blockHash = Some(randomBlockHash)
spendingTxIdOpt = None
)
}
@ -201,7 +200,8 @@ object WalletTestUtil {
totalOutput = Satoshis.zero,
numInputs = 1,
numOutputs = 1,
lockTime = UInt32.zero
lockTime = UInt32.zero,
blockHashOpt = Some(randomBlockHash)
)
val incomingDb = IncomingTransactionDb(utxo.txid, utxo.output.value)
for {

View File

@ -41,7 +41,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
output = TransactionOutput(10.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
blockHash = None
spendingTxIdOpt = None
)
val utxo2 = SegwitV0SpendingInfo(
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
@ -51,7 +51,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
output = TransactionOutput(90.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
blockHash = None
spendingTxIdOpt = None
)
val utxo3 = SegwitV0SpendingInfo(
txid = CryptoGenerators.doubleSha256Digest.sampleSome.flip,
@ -61,7 +61,7 @@ class CoinSelectorTest extends BitcoinSWalletTest {
output = TransactionOutput(20.sats, ScriptPubKey.empty),
privKeyPath = WalletTestUtil.sampleSegwitPath,
scriptWitness = WitnessGenerators.scriptWitness.sampleSome,
blockHash = None
spendingTxIdOpt = None
)
test(CoinSelectionFixture(output, feeRate, utxo1, utxo2, utxo3))

View File

@ -36,11 +36,13 @@ class ProcessBlockTest extends BitcoinSWalletTest {
height <- bitcoind.getBlockCount
bestHash <- bitcoind.getBestBlockHash
syncHeightOpt <- wallet.getSyncDescriptorOpt()
txDbOpt <- wallet.transactionDAO.findByTxId(txId)
} yield {
assert(txDbOpt.isDefined)
assert(txDbOpt.get.blockHashOpt.contains(hash))
assert(utxos.size == 1)
assert(utxos.head.output.scriptPubKey == addr.scriptPubKey)
assert(utxos.head.output.value == 1.bitcoin)
assert(utxos.head.blockHash.contains(hash))
assert(utxos.head.txid == txId)
assert(syncHeightOpt.contains(SyncHeightDescriptor(bestHash, height)))

View File

@ -2,9 +2,9 @@ package org.bitcoins.wallet
import org.bitcoins.core.api.wallet.WalletApi
import org.bitcoins.core.currency._
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkitcore.Implicits._
import org.bitcoins.testkitcore.gen.TransactionGenerators
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.scalatest.compatible.Assertion
@ -36,7 +36,12 @@ class ProcessTransactionTest extends BitcoinSWalletTest {
} yield {
assert(oldConfirmed == newConfirmed)
assert(oldUnconfirmed == newUnconfirmed)
assert(oldUtxos == newUtxos)
// make utxos comparable
val comparableOldUtxos =
oldUtxos.map(_.copyWithId(0)).sortBy(_.outPoint.hex)
val comparableNewUtxos =
newUtxos.map(_.copyWithId(0)).sortBy(_.outPoint.hex)
assert(comparableOldUtxos == comparableNewUtxos)
assert(oldTransactions == newTransactions)
}

View File

@ -184,10 +184,13 @@ class RescanHandlingTest extends BitcoinSWalletTest {
newTxWallet <- newTxWalletF
account <- newTxWallet.getDefaultAccount()
blocks <-
txIds <-
newTxWallet.spendingInfoDAO
.findAllForAccount(account.hdAccount)
.map(_.flatMap(_.blockHash).distinct)
.map(_.map(_.txid))
blocks <- newTxWallet.transactionDAO
.findByTxIdBEs(txIds)
.map(_.flatMap(_.blockHashOpt))
_ <- newTxWallet.clearAllUtxosAndAddresses()
scriptPubKeys <-
@ -265,9 +268,11 @@ class RescanHandlingTest extends BitcoinSWalletTest {
val utxosF = wallet.listUtxos()
val oldestHeightF = for {
utxos <- utxosF
blockhashes = utxos.map(_.blockHash)
blockhashes <- wallet.transactionDAO
.findByTxIdBEs(utxos.map(_.txid))
.map(_.flatMap(_.blockHashOpt))
heights <- FutureUtil.sequentially(blockhashes) { hash =>
wallet.chainQueryApi.getBlockHeight(hash.get)
wallet.chainQueryApi.getBlockHeight(hash)
}
} yield heights.min.get

View File

@ -0,0 +1,57 @@
package org.bitcoins.wallet
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.wallet.utxo.TxoState._
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkit.wallet.WalletTestUtil._
import org.scalatest.FutureOutcome
class UTXOHandlingTest extends BitcoinSWalletTest {
behavior of "UTXOHandling"
override type FixtureParam = Wallet
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withNewWallet(test, getBIP39PasswordOpt())
}
it must "correctly update txo state based on confirmations" in { wallet =>
val utxo = sampleSegwitUTXO(EmptyScriptPubKey)
val requiredConfs = 6
assert(wallet.walletConfig.requiredConfirmations == requiredConfs)
val immatureCoinbase = utxo.copyWithState(ImmatureCoinbase)
val pendingConfReceived = utxo.copyWithState(PendingConfirmationsReceived)
val pendingConfSpent = utxo.copyWithState(PendingConfirmationsSpent)
val confReceived = utxo.copyWithState(ConfirmedReceived)
val confSpent = utxo.copyWithState(ConfirmedSpent)
val reserved = utxo.copyWithState(Reserved)
val dne = utxo.copyWithState(DoesNotExist)
assert(wallet.updateTxoWithConfs(reserved, 1) == reserved)
assert(wallet.updateTxoWithConfs(immatureCoinbase, 10) == immatureCoinbase)
assert(wallet.updateTxoWithConfs(immatureCoinbase, 101) == confReceived)
assert(
wallet.updateTxoWithConfs(pendingConfReceived, 1) == pendingConfReceived)
assert(
wallet.updateTxoWithConfs(pendingConfReceived,
requiredConfs) == confReceived)
assert(wallet.updateTxoWithConfs(pendingConfSpent, 1) == pendingConfSpent)
assert(
wallet.updateTxoWithConfs(pendingConfSpent, requiredConfs) == confSpent)
assert(wallet.updateTxoWithConfs(dne, 1) == dne)
assert(wallet.updateTxoWithConfs(dne, requiredConfs) == dne)
assert(wallet.updateTxoWithConfs(confSpent, 1) == confSpent)
assert(wallet.updateTxoWithConfs(confSpent, requiredConfs) == confSpent)
assert(wallet.updateTxoWithConfs(confReceived, 1) == confReceived)
assert(
wallet.updateTxoWithConfs(confReceived, requiredConfs) == confReceived)
}
}

View File

@ -1,9 +1,9 @@
package org.bitcoins.wallet
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.{EmptyScriptPubKey, P2PKHScriptPubKey}
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.core.wallet.utxo.TxoState._
@ -21,10 +21,6 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
override type FixtureParam = WalletWithBitcoind
val testAddr: BitcoinAddress =
BitcoinAddress
.fromString("bcrt1qlhctylgvdsvaanv539rg7hyn0sjkdm23y70kgq")
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withFundedWalletAndBitcoind(test, getBIP39PasswordOpt())
}
@ -40,6 +36,7 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
newTransactions <- wallet.listTransactions()
} yield {
assert(updatedCoins.forall(_.state == TxoState.PendingConfirmationsSpent))
assert(updatedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
assert(!oldTransactions.map(_.transaction).contains(tx))
assert(newTransactions.map(_.transaction).contains(tx))
}
@ -60,8 +57,11 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
// Give tx a fake hash so it can appear as it's in a block
hash <- bitcoind.getBestBlockHash
_ <- wallet.spendingInfoDAO.upsertAllSpendingInfoDb(
updatedCoins.map(_.copyWithBlockHash(hash)).toVector)
_ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates()
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block
_ <- bitcoind.getNewAddress.flatMap(
@ -70,10 +70,128 @@ class UTXOLifeCycleTest extends BitcoinSWalletTest {
// Need to call this to actually update the state, normally a node callback would do this
_ <- wallet.updateUtxoPendingStates()
confirmedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
} yield assert(confirmedCoins.forall(_.state == ConfirmedSpent))
} yield {
assert(confirmedCoins.forall(_.state == ConfirmedSpent))
assert(confirmedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
}
}
it should "track a utxo state change to pending recieved" in { param =>
it should "handle an RBF transaction on unconfirmed coins" in { param =>
val WalletWithBitcoindRpc(wallet, _) = param
for {
tx <- wallet.sendToAddress(testAddr,
Satoshis(3000),
Some(SatoshisPerByte.one))
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ = assert(coins.forall(_.state == PendingConfirmationsSpent))
_ = 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(_.spendingTxIdOpt.contains(rbf.txIdBE)))
}
}
it should "handle attempting to spend an immature coinbase" in { param =>
val WalletWithBitcoindRpc(wallet, _) = param
for {
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
updatedCoins = coins.map(_.copyWithState(TxoState.ImmatureCoinbase))
_ <- wallet.spendingInfoDAO.updateAllSpendingInfoDb(updatedCoins.toVector)
// Create tx to spend immature coinbase utxos
newTx = {
val inputs = coins.map { db =>
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
}
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
}
res <- recoverToSucceededIf[RuntimeException](
wallet.processTransaction(newTx, None))
} yield res
}
it should "handle processing a new spending tx for a spent utxo" in { param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param
for {
oldTransactions <- wallet.listTransactions()
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
updatedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
newTransactions <- wallet.listTransactions()
_ = assert(updatedCoins.forall(_.state == PendingConfirmationsSpent))
_ = assert(!oldTransactions.map(_.transaction).contains(tx))
_ = assert(newTransactions.map(_.transaction).contains(tx))
// Give tx a fake hash so it can appear as it's in a block
hash <- bitcoind.getBestBlockHash
_ <- wallet.processTransaction(tx, Some(hash))
pendingCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
_ <- wallet.updateUtxoPendingStates()
_ = assert(pendingCoins.forall(_.state == PendingConfirmationsSpent))
// Put confirmations on top of the tx's block
_ <- bitcoind.getNewAddress.flatMap(
bitcoind.generateToAddress(wallet.walletConfig.requiredConfirmations,
_))
// Need to call this to actually update the state, normally a node callback would do this
_ <- wallet.updateUtxoPendingStates()
confirmedCoins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
// Assert tx is confirmed
_ = assert(confirmedCoins.forall(_.state == ConfirmedSpent))
_ = assert(confirmedCoins.forall(_.spendingTxIdOpt.contains(tx.txIdBE)))
// Create tx to spend same utxos
newTx = {
val inputs = updatedCoins.map { db =>
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
}
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
}
res <- recoverToSucceededIf[RuntimeException](
wallet.processTransaction(newTx, None))
} yield res
}
it should "handle processing a new spending tx for a DNE utxo" in { param =>
val WalletWithBitcoindRpc(wallet, _) = param
for {
tx <- wallet.sendToAddress(testAddr, Satoshis(3000), None)
coins <- wallet.spendingInfoDAO.findOutputsBeingSpent(tx)
dneCoins = coins.map(_.copyWithState(TxoState.DoesNotExist))
_ <- wallet.spendingInfoDAO.updateAllSpendingInfoDb(dneCoins.toVector)
// Create tx to spend dne utxos
newTx = {
val inputs = coins.map { db =>
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
}
BaseTransaction(Int32.zero, inputs, Vector.empty, UInt32.zero)
}
res <- recoverToSucceededIf[RuntimeException](
wallet.processTransaction(newTx, None))
} yield res
}
it should "track a utxo state change to pending received" in { param =>
val WalletWithBitcoindRpc(wallet, bitcoind) = param
for {

View File

@ -2,15 +2,15 @@ package org.bitcoins.wallet
import org.bitcoins.core.currency._
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.script.EmptyScriptSignature
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.TxoState
import org.bitcoins.rpc.BitcoindException.InvalidAddressOrKey
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.RandomFeeProvider
import org.bitcoins.testkit.wallet.{
BitcoinSWalletTest,
WalletTestUtil,
WalletWithBitcoind,
WalletWithBitcoindRpc
}
import org.bitcoins.testkit.wallet._
import org.scalatest.FutureOutcome
class WalletIntegrationTest extends BitcoinSWalletTest {
@ -192,7 +192,10 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
_ <- bitcoind.getNewAddress.flatMap(bitcoind.generateToAddress(6, _))
replacementInfo <- bitcoind.getRawTransaction(replacementTx.txIdBE)
utxos <- wallet.spendingInfoDAO.findOutputsBeingSpent(replacementTx)
} yield {
assert(utxos.forall(_.spendingTxIdOpt.contains(replacementTx.txIdBE)))
// Check correct one was confirmed
assert(replacementInfo.blockhash.isDefined)
}
@ -271,4 +274,76 @@ class WalletIntegrationTest extends BitcoinSWalletTest {
walletBal2 <- wallet.getBalance()
} yield assert(walletBal1 > walletBal2)
}
it should "correctly handle spending coinbase utxos" in {
walletWithBitcoind =>
val WalletWithBitcoindRpc(wallet, bitcoind) = walletWithBitcoind
val amountToSend = Bitcoins(49.99)
for {
// Mine to wallet
addr <- wallet.getNewAddress()
hash <- bitcoind.generateToAddress(1, addr).map(_.head)
block <- bitcoind.getBlockRaw(hash)
// Assert we mined to our address
coinbaseTx = block.transactions.head
_ = assert(
coinbaseTx.outputs.exists(_.scriptPubKey == addr.scriptPubKey))
_ <- wallet.processBlock(block)
// Verify we funded the wallet
allUtxos <- wallet.spendingInfoDAO.findAllSpendingInfos()
_ = assert(allUtxos.size == 1)
utxos <- wallet.listUtxos(TxoState.ImmatureCoinbase)
_ = assert(utxos.size == 1)
bitcoindAddr <- bitcoind.getNewAddress
// Attempt to spend utxo
_ <- recoverToSucceededIf[RuntimeException](
wallet.sendToAddress(bitcoindAddr, valueToBitcoind, None))
spendingTx = {
val inputs = utxos.map { db =>
TransactionInput(db.outPoint, EmptyScriptSignature, UInt32.zero)
}
val outputs =
Vector(TransactionOutput(amountToSend, bitcoindAddr.scriptPubKey))
BaseTransaction(Int32.two, inputs, outputs, UInt32.zero)
}
_ <- recoverToSucceededIf[RuntimeException](
wallet.processTransaction(spendingTx, None))
// Make coinbase mature
_ <- bitcoind.generateToAddress(101, bitcoindAddr)
_ <- wallet.updateUtxoPendingStates()
// Create valid spending tx
psbt = PSBT.fromUnsignedTx(spendingTx)
signedPSBT <- wallet.signPSBT(psbt)
signedTx = signedPSBT.finalizePSBT
.flatMap(_.extractTransactionAndValidate)
.get
// Process tx, validate correctly moved to
_ <- wallet.processTransaction(signedTx, None)
newCoinbaseUtxos <- wallet.listUtxos(TxoState.ImmatureCoinbase)
_ = assert(newCoinbaseUtxos.isEmpty)
spentUtxos <- wallet.listUtxos(TxoState.PendingConfirmationsSpent)
_ = assert(spentUtxos.size == 1)
// Assert spending tx valid to bitcoind
oldBalance <- bitcoind.getBalance
_ = assert(oldBalance == Satoshis(510000000000L))
_ <- bitcoind.sendRawTransaction(signedTx)
_ <- bitcoind.generateToAddress(1, bitcoindAddr)
newBalance <- bitcoind.getBalance
} yield assert(newBalance == oldBalance + amountToSend + Bitcoins(50))
}
}

View File

@ -200,7 +200,7 @@ class WalletUnitTest extends BitcoinSWalletTest {
spk = MultiSignatureScriptPubKey(2, Vector(dummyKey, walletKey))
dummyPrevTx = dummyTx(spk = spk)
prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx)
prevTxDb = TransactionDbHelper.fromTransaction(dummyPrevTx, None)
_ <- wallet.transactionDAO.create(prevTxDb)
psbt = dummyPSBT(prevTxId = dummyPrevTx.txId)

View File

@ -1,18 +1,16 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.api.wallet.db.{
IncomingTransactionDb,
TransactionDb,
TransactionDbHelper
}
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.WalletTestUtil
class IncomingTransactionDAOTest extends WalletDAOFixture {
val txDb: TransactionDb =
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction,
Some(DoubleSha256DigestBE.empty))
val incoming: IncomingTransactionDb =
IncomingTransactionDb(WalletTestUtil.sampleTransaction.txIdBE,

View File

@ -1,18 +1,16 @@
package org.bitcoins.wallet.models
import org.bitcoins.core.api.wallet.db.{
OutgoingTransactionDb,
TransactionDb,
TransactionDbHelper
}
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.testkit.fixtures.WalletDAOFixture
import org.bitcoins.testkit.wallet.WalletTestUtil
class OutgoingTransactionDAOTest extends WalletDAOFixture {
val txDb: TransactionDb =
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction,
Some(DoubleSha256DigestBE.empty))
val outgoing: OutgoingTransactionDb = OutgoingTransactionDb.fromTransaction(
WalletTestUtil.sampleTransaction,

View File

@ -7,7 +7,7 @@ import org.bitcoins.testkit.wallet.WalletTestUtil
class TransactionDAOTest extends WalletDAOFixture {
val txDb: TransactionDb =
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction)
TransactionDbHelper.fromTransaction(WalletTestUtil.sampleTransaction, None)
it should "insert and read an transaction into the database" in { daos =>
val txDAO = daos.transactionDAO

View File

@ -0,0 +1,16 @@
ALTER TABLE "tx_table"
ADD COLUMN "block_hash" TEXT;
ALTER TABLE "tx_table"
Drop COLUMN "id";
UPDATE "tx_table"
set "block_hash" = (
select "txo_spending_info"."block_hash"
from "txo_spending_info"
where "txo_spending_info"."txid" = "tx_table"."txIdBE"
);
-- Delete block_hash column, add spending_txid column
ALTER TABLE "txo_spending_info" DROP COLUMN "block_hash";
ALTER TABLE "txo_spending_info" ADD COLUMN "spending_txid" TEXT;

View File

@ -0,0 +1,19 @@
ALTER TABLE "tx_table"
ADD COLUMN "block_hash" VARCHAR(254);
UPDATE "tx_table"
set "block_hash" = (
select "txo_spending_info"."block_hash"
from "txo_spending_info"
where "txo_spending_info"."txid" = "tx_table"."txIdBE"
);
-- Delete block_hash column, add spending_txid column
CREATE TABLE IF NOT EXISTS "txo_spending_info_backup" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key_id" INT NOT NULL,"value" INTEGER NOT NULL,"hd_privkey_path" VARCHAR(254) NOT NULL,"redeem_script" VARCHAR(254),"script_witness" VARCHAR(254),"txid" VARCHAR(254) NOT NULL,"block_hash" VARCHAR(254), "txo_state" VARCHAR(254) NOT NULL);
INSERT INTO "txo_spending_info_backup" SELECT * FROM "txo_spending_info";
DROP TABLE "txo_spending_info";
CREATE TABLE IF NOT EXISTS "txo_spending_info" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"tx_outpoint" VARCHAR(254) NOT NULL, "script_pub_key_id" INT NOT NULL,"value" INTEGER NOT NULL,"hd_privkey_path" VARCHAR(254) NOT NULL,"redeem_script" VARCHAR(254),"script_witness" VARCHAR(254),"txid" VARCHAR(254) NOT NULL, "txo_state" VARCHAR(254) NOT NULL, spending_txid VARCHAR(254), constraint "fk_scriptPubKey" foreign key("script_pub_key_id") references "pub_key_scripts"("id"), constraint "fk_incoming_txId" foreign key("txid") references "wallet_incoming_txs"("txIdBE") on update NO ACTION on delete NO ACTION);
INSERT INTO "txo_spending_info" ("id","tx_outpoint","script_pub_key_id","value","hd_privkey_path","redeem_script","script_witness","txid","txo_state") SELECT t."id",t."tx_outpoint",t."script_pub_key_id",t.value,t."hd_privkey_path",t."redeem_script",t."script_witness",t."txid",t."txo_state" FROM "txo_spending_info_backup" t;
CREATE INDEX "txo_spending_info_spk_idx" ON "txo_spending_info"("script_pub_key_id");
DROP TABLE "txo_spending_info_backup";

View File

@ -107,7 +107,8 @@ abstract class Wallet
protected def downloadMissingUtxos: Future[Unit] =
for {
utxos <- utxosWithMissingTx
blockHashes = utxos.flatMap(_.blockHash.map(_.flip))
txDbs <- transactionDAO.findByTxIdBEs(utxos.map(_.txid))
blockHashes = txDbs.flatMap(_.blockHashOpt.map(_.flip))
// Download the block the tx is from so we process the block and subsequent txs
_ <-
if (blockHashes.nonEmpty) {
@ -512,12 +513,13 @@ abstract class Wallet
newFeeRate: FeeUnit): Future[Transaction] = {
for {
txDbOpt <- transactionDAO.findByTxId(txId)
tx <- txDbOpt match {
case Some(db) => Future.successful(db.transaction)
txDb <- txDbOpt match {
case Some(db) => Future.successful(db)
case None =>
Future.failed(
new RuntimeException(s"Unable to find transaction ${txId.hex}"))
}
tx = txDb.transaction
_ = require(TxUtil.isRBFEnabled(tx), "Transaction is not signaling RBF")
@ -529,11 +531,9 @@ abstract class Wallet
_ = require(utxos.size == tx.inputs.size,
"Can only bump fee for a transaction we own all the inputs")
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
blockHashes = oldOutputs.flatMap(_.blockHash).distinct
_ = require(
blockHashes.isEmpty,
s"Cannot replace a confirmed transaction, ${blockHashes.map(_.hex)}")
txDb.blockHashOpt.isEmpty,
s"Cannot replace a confirmed transaction, ${txDb.blockHashOpt.get.hex}")
spendingInfos <- FutureUtil.sequentially(utxos) { utxo =>
transactionDAO
@ -579,6 +579,7 @@ abstract class Wallet
Random.shuffle(myAddrs.map(_.scriptPubKey)).head
}
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
// Mark old outputs as replaced
_ <- spendingInfoDAO.updateAll(
oldOutputs.map(_.copyWithState(TxoState.DoesNotExist)))
@ -738,22 +739,21 @@ abstract class Wallet
feeRate: FeeUnit): Future[Transaction] = {
for {
txDbOpt <- transactionDAO.findByTxId(txId)
tx <- txDbOpt match {
case Some(db) => Future.successful(db.transaction)
txDb <- txDbOpt match {
case Some(db) => Future.successful(db)
case None =>
Future.failed(
new RuntimeException(s"Unable to find transaction ${txId.hex}"))
}
tx = txDb.transaction
spendingInfos <- spendingInfoDAO.findTx(tx)
_ = require(spendingInfos.nonEmpty,
s"Transaction ${txId.hex} must have an output we own")
oldOutputs <- spendingInfoDAO.findDbsForTx(txId)
blockHashes = oldOutputs.flatMap(_.blockHash).distinct
_ = require(
blockHashes.isEmpty,
s"No need to fee bump a confirmed transaction, ${blockHashes.map(_.hex)}")
txDb.blockHashOpt.isEmpty,
s"Cannot replace a confirmed transaction, ${txDb.blockHashOpt.get.hex}")
changeSpendingInfos = spendingInfos.flatMap { db =>
if (db.isChange) {

View File

@ -2,7 +2,6 @@ package org.bitcoins.wallet.internal
import org.bitcoins.core.api.wallet.db.{AccountDb, SpendingInfoDb}
import org.bitcoins.core.api.wallet.{CoinSelectionAlgo, CoinSelector}
import org.bitcoins.core.consensus.Consensus
import org.bitcoins.core.protocol.transaction._
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.builder._
@ -75,18 +74,8 @@ trait FundTransactionHandling extends WalletLogger { self: Wallet =>
}
// Need to remove immature coinbase inputs
coinbaseUtxos = utxoWithTxs.filter(_._2.isCoinbase)
confFs = coinbaseUtxos.map(utxo =>
chainQueryApi
.getNumberOfConfirmations(utxo._1.blockHash.get)
.map((utxo, _)))
confs <- FutureUtil.collect(confFs)
immatureCoinbases =
confs
.filter { case (_, confsOpt) =>
confsOpt.isDefined && confsOpt.get < Consensus.coinbaseMaturity
}
.map(_._1)
immatureCoinbases = utxoWithTxs.filter(
_._1.state == TxoState.ImmatureCoinbase)
} yield utxoWithTxs.filter(utxo =>
!immatureCoinbases.exists(_._1 == utxo._1))

View File

@ -1,14 +1,14 @@
package org.bitcoins.wallet.internal
import org.bitcoins.core.api.wallet.{AddUtxoError, AddUtxoSuccess}
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.api.wallet.{AddUtxoError, AddUtxoSuccess}
import org.bitcoins.core.consensus.Consensus
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.util.{FutureUtil, TimeUtil}
import org.bitcoins.core.util.TimeUtil
import org.bitcoins.core.wallet.fee.FeeUnit
import org.bitcoins.core.wallet.utxo.{AddressTag, TxoState}
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
@ -94,8 +94,10 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
/////////////////////
// Internal wallet API
protected def insertTransaction(tx: Transaction): Future[TransactionDb] = {
val txDb = TransactionDbHelper.fromTransaction(tx)
protected def insertTransaction(
tx: Transaction,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[TransactionDb] = {
val txDb = TransactionDbHelper.fromTransaction(tx, blockHashOpt)
transactionDAO.upsert(txDb)
}
@ -103,7 +105,8 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
transaction: Transaction,
feeRate: FeeUnit,
inputAmount: CurrencyUnit,
sentAmount: CurrencyUnit): Future[
sentAmount: CurrencyUnit,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
(TransactionDb, OutgoingTransactionDb)] = {
val outgoingDb =
OutgoingTransactionDb.fromTransaction(transaction,
@ -111,7 +114,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
sentAmount,
feeRate.calc(transaction))
for {
txDb <- insertTransaction(transaction)
txDb <- insertTransaction(transaction, blockHashOpt)
written <- outgoingTxDAO.upsert(outgoingDb)
} yield (txDb, written)
}
@ -131,7 +134,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
s"Processing TX from our wallet, transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
for {
(txDb, _) <-
insertOutgoingTransaction(transaction, feeRate, inputAmount, sentAmount)
insertOutgoingTransaction(transaction,
feeRate,
inputAmount,
sentAmount,
blockHashOpt)
result <- processTransactionImpl(txDb.transaction, blockHashOpt, newTags)
} yield {
val txid = txDb.transaction.txIdBE
@ -205,6 +212,11 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
for {
outputsBeingSpent <- spendingInfoDAO.findOutputsBeingSpent(transaction)
_ <-
if (outputsBeingSpent.nonEmpty)
insertTransaction(transaction, blockHashOpt)
else Future.unit
// unreserved outputs now they are in a block
outputsToUse = blockHashOpt match {
case Some(_) =>
@ -219,7 +231,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
processed <- Future
.sequence {
outputsToUse.map(markAsPendingSpent)
outputsToUse.map(markAsSpent(_, transaction.txIdBE))
}
.map(_.toVector)
} yield processed.flatten
@ -239,11 +251,9 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
logger.debug(
s"Processing transaction=${transaction.txIdBE} with blockHash=$blockHashOpt")
val incomingF = processIncomingUtxos(transaction, blockHashOpt, newTags)
val outgoingF = processOutgoingUtxos(transaction, blockHashOpt)
for {
incoming <- incomingF
outgoing <- outgoingF
incoming <- processIncomingUtxos(transaction, blockHashOpt, newTags)
outgoing <- processOutgoingUtxos(transaction, blockHashOpt)
_ <- walletCallbacks.executeOnTransactionProcessed(logger, transaction)
} yield {
ProcessTxResult(incoming, outgoing)
@ -251,24 +261,38 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
}
/** If the given UTXO is marked as unspent, updates
* its spending status. Otherwise returns `None`.
* its spending status. Otherwise returns an error.
*/
private def markAsPendingSpent(
out: SpendingInfoDb): Future[Option[SpendingInfoDb]] = {
private def markAsSpent(
out: SpendingInfoDb,
spendingTxId: DoubleSha256DigestBE): Future[Option[SpendingInfoDb]] = {
out.state match {
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived |
TxoState.ImmatureCoinbase =>
case TxoState.ConfirmedReceived | TxoState.PendingConfirmationsReceived =>
val updated =
out.copyWithState(state = TxoState.PendingConfirmationsSpent)
out
.copyWithState(state = TxoState.PendingConfirmationsSpent)
.copyWithSpendingTxId(spendingTxId)
val updatedF =
spendingInfoDAO.update(updated)
updatedF.foreach(updated =>
logger.debug(
s"Marked utxo=${updated.toHumanReadableString} as state=${updated.state}"))
updatedF.map(Some(_))
case TxoState.Reserved | TxoState.ConfirmedSpent |
TxoState.PendingConfirmationsSpent | TxoState.DoesNotExist =>
FutureUtil.none
case TxoState.Reserved | TxoState.PendingConfirmationsSpent =>
val updated =
out.copyWithSpendingTxId(spendingTxId)
val updatedF =
spendingInfoDAO.update(updated)
updatedF.map(Some(_))
case TxoState.ImmatureCoinbase =>
Future.failed(new RuntimeException(
s"Attempting to spend an ImmatureCoinbase ${out.outPoint.hex}, this should not be possible until it is confirmed."))
case TxoState.ConfirmedSpent =>
Future.failed(new RuntimeException(
s"Attempted to mark an already spent utxo ${out.outPoint.hex} with a new spending tx ${spendingTxId.hex}"))
case TxoState.DoesNotExist =>
Future.failed(new RuntimeException(
s"Attempted to process a transaction for a utxo that does not exist ${out.outPoint.hex} with a new spending tx ${spendingTxId.hex}"))
}
}
@ -279,17 +303,14 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
private def processUtxo(
transaction: Transaction,
index: Int,
state: TxoState,
blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = {
addUtxo(transaction = transaction,
vout = UInt32(index),
state = state,
blockHash = blockHash).flatMap {
case AddUtxoSuccess(utxo) => Future.successful(utxo)
case err: AddUtxoError =>
logger.error(s"Could not add UTXO", err)
Future.failed(err)
}
state: TxoState): Future[SpendingInfoDb] = {
addUtxo(transaction = transaction, vout = UInt32(index), state = state)
.flatMap {
case AddUtxoSuccess(utxo) => Future.successful(utxo)
case err: AddUtxoError =>
logger.error(s"Could not add UTXO", err)
Future.failed(err)
}
}
private case class OutputWithIndex(output: TransactionOutput, index: Int)
@ -311,65 +332,39 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
logger.error(errMsg)
Future.failed(new RuntimeException(errMsg))
} else {
(foundTxo.blockHash, blockHashOpt) match {
case (None, Some(blockHash)) =>
blockHashOpt match {
case Some(blockHash) =>
logger.debug(
s"Updating block_hash of txo=${transaction.txIdBE}, new block hash=$blockHash")
// update block hash
val txoWithHash = foundTxo.copyWithBlockHash(blockHash = blockHash)
// If the utxo was marked reserved we want to update it to spent now
// since it has been included in a block
val unreservedTxo = txoWithHash.state match {
val unreservedTxo = foundTxo.state match {
case TxoState.Reserved =>
txoWithHash.copyWithState(TxoState.PendingConfirmationsSpent)
foundTxo.copyWithState(TxoState.PendingConfirmationsSpent)
case TxoState.PendingConfirmationsReceived |
TxoState.ConfirmedReceived |
TxoState.PendingConfirmationsSpent | TxoState.ConfirmedSpent |
TxoState.DoesNotExist | TxoState.ImmatureCoinbase =>
txoWithHash
foundTxo
}
val updateTxDbF = insertTransaction(transaction, blockHashOpt)
// Update Txo State
updateUtxoConfirmedState(unreservedTxo).flatMap {
case Some(txo) =>
logger.debug(
s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}")
Future.successful(txo)
case None =>
// State was not updated so we need to update it so it's block hash is in the database
spendingInfoDAO.update(unreservedTxo)
}
case (Some(oldBlockHash), Some(newBlockHash)) =>
if (oldBlockHash == newBlockHash) {
logger.debug(
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
for {
relevantOuts <- getRelevantOutputs(transaction)
totalIncoming = relevantOuts.map(_.output.value).sum
_ <- insertIncomingTransaction(transaction, totalIncoming)
} yield foundTxo
} else {
val errMsg =
Seq(
s"Found TXO has block hash=${oldBlockHash}, tx we were given has block hash=${newBlockHash}.",
"This is either a reorg or a double spent, which is not implemented yet"
).mkString(" ")
logger.error(errMsg)
Future.failed(new RuntimeException(errMsg))
}
case (Some(blockHash), None) =>
val msg =
List(
s"Incoming transaction=${transaction.txIdBE} already has block hash=$blockHash! assigned",
s" I don't know how to handle this."
).mkString(" ")
logger.warn(msg)
Future.failed(new RuntimeException(msg))
case (None, None) =>
updateTxDbF.flatMap(_ =>
updateUtxoConfirmedState(unreservedTxo).flatMap {
case Some(txo) =>
logger.debug(
s"Updated block_hash of txo=${txo.txid.hex} new block hash=${blockHash.hex}")
Future.successful(txo)
case None =>
// State was not updated so we need to update it so it's block hash is in the database
spendingInfoDAO.update(unreservedTxo)
})
case None =>
logger.debug(
s"Skipping further processing of transaction=${transaction.txIdBE}, already processed.")
s"Skipping further processing of transaction=${transaction.txIdBE.hex}, already processed.")
Future.successful(foundTxo)
}
}
@ -403,8 +398,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
processUtxo(
transaction,
out.index,
state = state,
blockHash = blockHashOpt
state = state
)
}
Future.sequence(outputsVec)
@ -413,11 +407,12 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
private[wallet] def insertIncomingTransaction(
transaction: Transaction,
incomingAmount: CurrencyUnit): Future[
incomingAmount: CurrencyUnit,
blockHashOpt: Option[DoubleSha256DigestBE]): Future[
(TransactionDb, IncomingTransactionDb)] = {
val incomingDb = IncomingTransactionDb(transaction.txIdBE, incomingAmount)
for {
txDb <- insertTransaction(transaction)
txDb <- insertTransaction(transaction, blockHashOpt)
written <- incomingTxDAO.upsert(incomingDb)
} yield (txDb, written)
}
@ -478,7 +473,7 @@ private[wallet] trait TransactionProcessing extends WalletLogger {
}
val txDbF: Future[(TransactionDb, IncomingTransactionDb)] =
insertIncomingTransaction(transaction, totalIncoming)
insertIncomingTransaction(transaction, totalIncoming, blockHashOpt)
val prevTagsDbF = for {
(txDb, _) <- txDbF

View File

@ -22,12 +22,13 @@ import org.bitcoins.core.protocol.transaction.{
TransactionOutput
}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.utxo.TxoState._
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.wallet.{Wallet, WalletLogger}
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
import scala.util.{Failure, Success}
/** Provides functionality related to handling UTXOs in our wallet.
* The most notable examples of functionality here are enumerating
@ -87,68 +88,107 @@ private[wallet] trait UtxoHandling extends WalletLogger {
}
}
protected def updateUtxoConfirmedState(
private[wallet] def updateUtxoConfirmedState(
txo: SpendingInfoDb): Future[Option[SpendingInfoDb]] = {
updateUtxoConfirmedStates(Vector(txo)).map(_.headOption)
}
protected def updateUtxoConfirmedStates(
/** Returns a map of the SpendingInfoDbs with their relevant block.
* If the block hash is None, then it is a mempool transaction.
* The relevant block is determined by if the utxo has been spent or not.
* If it has been spent it uses the block that included the spending transaction,
* otherwise it uses the block that included the receiving transaction.
*/
private[wallet] def getDbsByRelevantBlock(
spendingInfoDbs: Vector[SpendingInfoDb]): Future[
Map[Option[DoubleSha256DigestBE], Vector[SpendingInfoDb]]] = {
val txIds =
spendingInfoDbs.map { db =>
db.spendingTxIdOpt match {
case Some(spendingTxId) =>
spendingTxId
case None =>
db.txid
}
}
transactionDAO.findByTxIdBEs(txIds).map { txDbs =>
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 =>
txo.txid
case PendingConfirmationsSpent | ConfirmedSpent =>
txo.spendingTxIdOpt.get
}
(blockHashMap(txToUse), txo)
}
blockHashAndDb.groupBy(_._1).map { case (blockHashOpt, vec) =>
blockHashOpt -> vec.map(_._2)
}
}
}
/** Updates the SpendingInfoDb to the correct state based
* on the number of confirmations it has received
*/
private[wallet] def updateTxoWithConfs(
txo: SpendingInfoDb,
confs: Int): SpendingInfoDb = {
txo.state match {
case TxoState.ImmatureCoinbase =>
if (confs > Consensus.coinbaseMaturity) {
if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedReceived)
else
txo.copyWithState(TxoState.PendingConfirmationsReceived)
} else txo
case TxoState.PendingConfirmationsReceived =>
if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedReceived)
else txo
case TxoState.PendingConfirmationsSpent =>
if (confs >= walletConfig.requiredConfirmations)
txo.copyWithState(TxoState.ConfirmedSpent)
else txo
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
txo
case TxoState.DoesNotExist | TxoState.ConfirmedReceived |
TxoState.ConfirmedSpent =>
txo
}
}
/** Updates all the given SpendingInfoDbs to the correct state
* based on how many confirmations they have received
*/
private[wallet] def updateUtxoConfirmedStates(
spendingInfoDbs: Vector[SpendingInfoDb]): Future[
Vector[SpendingInfoDb]] = {
val byBlock = spendingInfoDbs.groupBy(_.blockHash)
val toUpdateFs = byBlock.map {
case (Some(blockHash), txos) =>
chainQueryApi.getNumberOfConfirmations(blockHash).map {
case None =>
logger.warn(
s"Given txos exist in block (${blockHash.hex}) that we do not have or that has been reorged! $txos")
Vector.empty
case Some(confs) =>
txos.flatMap { txo =>
txo.state match {
case TxoState.ImmatureCoinbase =>
if (confs > Consensus.coinbaseMaturity) {
if (confs >= walletConfig.requiredConfirmations) {
Some(txo.copyWithState(TxoState.ConfirmedReceived))
} else {
Some(
txo.copyWithState(
TxoState.PendingConfirmationsReceived))
}
} else {
None
}
case TxoState.PendingConfirmationsReceived =>
if (confs >= walletConfig.requiredConfirmations) {
Some(txo.copyWithState(TxoState.ConfirmedReceived))
} else {
None
}
case TxoState.PendingConfirmationsSpent =>
if (confs >= walletConfig.requiredConfirmations) {
Some(txo.copyWithState(TxoState.ConfirmedSpent))
} else {
None
}
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
None
case TxoState.DoesNotExist | TxoState.ConfirmedReceived |
TxoState.ConfirmedSpent =>
None
}
val toUpdateF = getDbsByRelevantBlock(spendingInfoDbs).flatMap {
txsByBlock =>
val toUpdateFs = txsByBlock.map {
case (Some(blockHash), txos) =>
chainQueryApi.getNumberOfConfirmations(blockHash).map {
case None =>
logger.warn(
s"Given txos exist in block (${blockHash.hex}) that we do not have or that has been reorged! $txos")
Vector.empty
case Some(confs) => txos.map(updateTxoWithConfs(_, confs))
}
case (None, txos) =>
logger.debug(
s"Currently have ${txos.size} transactions in the mempool")
Future.successful(Vector.empty)
}
case (None, txos) =>
logger.debug(s"Currently have ${txos.size} transactions in the mempool")
Future.successful(Vector.empty)
FutureUtil.collect(toUpdateFs)
}
for {
toUpdate <- FutureUtil.collect(toUpdateFs)
toUpdate <- toUpdateF
_ =
if (toUpdate.nonEmpty)
logger.info(s"${toUpdate.size} txos are now confirmed!")
@ -177,8 +217,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
state: TxoState,
output: TransactionOutput,
outPoint: TransactionOutPoint,
addressDb: AddressDb,
blockHash: Option[DoubleSha256DigestBE]): Future[SpendingInfoDb] = {
addressDb: AddressDb): Future[SpendingInfoDb] = {
val utxo: SpendingInfoDb = addressDb match {
case segwitAddr: SegWitAddressDb =>
@ -189,7 +228,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
output = output,
privKeyPath = segwitAddr.path,
scriptWitness = segwitAddr.witnessScript,
blockHash = blockHash
spendingTxIdOpt = None
)
case LegacyAddressDb(path, _, _, _, _) =>
LegacySpendingInfo(state = state,
@ -197,7 +236,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
outPoint = outPoint,
output = output,
privKeyPath = path,
blockHash = blockHash)
spendingTxIdOpt = None)
case nested: NestedSegWitAddressDb =>
NestedSegwitV0SpendingInfo(
outPoint = outPoint,
@ -207,8 +246,8 @@ private[wallet] trait UtxoHandling extends WalletLogger {
scriptWitness = P2WPKHWitnessV0(nested.ecPublicKey),
txid = tx.txIdBE,
state = state,
id = None,
blockHash = blockHash
spendingTxIdOpt = None,
id = None
)
}
@ -228,8 +267,7 @@ private[wallet] trait UtxoHandling extends WalletLogger {
protected def addUtxo(
transaction: Transaction,
vout: UInt32,
state: TxoState,
blockHash: Option[DoubleSha256DigestBE]): Future[AddUtxoResult] = {
state: TxoState): Future[AddUtxoResult] = {
logger.info(s"Adding UTXO to wallet: ${transaction.txId.hex}:${vout.toInt}")
@ -266,11 +304,10 @@ private[wallet] trait UtxoHandling extends WalletLogger {
state = state,
output = output,
outPoint = outPoint,
addressDb = addressDb,
blockHash = blockHash)
addressDb = addressDb)
}
.flatMap {
case Right(utxoF) => utxoF.map(AddUtxoSuccess(_))
case Right(utxoF) => utxoF.map(AddUtxoSuccess)
case Left(e) => Future.successful(e)
}
}
@ -300,35 +337,19 @@ private[wallet] trait UtxoHandling extends WalletLogger {
require(unreserved.isEmpty, s"Some utxos are not reserved, got $unreserved")
// unmark all utxos are reserved
val groupedUtxos = utxos
val updatedUtxos = utxos
.map(_.copyWithState(TxoState.PendingConfirmationsReceived))
.groupBy(_.blockHash)
val mempoolUtxos = Try(groupedUtxos(None)).getOrElse(Vector.empty)
// get the ones in blocks
val utxosInBlocks = groupedUtxos.flatMap {
case (Some(_), utxos) =>
utxos
case (None, _) =>
None
}.toVector
for {
// update utxos in the mempool
updatedMempoolUtxos <-
spendingInfoDAO.updateAllSpendingInfoDb(mempoolUtxos)
// update the confirmed utxos
updatedConfirmed <- updateUtxoConfirmedStates(utxosInBlocks)
updatedConfirmed <- updateUtxoConfirmedStates(updatedUtxos)
// update the utxos that are in blocks but not considered confirmed yet
pendingConf = utxosInBlocks.filterNot(utxo =>
pendingConf = updatedUtxos.filterNot(utxo =>
updatedConfirmed.exists(_.outPoint == utxo.outPoint))
updatedPendingConf <- spendingInfoDAO.updateAllSpendingInfoDb(pendingConf)
updated <- spendingInfoDAO.updateAllSpendingInfoDb(
pendingConf ++ updatedConfirmed)
// Return all utxos that were updated
updated = updatedMempoolUtxos ++ updatedConfirmed ++ updatedPendingConf
_ <- walletCallbacks.executeOnReservedUtxos(logger, updated)
} yield updated
}

View File

@ -1,7 +1,5 @@
package org.bitcoins.wallet.models
import java.sql.SQLException
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.hd._
@ -17,6 +15,7 @@ import org.bitcoins.crypto.DoubleSha256DigestBE
import org.bitcoins.db.CRUDAutoInc
import org.bitcoins.wallet.config._
import java.sql.SQLException
import scala.concurrent.{ExecutionContext, Future}
case class SpendingInfoDAO()(implicit
@ -483,7 +482,8 @@ case class SpendingInfoDAO()(implicit
def scriptWitnessOpt: Rep[Option[ScriptWitness]] = column("script_witness")
def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash")
def spendingTxIdOpt: Rep[Option[DoubleSha256DigestBE]] = column(
"spending_txid")
/** All UTXOs must have a SPK in the wallet that gets spent to */
def fk_scriptPubKeyId: slick.lifted.ForeignKeyQuery[_, ScriptPubKeyDb] = {
@ -511,7 +511,7 @@ case class SpendingInfoDAO()(implicit
privKeyPath,
redeemScriptOpt,
scriptWitnessOpt,
blockHash,
spendingTxIdOpt,
id.?).<>((UTXORecord.apply _).tupled, UTXORecord.unapply)
}
}

View File

@ -101,10 +101,22 @@ case class TransactionDAO()(implicit
override val table = TableQuery[TransactionTable]
def findAllUnconfirmed(): Future[Vector[TransactionDb]] = {
val query = table.filter(_.blockHash === Rep.None[DoubleSha256DigestBE])
safeDatabase.runVec(query.result)
}
def findAllConfirmed(): Future[Vector[TransactionDb]] = {
val query = table.filterNot(_.blockHash === Rep.None[DoubleSha256DigestBE])
safeDatabase.runVec(query.result)
}
class TransactionTable(tag: Tag)
extends TxTable[TransactionDb](tag, schemaName, "tx_table") {
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.Unique)
def txIdBE: Rep[DoubleSha256DigestBE] = column("txIdBE", O.PrimaryKey)
def transaction: Rep[Transaction] = column("transaction")
@ -123,6 +135,8 @@ case class TransactionDAO()(implicit
def locktime: Rep[UInt32] = column("locktime")
def blockHash: Rep[Option[DoubleSha256DigestBE]] = column("block_hash")
def * : ProvenShape[TransactionDb] =
(txIdBE,
transaction,
@ -132,7 +146,8 @@ case class TransactionDAO()(implicit
totalOutput,
numInputs,
numOutputs,
locktime).<>(TransactionDb.tupled, TransactionDb.unapply)
locktime,
blockHash).<>(TransactionDb.tupled, TransactionDb.unapply)
def primaryKey: PrimaryKey =
primaryKey("pk_tx", sourceColumns = txIdBE)