Watch arbitrary SPKs (#1860)

* Watch arbitrary SPKs

* fix unit tests

* fix unit tests

* one more fix

* revert the compiler parameters
This commit is contained in:
rorp 2020-08-20 12:33:18 -07:00 committed by GitHub
parent 113f97946c
commit 2f8dcd1e57
9 changed files with 141 additions and 35 deletions

View File

@ -1,8 +1,11 @@
package org.bitcoins.node
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.script.MultiSignatureScriptPubKey
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutput}
import org.bitcoins.core.util.EnvUtil
import org.bitcoins.core.wallet.fee.SatoshisPerByte
import org.bitcoins.crypto.ECPublicKey
import org.bitcoins.rpc.client.common.BitcoindVersion
import org.bitcoins.rpc.util.AsyncUtil
import org.bitcoins.server.BitcoinSAppConfig
@ -14,7 +17,7 @@ import org.bitcoins.testkit.node.{
NodeUnitTest
}
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.{OnTransactionProcessed, Wallet, WalletCallbacks}
import org.scalatest.FutureOutcome
import scala.concurrent.{Future, Promise}
@ -36,14 +39,19 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
} else {
withNeutrinoNodeFundedWalletBitcoind(
test = test,
callbacks = callbacks,
nodeCallbacks = nodeCallbacks,
walletCallbacks = walletCallbacks,
bip39PasswordOpt = getBIP39PasswordOpt(),
versionOpt = Some(BitcoindVersion.Experimental))
versionOpt = Some(BitcoindVersion.Experimental)
)
}
}
// unlike other mutable collection types java.util.Vector is thread safe
private var txs = new java.util.Vector[Transaction]()
private var walletP: Promise[Wallet] = Promise()
private var walletF: Future[Wallet] = walletP.future
after {
//reset assertion after a test runs, because we
//are doing mutation to work around our callback
@ -51,13 +59,24 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
//after a NeutrinoNode is constructed :-(
walletP = Promise()
walletF = walletP.future
txs = new java.util.Vector[Transaction]()
}
val TestAmount = 1.bitcoin
val FeeRate = SatoshisPerByte(10.sats)
val TestFees = 2240.sats
def callbacks: NodeCallbacks = {
def walletCallbacks: WalletCallbacks = {
val onTxProcessed: OnTransactionProcessed = { tx =>
Future {
txs.add(tx)
()
}
}
WalletCallbacks(onTransactionProcessed = Vector(onTxProcessed))
}
def nodeCallbacks: NodeCallbacks = {
val onBlock: OnBlockReceived = { block =>
for {
wallet <- walletF
@ -153,6 +172,51 @@ class NeutrinoNodeWithWalletTest extends NodeUnitTest {
} yield succeed
}
it must "watch an arbitrary SPKs" taggedAs UsesExperimentalBitcoind in {
param =>
val NeutrinoNodeFundedWalletBitcoind(node, wallet, bitcoind, _) = param
walletP.success(wallet)
def generateBlock() =
for {
_ <-
bitcoind.getNewAddress
.flatMap(bitcoind.generateToAddress(1, _))
_ <- NodeTestUtil.awaitSync(node, bitcoind)
_ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind)
} yield ()
val pk1 = ECPublicKey.freshPublicKey
val pk2 = ECPublicKey.freshPublicKey
val spk = MultiSignatureScriptPubKey(2, Vector(pk1, pk2))
val sats = TestAmount
val output = TransactionOutput(sats, spk)
for {
_ <- node.sync()
_ <- NodeTestUtil.awaitSync(node, bitcoind)
_ <- NodeTestUtil.awaitCompactFiltersSync(node, bitcoind)
// start watching
_ <- wallet.watchScriptPubKey(spk)
// send
txSent <- wallet.sendToOutputs(Vector(output), FeeRate)
_ <- node.broadcastTransaction(txSent)
// confirm
_ <- generateBlock()
_ <- generateBlock()
_ <- generateBlock()
// verify
_ <- AsyncUtil.awaitConditionF { () =>
Future { txs.contains(txSent) }
}
} yield succeed
}
it must "rescan and receive information about received payments" taggedAs UsesExperimentalBitcoind in {
param =>
val NeutrinoNodeFundedWalletBitcoind(node, wallet, bitcoind, _) = param

View File

@ -55,6 +55,7 @@ class SpvNodeWithWalletTest extends NodeUnitTest {
assertionP.success(result)
}
}
()
}
}
NodeCallbacks(

View File

@ -7,6 +7,7 @@ import sbt.Keys._
import scala.util.Properties
object CommonSettings {
private val isCI = {
sys.props
.get("CI")
@ -38,21 +39,18 @@ object CommonSettings {
apiURL := homepage.value.map(_.toString + "/api").map(url(_)),
// scaladoc settings end
////
scalacOptions in Compile := compilerOpts(scalaVersion.value),
//remove annoying import unused things in the scala console
//https://stackoverflow.com/questions/26940253/in-sbt-how-do-you-override-scalacoptions-for-console-in-all-configurations
scalacOptions in (Compile, console) ~= (_ filterNot (s =>
s == "-Ywarn-unused-import"
|| s =="-Ywarn-unused"
|| s =="-Xfatal-warnings"
//for 2.13 -- they use different compiler opts
|| s == "-Ywarn-unused"
|| s == "-Xfatal-warnings"
//for 2.13 -- they use different compiler opts
|| s == "-Xlint:unused")),
//we don't want -Xfatal-warnings for publishing with publish/publishLocal either
scalacOptions in (Compile,doc) ~= (_ filterNot (s =>
scalacOptions in (Compile, doc) ~= (_ filterNot (s =>
s == "-Xfatal-warnings")),
scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value,
scalacOptions in Test := testCompilerOpts,
Compile / compile / javacOptions ++= {
@ -77,7 +75,7 @@ object CommonSettings {
)
}
private val scala2_13CompilerOpts = Seq("-Xlint:unused","-Xfatal-warnings")
private val scala2_13CompilerOpts = Seq("-Xlint:unused", "-Xfatal-warnings")
private val nonScala2_13CompilerOpts = Seq(
"-Xmax-classfile-name",
@ -123,5 +121,6 @@ object CommonSettings {
lazy val prodSettings: Seq[Setting[_]] = settings
lazy val binariesPath = Paths.get(Properties.userHome, ".bitcoin-s", "binaries")
lazy val binariesPath =
Paths.get(Properties.userHome, ".bitcoin-s", "binaries")
}

View File

@ -44,6 +44,7 @@ import org.bitcoins.testkit.node.fixture.{
}
import org.bitcoins.testkit.rpc.BitcoindRpcTestUtil
import org.bitcoins.testkit.wallet.{BitcoinSWalletTest, WalletWithBitcoindRpc}
import org.bitcoins.wallet.WalletCallbacks
import org.scalatest.FutureOutcome
import scala.concurrent.duration._
@ -250,11 +251,13 @@ trait NodeUnitTest extends BitcoinSFixture with EmbeddedPg {
makeDependentFixture(
build = () =>
NodeUnitTest.createSpvNodeFundedWalletBitcoind(callbacks = callbacks,
NodeUnitTest.createSpvNodeFundedWalletBitcoind(nodeCallbacks =
callbacks,
bip39PasswordOpt =
bip39PasswordOpt,
versionOpt =
Option(V18))(
versionOpt = Option(V18),
walletCallbacks =
WalletCallbacks.empty)(
system, // Force V18 because Spv is disabled on versions after
appConfig),
destroy = NodeUnitTest.destroyNodeFundedWalletBitcoind(
@ -264,9 +267,10 @@ trait NodeUnitTest extends BitcoinSFixture with EmbeddedPg {
def withNeutrinoNodeFundedWalletBitcoind(
test: OneArgAsyncTest,
callbacks: NodeCallbacks,
nodeCallbacks: NodeCallbacks,
bip39PasswordOpt: Option[String],
versionOpt: Option[BitcoindVersion] = None)(implicit
versionOpt: Option[BitcoindVersion] = None,
walletCallbacks: WalletCallbacks = WalletCallbacks.empty)(implicit
system: ActorSystem,
appConfig: BitcoinSAppConfig): FutureOutcome = {
@ -274,9 +278,10 @@ trait NodeUnitTest extends BitcoinSFixture with EmbeddedPg {
build = () =>
NodeUnitTest
.createNeutrinoNodeFundedWalletBitcoind(
callbacks,
nodeCallbacks,
bip39PasswordOpt,
versionOpt)(system, appConfig),
versionOpt,
walletCallbacks)(system, appConfig),
destroy = NodeUnitTest.destroyNodeFundedWalletBitcoind(
_: NodeFundedWalletBitcoind)(system, appConfig)
)(test)
@ -372,7 +377,8 @@ object NodeUnitTest extends P2PLogger {
/** Creates a spv node, a funded bitcoin-s wallet, all of which are connected to bitcoind */
def createSpvNodeFundedWalletBitcoind(
callbacks: NodeCallbacks,
nodeCallbacks: NodeCallbacks,
walletCallbacks: WalletCallbacks,
bip39PasswordOpt: Option[String],
versionOpt: Option[BitcoindVersion] = None)(implicit
system: ActorSystem,
@ -381,12 +387,13 @@ object NodeUnitTest extends P2PLogger {
require(appConfig.isSPVEnabled && !appConfig.isNeutrinoEnabled)
for {
bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt)
node <- createSpvNode(bitcoind, callbacks)
node <- createSpvNode(bitcoind, nodeCallbacks)
fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(
bitcoind,
node,
node,
bip39PasswordOpt)
bip39PasswordOpt,
walletCallbacks)
} yield {
SpvNodeFundedWalletBitcoind(node = node,
wallet = fundedWallet.wallet,
@ -397,9 +404,10 @@ object NodeUnitTest extends P2PLogger {
/** Creates a neutrino node, a funded bitcoin-s wallet, all of which are connected to bitcoind */
def createNeutrinoNodeFundedWalletBitcoind(
callbacks: NodeCallbacks,
nodeCallbacks: NodeCallbacks,
bip39PasswordOpt: Option[String],
versionOpt: Option[BitcoindVersion])(implicit
versionOpt: Option[BitcoindVersion],
walletCallbacks: WalletCallbacks)(implicit
system: ActorSystem,
appConfig: BitcoinSAppConfig): Future[
NeutrinoNodeFundedWalletBitcoind] = {
@ -407,12 +415,13 @@ object NodeUnitTest extends P2PLogger {
require(appConfig.isNeutrinoEnabled && !appConfig.isSPVEnabled)
for {
bitcoind <- BitcoinSFixture.createBitcoindWithFunds(versionOpt)
node <- createNeutrinoNode(bitcoind, callbacks)
node <- createNeutrinoNode(bitcoind, nodeCallbacks)
fundedWallet <- BitcoinSWalletTest.fundedWalletAndBitcoind(
bitcoindRpcClient = bitcoind,
nodeApi = node,
chainQueryApi = node,
bip39PasswordOpt = bip39PasswordOpt)
bip39PasswordOpt = bip39PasswordOpt,
walletCallbacks = walletCallbacks)
} yield {
NeutrinoNodeFundedWalletBitcoind(node = node,
wallet = fundedWallet.wallet,

View File

@ -26,7 +26,7 @@ import org.bitcoins.testkit.util.FileUtil
import org.bitcoins.testkit.wallet.FundWalletUtil.FundedWallet
import org.bitcoins.testkit.{BitcoinSTestAppConfig, EmbeddedPg}
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.{Wallet, WalletLogger}
import org.bitcoins.wallet.{Wallet, WalletCallbacks, WalletLogger}
import org.scalatest._
import scala.concurrent.{
@ -564,10 +564,12 @@ object BitcoinSWalletTest extends WalletLogger {
versionOpt: Option[BitcoindVersion],
nodeApi: NodeApi,
bip39PasswordOpt: Option[String],
chainQueryApi: ChainQueryApi)(implicit
chainQueryApi: ChainQueryApi,
walletCallbacks: WalletCallbacks)(implicit
config: BitcoinSAppConfig,
system: ActorSystem): Future[WalletWithBitcoind] = {
import system.dispatcher
config.walletConf.addCallbacks(walletCallbacks)
for {
wallet <- BitcoinSWalletTest.createWallet2Accounts(nodeApi,
chainQueryApi,
@ -581,10 +583,12 @@ object BitcoinSWalletTest extends WalletLogger {
bitcoindRpcClient: BitcoindRpcClient,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
bip39PasswordOpt: Option[String])(implicit
bip39PasswordOpt: Option[String],
walletCallbacks: WalletCallbacks)(implicit
config: BitcoinSAppConfig,
system: ActorSystem): Future[WalletWithBitcoind] = {
import system.dispatcher
config.walletConf.addCallbacks(walletCallbacks)
for {
wallet <- BitcoinSWalletTest.createWallet2Accounts(
nodeApi = nodeApi,

View File

@ -1,6 +1,7 @@
package org.bitcoins.wallet
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
import org.bitcoins.core.protocol.script.EmptyScriptPubKey
import org.bitcoins.core.protocol.transaction.TransactionOutput
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.StorageLocationTag.HotStorage
@ -244,4 +245,20 @@ class AddressHandlingTest extends BitcoinSWalletTest {
assert(hotStorageTags.head.address == addr)
}
}
it must "add public key scripts to watch" in { fundedWallet: FundedWallet =>
val wallet = fundedWallet.wallet
val spk = EmptyScriptPubKey
for {
before <- wallet.listScriptPubKeys()
spkDb <- wallet.watchScriptPubKey(spk)
after <- wallet.listScriptPubKeys()
} yield {
assert(before.size + 1 == after.size)
assert(spkDb.scriptPubKey == spk)
assert(spkDb.id.nonEmpty)
assert(!before.contains(spkDb))
assert(after.contains(spkDb))
}
}
}

View File

@ -140,11 +140,9 @@ abstract class Wallet
Wallet] = {
for {
utxos <- listUtxos()
addresses <- listAddresses()
scripts <- listScriptPubKeys()
scriptPubKeys =
utxos.flatMap(_.redeemScriptOpt).toSet ++ addresses
.map(_.scriptPubKey)
.toSet
utxos.flatMap(_.redeemScriptOpt).toSet ++ scripts.map(_.scriptPubKey)
_ <- FutureUtil.sequentially(blockFilters) {
case (blockHash, blockFilter) =>
val matcher = SimpleFilterMatcher(blockFilter)

View File

@ -10,6 +10,7 @@ import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.core.currency.CurrencyUnit
import org.bitcoins.core.hd.AddressType
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script.ScriptPubKey
import org.bitcoins.core.protocol.transaction.{
Transaction,
TransactionOutPoint,
@ -24,6 +25,7 @@ import org.bitcoins.wallet.WalletLogger
import org.bitcoins.wallet.models.{
AddressDb,
AddressTagDb,
ScriptPubKeyDb,
SpendingInfoDb,
TransactionDb
}
@ -129,6 +131,10 @@ trait WalletApi extends WalletLogger {
def listUnusedAddresses(): Future[Vector[AddressDb]]
def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]]
def watchScriptPubKey(scriptPubKey: ScriptPubKey): Future[ScriptPubKeyDb]
def markUTXOsAsReserved(
utxos: Vector[SpendingInfoDb]): Future[Vector[SpendingInfoDb]]

View File

@ -20,7 +20,8 @@ import org.bitcoins.wallet.models.{
AccountDb,
AddressDb,
AddressDbHelper,
AddressTagDb
AddressTagDb,
ScriptPubKeyDb
}
import scala.concurrent.{Await, Future, Promise, TimeoutException}
@ -108,6 +109,13 @@ private[wallet] trait AddressHandling extends WalletLogger {
}
}
override def listScriptPubKeys(): Future[Vector[ScriptPubKeyDb]] =
scriptPubKeyDAO.findAll()
override def watchScriptPubKey(
scriptPubKey: ScriptPubKey): Future[ScriptPubKeyDb] =
scriptPubKeyDAO.create(ScriptPubKeyDb(scriptPubKey = scriptPubKey))
/** Enumerates the public keys in this wallet */
protected[wallet] def listPubkeys(): Future[Vector[ECPublicKey]] =
addressDAO.findAllPubkeys()