2021 04 24 bitcoin s scripts (#2961)

* Add new app/scripts project which is meant to store useful bitcoin-s scripts

* Add ScanBitcoind with an example of counting segwit txs between a range

* Fix bug of creating a new actor system everytime BitcoindRpcClient.apply(instance) is called

* Add BitcoindRpcClient.fromVersionNoSystem()

* Take ben's suggestions

* Fix compile

* Rework P2SHScriptSignature.isStandardNonP2SH() to account for nesting p2sh script inside of it

* fix compile on java8

* Enable app packaging in scripts project
This commit is contained in:
Chris Stewart 2021-04-25 14:03:22 -05:00 committed by GitHub
parent 27afb66220
commit 9ecea9f710
8 changed files with 213 additions and 17 deletions

View File

@ -0,0 +1,94 @@
package org.bitcoins.scripts
import akka.NotUsed
import akka.stream.scaladsl.{Keep, Sink, Source}
import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.WitnessTransaction
import org.bitcoins.rpc.client.common.BitcoindRpcClient
import org.bitcoins.server.BitcoindRpcAppConfig
import org.bitcoins.server.routes.BitcoinSRunner
import scala.concurrent.Future
/** Useful script for scanning bitcoind
* This file assumes you have pre-configured the connection
* between bitcoin-s and bitcoind inside of bitcoin-s.conf
* @see https://bitcoin-s.org/docs/config/configuration#example-configuration-file
*/
class ScanBitcoind(override val args: Array[String]) extends BitcoinSRunner {
override val actorSystemName = "scan-bitcoind"
implicit val rpcAppConfig: BitcoindRpcAppConfig =
BitcoindRpcAppConfig(datadir, baseConfig)
override def startup: Future[Unit] = {
val bitcoind = rpcAppConfig.client
val startHeight = 675000
val endHeightF: Future[Int] = bitcoind.getBlockCount
for {
endHeight <- endHeightF
_ <- countSegwitTxs(bitcoind, startHeight, endHeight)
_ <- system.terminate()
} yield {
sys.exit(0)
}
}
/** Searches a given Source[Int] that represents block heights applying f to them and returning a Seq[T] with the results */
def searchBlocks[T](
bitcoind: BitcoindRpcClient,
source: Source[Int, NotUsed],
f: Block => T,
numParallelism: Int =
Runtime.getRuntime.availableProcessors() * 2): Future[Seq[T]] = {
source
.mapAsync(parallelism = numParallelism) { height =>
bitcoind
.getBlockHash(height)
.flatMap(h => bitcoind.getBlockRaw(h))
.map(b => (b, height))
}
.mapAsync(numParallelism) { case (block, height) =>
logger.info(
s"Searching block at height=$height hashBE=${block.blockHeader.hashBE.hex}")
Future {
f(block)
}
}
.toMat(Sink.seq)(Keep.right)
.run()
}
def countSegwitTxs(
bitcoind: BitcoindRpcClient,
startHeight: Int,
endHeight: Int): Future[Unit] = {
val startTime = System.currentTimeMillis()
val source: Source[Int, NotUsed] = Source(startHeight.to(endHeight))
//in this simple example, we are going to count the number of witness transactions
val countSegwitTxs: Block => Int = { block: Block =>
block.transactions.count(_.isInstanceOf[WitnessTransaction])
}
val countsF: Future[Seq[Int]] = for {
counts <- searchBlocks[Int](bitcoind, source, countSegwitTxs)
} yield counts
val countF: Future[Int] = countsF.map(_.sum)
for {
count <- countF
endTime = System.currentTimeMillis()
_ = println(
s"Count of segwit txs from height=${startHeight} to endHeight=${endHeight} is ${count}. It took ${endTime - startTime}ms ")
} yield ()
}
}
object ScanBitcoind extends App {
new ScanBitcoind(args).run()
}

View File

@ -0,0 +1,31 @@
package org.bitcoins.scripts
import org.bitcoins.server.BitcoinSAppConfig
import org.bitcoins.server.routes.BitcoinSRunner
import java.nio.file.Paths
import scala.concurrent.Future
/** This script zips your $HOME/.bitcoin-s/ directory to a specified path, excluding chaindb.sqlite */
class ZipDatadir(override val args: Array[String]) extends BitcoinSRunner {
override def actorSystemName: String = "Zip-datadir"
implicit lazy val conf: BitcoinSAppConfig =
BitcoinSAppConfig(datadir, baseConfig)
override def startup: Future[Unit] = {
//replace the line below with where you want to zip too
val path = Paths.get("/tmp", "bitcoin-s.zip")
val target = conf.zipDatadir(path)
logger.info(s"Done zipping to $target!")
for {
_ <- system.terminate()
} yield sys.exit(0)
}
}
object Zip extends App {
new ZipDatadir(args).run()
}

View File

@ -1,6 +1,5 @@
package org.bitcoins.server
import akka.actor.ActorSystem
import com.typesafe.config.Config
import org.bitcoins.db._
import org.bitcoins.node.NodeType
@ -128,9 +127,7 @@ case class BitcoindRpcAppConfig(
lazy val client: BitcoindRpcClient = {
val version = bitcoindInstance.getVersion
implicit val system: ActorSystem =
ActorSystem.create("bitcoind-rpc-client-created-by-bitcoin-s", config)
BitcoindRpcClient.fromVersion(version, bitcoindInstance)
BitcoindRpcClient.fromVersionNoSystem(version, bitcoindInstance)
}
}

View File

@ -222,6 +222,8 @@ object BitcoindRpcClient {
*/
private[rpc] val ActorSystemName = "bitcoind-rpc-client-created-by-bitcoin-s"
implicit private lazy val system = ActorSystem.create(ActorSystemName)
/** Creates an RPC client from the given instance.
*
* Behind the scenes, we create an actor system for
@ -229,8 +231,7 @@ object BitcoindRpcClient {
* manually specify an actor system for the RPC client.
*/
def apply(instance: BitcoindInstance): BitcoindRpcClient = {
implicit val system = ActorSystem.create(ActorSystemName)
withActorSystem(instance)
withActorSystem(instance)(system)
}
/** Creates an RPC client from the given instance,
@ -272,6 +273,12 @@ object BitcoindRpcClient {
bitcoind
}
def fromVersionNoSystem(
version: BitcoindVersion,
instance: BitcoindInstance): BitcoindRpcClient = {
fromVersion(version, instance)(system)
}
}
sealed trait BitcoindVersion

View File

@ -207,7 +207,8 @@ lazy val `bitcoin-s` = project
oracleServerTest,
serverRoutes,
lndRpc,
lndRpcTest
lndRpcTest,
scripts
)
.dependsOn(
secp256k1jni,
@ -253,7 +254,8 @@ lazy val `bitcoin-s` = project
oracleServerTest,
serverRoutes,
lndRpc,
lndRpcTest
lndRpcTest,
scripts
)
.settings(CommonSettings.settings: _*)
// unidoc aggregates Scaladocs for all subprojects into one big doc
@ -700,6 +702,16 @@ lazy val oracleExplorerClient = project
)
.dependsOn(coreJVM, appCommons, testkit % "test->test")
lazy val scripts = project
.in(file("app/scripts"))
.settings(CommonSettings.settings: _*)
.settings(
name := "bitcoin-s-scripts",
publishArtifact := false //do not want to publish our scripts
)
.dependsOn(appServer)
.enablePlugins(JavaAppPackaging)
/** Given a database name, returns the appropriate
* Flyway settings we apply to a project (chain, node, wallet)
*/

View File

@ -0,0 +1,36 @@
package org.bitcoins.core.protocol.script
import org.bitcoins.core.script.constant.ScriptConstant
import org.bitcoins.testkitcore.util.BitcoinSJvmTest
class ConditionalScriptPubKeyTest extends BitcoinSJvmTest {
behavior of "ConditionalScriptPubKey"
it must "be able to parse a conditional spk with a nested p2sh script" in {
//see: https://github.com/bitcoin-s/bitcoin-s/issues/2962
val scriptSigHex =
"48304502207ffb30631d837895ac1b415408b70a809090b25d4070198043299fe247f62d2602210089b52f0d243dea1e4313ef67723b0d0642ff77bb6ca05ea85296b2539c797f5c81513d632103184b16f5d6c01e2d6ded3f8a292e5b81608318ecb8e93aa3747bc88b8dbf256cac67a914f5862841f254a1483eab66909ae588e45d617c5e8768"
val scriptSig = ScriptSignature.fromAsmHex(scriptSigHex)
scriptSig match {
case p2sh: P2SHScriptSignature =>
assert(p2sh.redeemScript.isInstanceOf[IfConditionalScriptPubKey])
case x => fail(s"Did not parse a p2sh script sig, got=$x")
}
}
it must "consider a redeem script that contains OP_IF in it with a nested p2sh script pubkey in it" in {
//see: https://github.com/bitcoin-s/bitcoin-s/issues/2962
val hex =
"632103184b16f5d6c01e2d6ded3f8a292e5b81608318ecb8e93aa3747bc88b8dbf256cac67a914f5862841f254a1483eab66909ae588e45d617c5e8768"
val scriptPubKey = RawScriptPubKey.fromAsmHex(hex)
assert(scriptPubKey.isInstanceOf[IfConditionalScriptPubKey])
val constant = ScriptConstant.fromHex(hex)
assert(P2SHScriptSignature.isRedeemScript(constant))
}
}

View File

@ -613,9 +613,6 @@ sealed trait ConditionalScriptPubKey extends RawScriptPubKey {
val opElseIndex: Int = opElseIndexOpt.get
require(!P2SHScriptPubKey.isValidAsm(trueSPK.asm) && !P2SHScriptPubKey
.isValidAsm(falseSPK.asm),
"ConditionalScriptPubKey cannot wrap P2SH")
require(
!WitnessScriptPubKey
.isValidAsm(trueSPK.asm) && !WitnessScriptPubKey

View File

@ -273,20 +273,42 @@ object P2SHScriptSignature extends ScriptFactory[P2SHScriptSignature] {
_: P2PKScriptPubKey | _: P2PKWithTimeoutScriptPubKey |
_: WitnessScriptPubKeyV0 | _: UnassignedWitnessScriptPubKey =>
true
case _: P2SHScriptPubKey =>
true
case EmptyScriptPubKey => isRecursiveCall // Fine if nested
case conditional: ConditionalScriptPubKey =>
isStandardNonP2SH(conditional.firstSPK,
isRecursiveCall = true) && isStandardNonP2SH(
conditional.secondSPK,
isRecursiveCall = true)
val first =
isStandardNonP2SH(conditional.firstSPK, isRecursiveCall = true)
val second =
isStandardNonP2SH(conditional.secondSPK, isRecursiveCall = true)
//we need to see if we have a p2sh scriptpubkey nested
//inside of the conditional spk. This can actually happen
//when you literally just want to use the script OP_HASH160 <bytes> OP_EQUAL
//independent of the normal p2sh flow
//see: https://github.com/bitcoin-s/bitcoin-s/issues/2962
(first, second) match {
case (true, true) => true
case (true, false) =>
P2SHScriptPubKey.isValidAsm(conditional.secondSPK.asm)
case (false, true) =>
P2SHScriptPubKey.isValidAsm(conditional.firstSPK.asm)
case (false, false) =>
val isP2SHFirst =
P2SHScriptPubKey.isValidAsm(conditional.firstSPK.asm)
val isP2SHSecond =
P2SHScriptPubKey.isValidAsm(conditional.secondSPK.asm)
isP2SHFirst && isP2SHSecond
}
case locktime: LockTimeScriptPubKey =>
if (Try(locktime.locktime).isSuccess) {
isStandardNonP2SH(locktime.nestedScriptPubKey, isRecursiveCall = true)
} else {
false
}
case _: NonStandardScriptPubKey | _: WitnessCommitment |
_: P2SHScriptPubKey =>
case _: NonStandardScriptPubKey | _: WitnessCommitment =>
false
}
}