Address issue #673, also refactor fixture stuff to be in companion ob… (#676)

* Address issue #673, also refactor fixture stuff to be in companion objects rather than traits so they can be re-used without extending the trait itself. This allows for more modularity with our fixtures

* fix bugs in unit tests, address #675 to make MerkleBufferTests faster

* Refactor test timeout in UpdateBloomFitlerTest

* fix callback logic bug on matching unrelated txs/addresses in UpdateBloomFilterTest, re-order broadcasting of things to avoid async bugs hopefully

* Make  a def in NodeUnitTest, this keeps tests in the same suite from using the same config. This allows us to write multiple tests per suite, instead of just one. This also adds implicit parameters to our fixture constructors/destructors to properly create and destroy this config. The long term goal here also should be getting rid of config.initialize() we are calling everywhere as this is a anti-pattern, fixtures should take care of construction and destruction of things

* Broadcast tx earlier in test case for UpdateBloomFilterTest

* Rework NodeWithWalletTest to use the new fixtures we have in the node project, now use SpvNodeFundedWalletBitcoind
This commit is contained in:
Chris Stewart 2019-08-06 11:49:17 -05:00 committed by GitHub
parent 4d1b509ae7
commit c934d8efc2
14 changed files with 459 additions and 379 deletions

View File

@ -125,6 +125,8 @@ object Blockchain extends ChainVerificationLogger {
*/
private def parseSuccessOrFailure(nested: Vector[Future[BlockchainUpdate]])(
implicit ec: ExecutionContext): Future[BlockchainUpdate] = {
require(nested.nonEmpty,
s"Cannot parse success or failure if we don't have any updates!")
val successfulTipOptF: Future[Option[BlockchainUpdate]] = {
Future.find(nested) {
case update: BlockchainUpdate =>

View File

@ -14,9 +14,11 @@ import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.rpc.BitcoindException
import org.bitcoins.core.protocol.transaction.Transaction
import org.scalactic.Bool
import scala.concurrent.Future
import scala.concurrent.duration._
import org.bitcoins.testkit.async.TestAsyncUtil
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.WalletWithBitcoind
class BroadcastTransactionTest extends BitcoinSWalletTest {

View File

@ -1,122 +1,96 @@
package org.bitcoins.node
import org.bitcoins.core.currency._
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.Peer
import org.scalatest.FutureOutcome
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import scala.concurrent.Future
import org.bitcoins.node.networking.peer.DataMessageHandler
import scala.concurrent.Promise
import scala.concurrent.duration._
import org.scalatest.compatible.Assertion
import org.scalatest.exceptions.TestFailedException
import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.testkit.node.NodeTestUtil
import akka.actor.Cancellable
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.crypto.DoubleSha256DigestBE
import scala.util.Try
import scala.util.Failure
import scala.util.Success
import scala.concurrent.Await
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.bloom.BloomFilter
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.core.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.core.currency._
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.networking.peer.DataMessageHandler
import org.bitcoins.testkit.node.NodeUnitTest.SpvNodeFundedWalletBitcoind
import org.bitcoins.testkit.node.{NodeTestUtil, NodeUnitTest}
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.scalatest.FutureOutcome
import org.scalatest.exceptions.TestFailedException
class NodeWithWalletTest extends BitcoinSWalletTest {
import scala.concurrent.duration._
import scala.concurrent.{Future, Promise}
override type FixtureParam = WalletWithBitcoind
class NodeWithWalletTest extends NodeUnitTest {
def withFixture(test: OneArgAsyncTest): FutureOutcome =
withNewWalletAndBitcoind(test)
override type FixtureParam = SpvNodeFundedWalletBitcoind
it must "load a bloom filter and receive information about received payments" in {
param =>
val WalletWithBitcoind(wallet, rpc) = param
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withSpvNodeFundedWalletBitcoind(test, callbacks)
}
/**
* This is not ideal, how do we get one implicit value (`config`)
* to resolve to multiple implicit parameters?
*/
implicit val nodeConfig: NodeAppConfig = config
implicit val chainConfig: ChainAppConfig = config
private val assertionP: Promise[Boolean] = Promise()
var expectedTxId: Option[DoubleSha256Digest] = None
var cancellable: Option[Cancellable] = None
private val expectedTxIdP: Promise[DoubleSha256Digest] = Promise()
private val expectedTxIdF: Future[DoubleSha256Digest] = expectedTxIdP.future
val completionP = Promise[Assertion]
private val walletP: Promise[UnlockedWalletApi] = Promise()
private val walletF: Future[UnlockedWalletApi] = walletP.future
val amountFromBitcoind = 1.bitcoin
val callbacks = {
def callbacks: SpvNodeCallbacks = {
val onTx: DataMessageHandler.OnTxReceived = { tx =>
if (expectedTxId.contains(tx.txId)) {
logger.debug(s"Cancelling timeout we set earlier")
cancellable.map(_.cancel())
for {
expectedTxId <- expectedTxIdF
wallet <- walletF
} yield {
if (expectedTxId == tx.txId) {
for {
prevBalance <- wallet.getUnconfirmedBalance()
_ <- wallet.processTransaction(tx, confirmations = 0)
balance <- wallet.getUnconfirmedBalance()
} completionP.complete {
Try {
assert(balance == prevBalance + amountFromBitcoind)
} yield {
val result = balance == prevBalance + amountFromBitcoind
assertionP.success(result)
}
}
}
}
SpvNodeCallbacks(
onTxReceived = Seq(onTx)
)
}
def processWalletTx(tx: DoubleSha256DigestBE) = {
expectedTxId = Some(tx.flip)
it must "load a bloom filter and receive information about received payments" in {
param =>
val SpvNodeFundedWalletBitcoind(initSpv, wallet, rpc) = param
walletP.success(wallet)
var cancellable: Option[Cancellable] = None
def processWalletTx(tx: DoubleSha256DigestBE): DoubleSha256DigestBE = {
expectedTxIdP.success(tx.flip)
// how long we're waiting for a tx notify before failing the test
val delay = 15.seconds
val failTest: Runnable = new Runnable {
override def run = {
if (!assertionP.isCompleted) {
val msg =
s"Did not receive sent transaction within $delay"
logger.error(msg)
completionP.failure(new TestFailedException(msg, 0))
assertionP.failure(new TestFailedException(msg, 0))
}
}
}
logger.debug(s"Setting timeout for receiving TX through node")
cancellable = Some(actorSystem.scheduler.scheduleOnce(delay, failTest))
cancellable = Some(system.scheduler.scheduleOnce(delay, failTest))
tx
}
for {
_ <- config.initialize()
address <- wallet.getNewAddress()
bloom <- wallet.getBloomFilter()
spv <- {
val peer = Peer.fromBitcoind(rpc.instance)
val chainHandler = {
val bhDao = BlockHeaderDAO()
ChainHandler(bhDao)
}
val spv =
SpvNode(peer,
chainHandler,
bloomFilter = bloom,
callbacks = callbacks)
spv.start()
}
address <- wallet.getNewAddress()
spv <- initSpv.start()
updatedBloom = spv.updateBloomFilter(address).bloomFilter
_ <- spv.sync()
_ <- NodeTestUtil.awaitSync(spv, rpc)
@ -125,10 +99,10 @@ class NodeWithWalletTest extends BitcoinSWalletTest {
.map(processWalletTx)
ourTx <- rpc.getTransaction(ourTxid)
_ = assert(bloom.isRelevant(ourTx.hex))
_ = assert(updatedBloom.isRelevant(ourTx.hex))
assertion <- completionP.future
} yield assertion
result <- assertionP.future
} yield assert(result)
}
}

View File

@ -1,176 +1,149 @@
package org.bitcoins.node
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.bitcoins.node.models.Peer
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.testkit.node.NodeTestUtil
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.networking.peer.DataMessageHandler
import akka.actor.Cancellable
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.blockchain.MerkleBlock
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.node.networking.peer.DataMessageHandler
import org.bitcoins.testkit.node.NodeUnitTest.SpvNodeFundedWalletBitcoind
import org.bitcoins.testkit.node.{NodeTestUtil, NodeUnitTest}
import org.scalatest.exceptions.TestFailedException
import org.scalatest.{BeforeAndAfter, FutureOutcome}
import scala.concurrent._
import scala.concurrent.duration._
import org.scalatest.compatible.Assertion
import org.bitcoins.core.currency._
import scala.util.Try
import akka.actor.Cancellable
import org.scalatest.run
import org.scalatest.exceptions.TestFailedException
import org.bitcoins.core.wallet.fee.SatoshisPerByte
class UpdateBloomFilterTest extends BitcoinSWalletTest {
override type FixtureParam = WalletWithBitcoind
class UpdateBloomFilterTest extends NodeUnitTest with BeforeAndAfter {
override type FixtureParam = SpvNodeFundedWalletBitcoind
def withFixture(test: OneArgAsyncTest): FutureOutcome =
withFundedWalletAndBitcoind(test)
def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withSpvNodeFundedWalletBitcoind(test, callbacks)
}
val testTimeout = 30.seconds
private var assertionP: Promise[Boolean] = Promise()
after {
//reset assertion after a test runs, because we
//are doing mutation to work around our callback
//limitations, we can't currently modify callbacks
//after a SpvNode is constructed :-(
assertionP = Promise()
}
/** The address we expect to receive funds at */
private val addressFromWalletP: Promise[BitcoinAddress] = Promise()
// the TX we sent from our wallet to bitcoind,
// we expect to get notified once this is
// confirmed
private val txFromWalletP: Promise[Transaction] = Promise()
def addressCallback: DataMessageHandler.OnTxReceived = { tx: Transaction =>
// we check if any of the addresses in the TX
// pays to our wallet address
val _ = for {
addressFromWallet <- addressFromWalletP.future
result = tx.outputs.exists(
_.scriptPubKey == addressFromWallet.scriptPubKey)
} yield {
if (result) {
assertionP.success(true)
}
}
}
def txCallback: DataMessageHandler.OnMerkleBlockReceived = {
(_: MerkleBlock, txs: Vector[Transaction]) =>
{
txFromWalletP.future
.map { tx =>
if (txs.contains(tx)) {
assertionP.success(true)
}
}
}
}
def callbacks: SpvNodeCallbacks = {
SpvNodeCallbacks(onTxReceived = Vector(addressCallback),
onMerkleBlockReceived = Vector(txCallback))
}
it must "update the bloom filter with an address" in { param =>
val WalletWithBitcoind(wallet, rpc) = param
implicit val chainConf: ChainAppConfig = config
implicit val nodeConf: NodeAppConfig = config
val assertionP = Promise[Assertion]
val assertionF = assertionP.future
val SpvNodeFundedWalletBitcoind(initSpv, wallet, rpc) = param
// we want to schedule a runnable that aborts
// the test after a timeout, but then
// we need to cancel that runnable once
// we get a result
var cancelable: Option[Cancellable] = None
val timeout = 15.seconds
for {
_ <- config.initialize()
firstBloom <- wallet.getBloomFilter()
// this has to be generated after our bloom filter
// is calculated
addressFromWallet <- wallet.getNewAddress()
spv <- {
val callback = SpvNodeCallbacks.onTxReceived { tx =>
rpc.getRawTransaction(tx.txIdBE).foreach { res =>
val paysToOurAddress =
// we check if any of the addresses in the TX
// pays to our wallet address
res.vout.exists(_.scriptPubKey.addresses match {
case None => false
case Some(addresses) => addresses.exists(_ == addressFromWallet)
})
cancelable.forall(_.cancel())
assertionP.complete {
Try {
assert(paysToOurAddress)
}
}
}
}
val peer = Peer.fromBitcoind(rpc.instance)
val chain = {
val dao = BlockHeaderDAO()
ChainHandler(dao)
}
val spv =
SpvNode(peer, chain, bloomFilter = firstBloom, callbacks = callback)
spv.start()
}
_ = addressFromWalletP.success(addressFromWallet)
spv <- initSpv.start()
_ = spv.updateBloomFilter(addressFromWallet)
_ <- spv.sync()
_ <- rpc.sendToAddress(addressFromWallet, 1.bitcoin)
_ <- NodeTestUtil.awaitSync(spv, rpc)
_ = spv.updateBloomFilter(addressFromWallet)
_ = {
val runnable = new Runnable {
cancelable = Some {
system.scheduler.scheduleOnce(
testTimeout,
new Runnable {
override def run: Unit = {
assertionP.failure(
new TestFailedException(
s"Did not receive a TX message after $timeout!",
if (!assertionP.isCompleted)
assertionP.failure(new TestFailedException(
s"Did not receive a merkle block message after $testTimeout!",
failedCodeStackDepth = 0))
}
}
cancelable = Some {
actorSystem.scheduler.scheduleOnce(timeout, runnable)
)
}
}
_ <- rpc.sendToAddress(addressFromWallet, 1.bitcoin)
assertion <- assertionF
} yield assertion
result <- assertionP.future
} yield assert(result)
}
it must "update the bloom filter with a TX" in { param =>
val WalletWithBitcoind(wallet, rpc) = param
implicit val chainConf: ChainAppConfig = config
implicit val nodeConf: NodeAppConfig = config
val assertionP = Promise[Assertion]
val assertionF = assertionP.future
val SpvNodeFundedWalletBitcoind(initSpv, wallet, rpc) = param
// we want to schedule a runnable that aborts
// the test after a timeout, but then
// we need to cancel that runnable once
// we get a result
var cancelable: Option[Cancellable] = None
// the TX we sent from our wallet to bitcoind,
// we expect to get notified once this is
// confirmed
var txFromWallet: Option[Transaction] = None
val timeout = 15.seconds
for {
_ <- config.initialize()
firstBloom <- wallet.getBloomFilter()
spv <- {
val callback = SpvNodeCallbacks.onMerkleBlockReceived { (block, txs) =>
val isFromOurWallet = txFromWallet.exists(tx => txs.contains(tx))
// we might receive more merkle blocks than just the
// one for our TX
if (isFromOurWallet) {
assertionP.success(assert(isFromOurWallet))
}
}
val peer = Peer.fromBitcoind(rpc.instance)
val chain = {
val dao = BlockHeaderDAO()
ChainHandler(dao)
}
val spv =
SpvNode(peer, chain, bloomFilter = firstBloom, callbacks = callback)
spv.start()
}
_ <- spv.sync()
_ <- NodeTestUtil.awaitSync(spv, rpc)
spv <- initSpv.start()
addressFromBitcoind <- rpc.getNewAddress
tx <- wallet
.sendToAddress(addressFromBitcoind,
5.bitcoin,
SatoshisPerByte(100.sats))
.map { tx =>
txFromWallet = Some(tx)
tx
}
_ = txFromWalletP.success(tx)
spvNewBloom = spv.updateBloomFilter(tx)
_ = spv.broadcastTransaction(tx)
_ <- spv.sync()
_ <- NodeTestUtil.awaitSync(spv, rpc)
_ = assert(spvNewBloom.bloomFilter.contains(tx.txId))
_ = {
val _ = spv.broadcastTransaction(tx)
val SpvNode(_, _, newBloom, _) = spv.updateBloomFilter(tx)
assert(newBloom.contains(tx.txId))
cancelable = Some {
actorSystem.scheduler.scheduleOnce(
timeout,
system.scheduler.scheduleOnce(
testTimeout,
new Runnable {
override def run: Unit = {
if (!assertionP.isCompleted)
assertionP.failure(
new TestFailedException(
s"Did not receive a merkle block message after $timeout!",
assertionP.failure(new TestFailedException(
s"Did not receive a merkle block message after $testTimeout!",
failedCodeStackDepth = 0))
}
}
@ -182,8 +155,8 @@ class UpdateBloomFilterTest extends BitcoinSWalletTest {
// we should get notified about the block
_ <- rpc.getNewAddress.flatMap(rpc.generateToAddress(1, _))
assertion <- assertionF
} yield assertion
result <- assertionP.future
} yield assert(result)
}
}

View File

@ -17,18 +17,11 @@ import scala.util.Failure
class MerkleBuffersTest extends BitcoinSUnitTest {
behavior of "MerkleBuffers"
/** Generating blocks and transactions take a little while,
* this is to prevent the test from taking a _really_ long
* time
*/
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
customGenDrivenConfig(executions = 3)
it must "match a merkle block with its corresponding transactions" in {
val txsAndBlockGen: Gen[(Seq[Transaction], Seq[Transaction], Block)] = for {
txs <- Gen.nonEmptyListOf(TransactionGenerators.transaction)
otherTxs <- Gen.nonEmptyListOf(TransactionGenerators.transaction)
txs <- TransactionGenerators.nonEmptySmallTransactions
otherTxs <- TransactionGenerators.nonEmptySmallTransactions
block <- BlockchainElementsGenerator.block(txs)
} yield (txs, otherTxs, block)

View File

@ -17,12 +17,12 @@ class PeerMessageHandlerTest extends NodeUnitTest {
test(())
}
private implicit val akkaTimeout = Timeout(timeout)
implicit private val akkaTimeout = Timeout(timeout)
behavior of "PeerHandler"
it must "be able to fully initialize a PeerMessageReceiver" in { _ =>
val peerHandlerF = buildPeerHandler()
val peerHandlerF = bitcoindPeerF.map(p => NodeUnitTest.buildPeerHandler(p))
val peerMsgSenderF = peerHandlerF.map(_.peerMsgSender)
val peerMsgRecvF = peerHandlerF.map(_.peerMsgRecv)

View File

@ -232,7 +232,7 @@ trait ChainUnitTest
}
def createBitcoindChainHandlerViaZmq(): Future[BitcoindChainHandlerViaZmq] = {
composeBuildersAndWrap(() => createBitcoind,
composeBuildersAndWrap(() => BitcoinSFixture.createBitcoind,
createChainHandlerWithBitcoindZmq,
BitcoindChainHandlerViaZmq.apply)()
}
@ -269,7 +269,7 @@ trait ChainUnitTest
def withBitcoindChainHandlerViaZmq(test: OneArgAsyncTest)(
implicit system: ActorSystem): FutureOutcome = {
val builder: () => Future[BitcoindChainHandlerViaZmq] =
composeBuildersAndWrap(builder = () => createBitcoind,
composeBuildersAndWrap(builder = () => BitcoinSFixture.createBitcoind,
dependentBuilder =
createChainHandlerWithBitcoindZmq,
wrap = BitcoindChainHandlerViaZmq.apply)
@ -280,7 +280,7 @@ trait ChainUnitTest
def withBitcoindChainHandlerViaRpc(test: OneArgAsyncTest)(
implicit system: ActorSystem): FutureOutcome = {
val builder: () => Future[BitcoindChainHandlerViaRpc] = { () =>
createBitcoind().flatMap(createChainApiWithBitcoindRpc(_))
BitcoinSFixture.createBitcoind().flatMap(createChainApiWithBitcoindRpc(_))
}
makeDependentFixture(builder, destroyBitcoindChainApiViaRpc)(test)

View File

@ -128,6 +128,10 @@ object TransactionGenerators extends BitcoinSLogger {
def smallTransactions: Gen[Seq[Transaction]] =
Gen.choose(0, 10).flatMap(i => Gen.listOfN(i, transaction))
def nonEmptySmallTransactions: Gen[Seq[Transaction]] = {
Gen.choose(1, 10).flatMap(i => Gen.listOfN(i, transaction))
}
def transaction: Gen[Transaction] =
Gen.oneOf(baseTransaction, witnessTransaction)

View File

@ -1,6 +1,7 @@
package org.bitcoins.testkit.fixtures
import akka.actor.ActorSystem
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.scalatest._
@ -8,7 +9,7 @@ import org.scalatest._
import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success}
trait BitcoinSFixture extends fixture.AsyncFlatSpec {
trait BitcoinSFixture extends fixture.AsyncFlatSpec with BitcoinSLogger {
/**
* Given functions to build and destroy a fixture, returns a OneArgAsyncTest => FutureOutcome
@ -33,7 +34,9 @@ trait BitcoinSFixture extends fixture.AsyncFlatSpec {
fixtureF.foreach { fixture =>
destroy(fixture).onComplete {
case Success(_) => destroyP.success(())
case Failure(err) => destroyP.failure(err)
case Failure(err) =>
logger.error(s"Failed to destroy fixture with err=${err}")
destroyP.failure(err)
}
}
}
@ -130,8 +133,13 @@ trait BitcoinSFixture extends fixture.AsyncFlatSpec {
}
}
}
object BitcoinSFixture {
def createBitcoindWithFunds()(
implicit system: ActorSystem): Future[BitcoindRpcClient] = {
import system.dispatcher
for {
bitcoind <- createBitcoind()
address <- bitcoind.getNewAddress
@ -142,6 +150,7 @@ trait BitcoinSFixture extends fixture.AsyncFlatSpec {
/** Creates a new bitcoind instance */
def createBitcoind()(
implicit system: ActorSystem): Future[BitcoindRpcClient] = {
import system.dispatcher
val instance = BitcoindRpcTestUtil.instance()
val bitcoind = new BitcoindRpcClient(instance)

View File

@ -1,11 +1,10 @@
package org.bitcoins.testkit.fixtures
import org.bitcoins.node.models.BroadcastAbleTransactionDAO
import org.scalatest._
import org.bitcoins.testkit.node.NodeUnitTest
import slick.jdbc.SQLiteProfile
import org.bitcoins.node.db.NodeDbManagement
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.BroadcastAbleTransactionDAO
import org.bitcoins.testkit.node.NodeUnitTest
import org.scalatest._
import slick.jdbc.SQLiteProfile
case class NodeDAOs(txDAO: BroadcastAbleTransactionDAO)
@ -18,9 +17,8 @@ trait NodeDAOFixture extends fixture.AsyncFlatSpec with NodeUnitTest {
final override type FixtureParam = NodeDAOs
implicit private val nodeConfig: NodeAppConfig = config
def withFixture(test: OneArgAsyncTest): FutureOutcome =
makeFixture(build = () => NodeDbManagement.createAll().map(_ => daos),
destroy = () => NodeDbManagement.dropAll())(test)
makeFixture(
build = () => NodeDbManagement.createAll()(nodeConfig, ec).map(_ => daos),
destroy = () => NodeDbManagement.dropAll()(nodeConfig, ec))(test)
}

View File

@ -3,10 +3,14 @@ package org.bitcoins.testkit.node
import java.net.InetSocketAddress
import akka.actor.ActorSystem
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.chain.config.ChainAppConfig
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.db.AppConfig
import org.bitcoins.node.SpvNode
import org.bitcoins.node.{SpvNode, SpvNodeCallbacks}
import org.bitcoins.node.config.NodeAppConfig
import org.bitcoins.node.models.Peer
import org.bitcoins.node.networking.peer.{
PeerHandler,
@ -16,10 +20,14 @@ import org.bitcoins.node.networking.peer.{
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.server.BitcoinSAppConfig._
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.chain.ChainUnitTest
import org.bitcoins.testkit.fixtures.BitcoinSFixture
import org.bitcoins.testkit.node.NodeUnitTest.SpvNodeFundedWalletBitcoind
import org.bitcoins.testkit.node.fixture.SpvNodeConnectedWithBitcoind
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.scalatest.{
BeforeAndAfter,
BeforeAndAfterAll,
@ -29,10 +37,6 @@ import org.scalatest.{
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.chain.blockchain.ChainHandler
import org.bitcoins.chain.models.BlockHeaderDAO
import org.bitcoins.node.SpvNodeCallbacks
trait NodeUnitTest
extends BitcoinSFixture
@ -60,83 +64,55 @@ trait NodeUnitTest
val timeout: FiniteDuration = 10.seconds
/** Wallet config with data directory set to user temp directory */
implicit protected lazy val config: BitcoinSAppConfig =
implicit protected def config: BitcoinSAppConfig =
BitcoinSTestAppConfig.getTestConfig()
implicit protected lazy val chainConfig: ChainAppConfig = config.chainConf
implicit protected lazy val nodeConfig: NodeAppConfig = config.nodeConf
implicit lazy val np: NetworkParameters = config.nodeConf.network
lazy val startedBitcoindF = BitcoindRpcTestUtil.startedBitcoindRpcClient()
lazy val bitcoindPeerF = startedBitcoindF.map(NodeTestUtil.getBitcoindPeer)
def buildPeerMessageReceiver(): PeerMessageReceiver = {
val dao = BlockHeaderDAO()
val chainHandler = ChainHandler(dao)
val receiver =
PeerMessageReceiver.newReceiver(chainHandler, SpvNodeCallbacks.empty)
receiver
}
def buildPeerHandler(): Future[PeerHandler] = {
bitcoindPeerF.map { peer =>
val peerMsgReceiver = buildPeerMessageReceiver()
//the problem here is the 'self', this needs to be an ordinary peer message handler
//that can handle the handshake
val peerMsgSender: PeerMessageSender = {
val client = NodeTestUtil.client(peer, peerMsgReceiver)
PeerMessageSender(client)
}
PeerHandler(peerMsgReceiver, peerMsgSender)
}
}
def peerSocketAddress(
bitcoindRpcClient: BitcoindRpcClient): InetSocketAddress = {
NodeTestUtil.getBitcoindSocketAddress(bitcoindRpcClient)
}
def createPeer(bitcoind: BitcoindRpcClient): Peer = {
val socket = peerSocketAddress(bitcoind)
Peer(id = None, socket = socket)
}
def createSpvNode(bitcoind: BitcoindRpcClient): Future[SpvNode] = {
val chainApiF = ChainUnitTest.createChainHandler()
val peer = createPeer(bitcoind)
for {
chainApi <- chainApiF
} yield
SpvNode(peer = peer,
chainApi = chainApi,
bloomFilter = NodeTestUtil.emptyBloomFilter)
}
def withSpvNode(test: OneArgAsyncTest)(
implicit system: ActorSystem): FutureOutcome = {
implicit system: ActorSystem,
appConfig: BitcoinSAppConfig): FutureOutcome = {
val spvBuilder: () => Future[SpvNode] = { () =>
val bitcoindF = createBitcoind()
val bitcoindF = BitcoinSFixture.createBitcoind()
bitcoindF.flatMap { bitcoind =>
createSpvNode(bitcoind).flatMap(_.start())
NodeUnitTest
.createSpvNode(bitcoind, SpvNodeCallbacks.empty)(system,
appConfig.chainConf,
appConfig.nodeConf)
.flatMap(_.start())
}
}
makeDependentFixture(
build = spvBuilder,
destroy = NodeUnitTest.destroySpvNode
destroy =
NodeUnitTest.destroySpvNode(_: SpvNode)(appConfig, system.dispatcher)
)(test)
}
def withSpvNodeConnectedToBitcoind(test: OneArgAsyncTest)(
implicit system: ActorSystem): FutureOutcome = {
implicit system: ActorSystem,
appConfig: BitcoinSAppConfig): FutureOutcome = {
val spvWithBitcoindBuilder: () => Future[SpvNodeConnectedWithBitcoind] = {
() =>
val bitcoindF = createBitcoind()
val bitcoindF = BitcoinSFixture.createBitcoind()
bitcoindF.flatMap { bitcoind =>
val startedSpv = createSpvNode(bitcoind).flatMap(_.start())
val spvNode = NodeUnitTest
.createSpvNode(bitcoind, SpvNodeCallbacks.empty)(
system,
appConfig.chainConf,
appConfig.nodeConf)
val startedSpv = spvNode
.flatMap(_.start())
startedSpv.map(spv => SpvNodeConnectedWithBitcoind(spv, bitcoind))
}
@ -144,13 +120,39 @@ trait NodeUnitTest
makeDependentFixture(
build = spvWithBitcoindBuilder,
destroy = NodeUnitTest.destorySpvNodeConnectedWithBitcoind
destroy = NodeUnitTest.destorySpvNodeConnectedWithBitcoind(
_: SpvNodeConnectedWithBitcoind)(system, appConfig)
)(test)
}
def withSpvNodeFundedWalletBitcoind(
test: OneArgAsyncTest,
callbacks: SpvNodeCallbacks)(
implicit system: ActorSystem,
appConfig: BitcoinSAppConfig): FutureOutcome = {
makeDependentFixture(
build = () =>
NodeUnitTest.createSpvNodeFundedWalletBitcoind(callbacks)(system,
appConfig),
destroy = NodeUnitTest.destroySpvNodeFundedWalletBitcoind(
_: SpvNodeFundedWalletBitcoind)(system, appConfig)
)(test)
}
}
object NodeUnitTest {
object NodeUnitTest extends BitcoinSLogger {
/**
* Creates
* 1. a funded bitcoind wallet
* 2. a funded bitcoin-s wallet
* 3. a chain handler with the appropriate tables created
* 4. a spv node that is connected to the bitcoin instance -- but not started! */
case class SpvNodeFundedWalletBitcoind(
spvNode: SpvNode,
wallet: UnlockedWalletApi,
bitcoindRpc: BitcoindRpcClient)
def destroySpvNode(spvNode: SpvNode)(
implicit config: BitcoinSAppConfig,
@ -174,4 +176,93 @@ object NodeUnitTest {
_ <- bitcoindDestroyF
} yield ()
}
/** Creates a spv node, a funded bitcoin-s wallet, all of which are connected to bitcoind */
def createSpvNodeFundedWalletBitcoind(callbacks: SpvNodeCallbacks)(
implicit system: ActorSystem,
appConfig: BitcoinSAppConfig): Future[SpvNodeFundedWalletBitcoind] = {
import system.dispatcher
val fundedWalletF = BitcoinSWalletTest.fundedWalletAndBitcoind()
for {
fundedWallet <- fundedWalletF
spvNode <- createSpvNode(fundedWallet.bitcoind, callbacks)
} yield {
SpvNodeFundedWalletBitcoind(spvNode = spvNode,
wallet = fundedWallet.wallet,
bitcoindRpc = fundedWallet.bitcoind)
}
}
def destroySpvNodeFundedWalletBitcoind(
fundedWalletBitcoind: SpvNodeFundedWalletBitcoind)(
implicit system: ActorSystem,
appConfig: BitcoinSAppConfig): Future[Unit] = {
import system.dispatcher
val walletWithBitcoind = {
BitcoinSWalletTest.WalletWithBitcoind(fundedWalletBitcoind.wallet,
fundedWalletBitcoind.bitcoindRpc)
}
val destroyedF = for {
_ <- destroySpvNode(fundedWalletBitcoind.spvNode)
_ <- BitcoinSWalletTest.destroyWalletWithBitcoind(walletWithBitcoind)
} yield ()
destroyedF
}
def buildPeerMessageReceiver()(
implicit system: ActorSystem,
chainAppConfig: ChainAppConfig,
nodeAppConfig: NodeAppConfig): PeerMessageReceiver = {
import system.dispatcher
val dao = BlockHeaderDAO()
val chainHandler = ChainHandler(dao)
val receiver =
PeerMessageReceiver.newReceiver(chainHandler, SpvNodeCallbacks.empty)
receiver
}
def buildPeerHandler(peer: Peer)(
implicit system: ActorSystem,
chainAppConfig: ChainAppConfig,
nodeAppConfig: NodeAppConfig): PeerHandler = {
val peerMsgReceiver = buildPeerMessageReceiver()
//the problem here is the 'self', this needs to be an ordinary peer message handler
//that can handle the handshake
val peerMsgSender: PeerMessageSender = {
val client = NodeTestUtil.client(peer, peerMsgReceiver)
PeerMessageSender(client)
}
PeerHandler(peerMsgReceiver, peerMsgSender)
}
def peerSocketAddress(
bitcoindRpcClient: BitcoindRpcClient): InetSocketAddress = {
NodeTestUtil.getBitcoindSocketAddress(bitcoindRpcClient)
}
def createPeer(bitcoind: BitcoindRpcClient): Peer = {
val socket = peerSocketAddress(bitcoind)
Peer(id = None, socket = socket)
}
def createSpvNode(bitcoind: BitcoindRpcClient, callbacks: SpvNodeCallbacks)(
implicit system: ActorSystem,
chainAppConfig: ChainAppConfig,
nodeAppConfig: NodeAppConfig): Future[SpvNode] = {
import system.dispatcher
val chainApiF = ChainUnitTest.createChainHandler()
val peer = createPeer(bitcoind)
for {
chainApi <- chainApiF
} yield {
SpvNode(peer = peer,
chainApi = chainApi,
bloomFilter = NodeTestUtil.emptyBloomFilter,
callbacks = callbacks)
}
}
}

View File

@ -30,6 +30,7 @@ trait BitcoinSWalletTest
with BitcoinSFixture
with BeforeAndAfterAll
with BitcoinSLogger {
import BitcoinSWalletTest._
implicit val actorSystem: ActorSystem = ActorSystem(getClass.getSimpleName)
implicit val ec: ExecutionContext = actorSystem.dispatcher
@ -52,48 +53,6 @@ trait BitcoinSWalletTest
AppConfig.throwIfDefaultDatadir(config.walletConf)
}
def destroyWallet(wallet: UnlockedWalletApi): Future[Unit] = {
WalletDbManagement
.dropAll()(config = wallet.walletConfig,
ec = implicitly[ExecutionContext])
.map(_ => ())
}
/** Returns a function that can be used to create a wallet fixture.
* If you pass in a configuration to this method that configuration
* is given to the wallet as user-provided overrides. You could for
* example use this to override the default data directory, network
* or account type.
*/
private def createNewWallet(
extraConfig: Option[Config]): () => Future[UnlockedWalletApi] =
() => {
val defaultConf = config.walletConf
val walletConfig = extraConfig match {
case None => defaultConf
case Some(c) => defaultConf.withOverrides(c)
}
// we want to check we're not overwriting
// any user data
AppConfig.throwIfDefaultDatadir(walletConfig)
walletConfig.initialize().flatMap { _ =>
Wallet
.initialize()(implicitly[ExecutionContext], walletConfig)
.map {
case InitializeWalletSuccess(wallet) => wallet
case err: InitializeWalletError =>
logger.error(s"Could not initialize wallet: $err")
fail(err)
}
}
}
/** Creates a wallet with the default configuration */
private def createDefaultWallet(): Future[UnlockedWalletApi] =
createNewWallet(None)() // get the standard config
/** Lets you customize the parameters for the created wallet */
val withNewConfiguredWallet: Config => OneArgAsyncTest => FutureOutcome =
walletConfig =>
@ -118,27 +77,6 @@ trait BitcoinSWalletTest
makeDependentFixture(build = createDefaultWallet _,
destroy = destroyWallet)(test)
case class WalletWithBitcoind(
wallet: UnlockedWalletApi,
bitcoind: BitcoindRpcClient)
def createWalletWithBitcoind(
wallet: UnlockedWalletApi): Future[WalletWithBitcoind] = {
val bitcoindF = createBitcoindWithFunds()
bitcoindF.map(WalletWithBitcoind(wallet, _))
}
def destroyWalletWithBitcoind(
walletWithBitcoind: WalletWithBitcoind): Future[Unit] = {
val WalletWithBitcoind(wallet, bitcoind) = walletWithBitcoind
val stopF = bitcoind.stop()
val destroyWalletF = destroyWallet(wallet)
for {
_ <- stopF
_ <- destroyWalletF
} yield ()
}
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
builder = createDefaultWallet _,
@ -151,9 +89,97 @@ trait BitcoinSWalletTest
}
def withFundedWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] =
composeBuildersAndWrapFuture(
builder = createDefaultWallet _,
dependentBuilder = createWalletWithBitcoind,
processResult = (_: UnlockedWalletApi, pair: WalletWithBitcoind) =>
fundWalletWithBitcoind(pair)
)
makeDependentFixture(builder, destroy = destroyWalletWithBitcoind)(test)
}
}
object BitcoinSWalletTest extends BitcoinSLogger {
case class WalletWithBitcoind(
wallet: UnlockedWalletApi,
bitcoind: BitcoindRpcClient)
/** Returns a function that can be used to create a wallet fixture.
* If you pass in a configuration to this method that configuration
* is given to the wallet as user-provided overrides. You could for
* example use this to override the default data directory, network
* or account type.
*/
private def createNewWallet(extraConfig: Option[Config])(
implicit config: BitcoinSAppConfig,
ec: ExecutionContext): () => Future[UnlockedWalletApi] =
() => {
val defaultConf = config.walletConf
val walletConfig = extraConfig match {
case None => defaultConf
case Some(c) => defaultConf.withOverrides(c)
}
// we want to check we're not overwriting
// any user data
AppConfig.throwIfDefaultDatadir(walletConfig)
walletConfig.initialize().flatMap { _ =>
Wallet
.initialize()(implicitly[ExecutionContext], walletConfig)
.map {
case InitializeWalletSuccess(wallet) => wallet
case err: InitializeWalletError =>
logger.error(s"Could not initialize wallet: $err")
throw new RuntimeException(
s"Failed to intialize wallet in fixture with err=${err}")
}
}
}
/** Creates a wallet with the default configuration */
private def createDefaultWallet()(
implicit config: BitcoinSAppConfig,
ec: ExecutionContext): Future[UnlockedWalletApi] =
createNewWallet(None)(config, ec)() // get the standard config
/** Pairs the given wallet with a bitcoind instance that has money in the bitcoind wallet */
def createWalletWithBitcoind(wallet: UnlockedWalletApi)(
implicit system: ActorSystem): Future[WalletWithBitcoind] = {
import system.dispatcher
val bitcoindF = BitcoinSFixture.createBitcoindWithFunds()
bitcoindF.map(WalletWithBitcoind(wallet, _))
}
/** Creates a default wallet, and then pairs it with a bitcoind instance that has money in the bitcoind wallet */
def createWalletWithBitcoind()(
implicit system: ActorSystem,
config: BitcoinSAppConfig): Future[WalletWithBitcoind] = {
import system.dispatcher
val unlockedWalletApiF = createDefaultWallet()
unlockedWalletApiF.flatMap(u => createWalletWithBitcoind(u))
}
/** Gives us a funded bitcoin-s wallet and the bitcoind instance that funded that wallet */
def fundedWalletAndBitcoind()(
implicit config: BitcoinSAppConfig,
system: ActorSystem): Future[WalletWithBitcoind] = {
import system.dispatcher
for {
wallet <- createDefaultWallet()
withBitcoind <- createWalletWithBitcoind(wallet)
funded <- fundWalletWithBitcoind(withBitcoind)
} yield funded
}
/** Funds the given wallet with money from the given bitcoind */
def fundWalletWithBitcoind(
pair: WalletWithBitcoind): Future[WalletWithBitcoind] = {
def fundWalletWithBitcoind(pair: WalletWithBitcoind)(
implicit ec: ExecutionContext): Future[WalletWithBitcoind] = {
val WalletWithBitcoind(wallet, bitcoind) = pair
for {
addr <- wallet.getNewAddress()
@ -171,16 +197,22 @@ trait BitcoinSWalletTest
}
}
def withFundedWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] =
composeBuildersAndWrapFuture(
builder = createDefaultWallet _,
dependentBuilder = createWalletWithBitcoind,
processResult = (_: UnlockedWalletApi, pair: WalletWithBitcoind) =>
fundWalletWithBitcoind(pair)
)
def destroyWalletWithBitcoind(walletWithBitcoind: WalletWithBitcoind)(
implicit ec: ExecutionContext): Future[Unit] = {
val WalletWithBitcoind(wallet, bitcoind) = walletWithBitcoind
val stopF = bitcoind.stop()
val destroyWalletF = destroyWallet(wallet)
for {
_ <- stopF
_ <- destroyWalletF
} yield ()
}
makeDependentFixture(builder, destroy = destroyWalletWithBitcoind)(test)
def destroyWallet(wallet: UnlockedWalletApi)(
implicit ec: ExecutionContext): Future[Unit] = {
WalletDbManagement
.dropAll()(config = wallet.walletConfig, ec = ec)
.map(_ => ())
}
}

View File

@ -8,6 +8,7 @@ import org.bitcoins.wallet.api.UnlockWalletSuccess
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.currency._
import org.bitcoins.testkit.Implicits._
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.WalletWithBitcoind
class WalletBloomTest extends BitcoinSWalletTest {
behavior of "Wallet bloom filter"

View File

@ -10,6 +10,7 @@ import org.scalatest.FutureOutcome
import scala.concurrent.Future
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.testkit.wallet.BitcoinSWalletTest.WalletWithBitcoind
class WalletIntegrationTest extends BitcoinSWalletTest {