Adds typesafe-config parsing of bitcoin.conf (#279)

* Adds typesafe-config parsing of bitcoin.conf

Adds ZmqConfig trait

* Only add datadir if not present

* Use old and new config settings
This commit is contained in:
Torkel Rogstad 2019-01-07 16:30:59 +01:00 committed by Chris Stewart
parent abcd7c5d6d
commit 1301336231
8 changed files with 364 additions and 41 deletions

View file

@ -15,6 +15,7 @@ object Deps {
val scodecV = "1.1.6"
val junitV = "0.11"
val nativeLoaderV = "2.3.2"
val typesafeConfigV = "1.3.3"
val bitcoinsV = "0.0.1-SNAPSHOT"
}
@ -27,6 +28,7 @@ object Deps {
val akkaHttp = "com.typesafe.akka" %% "akka-http" % V.akkav withSources() withJavadoc()
val akkaStream = "com.typesafe.akka" %% "akka-stream" % V.akkaStreamv withSources() withJavadoc()
val playJson = "com.typesafe.play" %% "play-json" % V.playv withSources() withJavadoc()
val typesafeConfig = "com.typesafe" % "config" % V.typesafeConfigV withSources() withJavadoc()
val logback = "ch.qos.logback" % "logback-classic" % V.logback withSources() withJavadoc()
@ -85,6 +87,7 @@ object Deps {
Compile.akkaStream,
Compile.playJson,
Compile.slf4j,
Compile.typesafeConfig,
Test.akkaHttp,
Test.akkaStream,
Test.logback,

View file

@ -1,8 +1,13 @@
package org.bitcoins.rpc.config
import java.io.File
import java.net.URI
import java.nio.file.Paths
import org.bitcoins.core.config.NetworkParameters
import com.typesafe.config._
import org.bitcoins.core.config._
import scala.util.{Failure, Properties, Success, Try}
/**
* Created by chris on 4/29/17.
@ -15,9 +20,9 @@ sealed trait BitcoindInstance {
def uri: URI
def rpcUri: URI
def authCredentials: BitcoindAuthCredentials
def zmqConfig: ZmqConfig
def rpcPort: Int = authCredentials.rpcPort
def zmqPortOpt: Option[Int]
}
object BitcoindInstance {
@ -26,15 +31,144 @@ object BitcoindInstance {
uri: URI,
rpcUri: URI,
authCredentials: BitcoindAuthCredentials,
zmqPortOpt: Option[Int])
extends BitcoindInstance
zmqConfig: ZmqConfig = ZmqConfig()
) extends BitcoindInstance
def apply(
network: NetworkParameters,
uri: URI,
rpcUri: URI,
authCredentials: BitcoindAuthCredentials,
zmqPortOpt: Option[Int] = None): BitcoindInstance = {
BitcoindInstanceImpl(network, uri, rpcUri, authCredentials, zmqPortOpt)
zmqConfig: ZmqConfig = ZmqConfig()
): BitcoindInstance = {
BitcoindInstanceImpl(network,
uri,
rpcUri,
authCredentials,
zmqConfig = zmqConfig)
}
private val DEFAULT_DATADIR =
Paths.get(Properties.userHome, ".bitcoin")
private val DEFAULT_CONF_FILE = DEFAULT_DATADIR.resolve("bitcoin.conf")
def fromDatadir(datadir: File = DEFAULT_DATADIR.toFile): BitcoindInstance = {
require(datadir.exists, s"${datadir.getPath} does not exist!")
require(datadir.isDirectory, s"${datadir.getPath} is not a directory!")
val file = Paths.get(datadir.getAbsolutePath, "bitcoin.conf").toFile
fromConfigFile(file)
}
def fromConfigFile(
file: File = DEFAULT_CONF_FILE.toFile): BitcoindInstance = {
require(file.exists, s"${file.getPath} does not exist!")
require(file.isFile, s"${file.getPath} is not a file!")
val config = ConfigFactory.parseFile(
file,
ConfigParseOptions.defaults
.setSyntax(ConfigSyntax.PROPERTIES)) // bitcoin.conf is not a proper .conf file, uses Java properties=like syntax
val configWithDatadir =
if (config.hasPath("datadir")) {
config
} else {
config.withValue("datadir",
ConfigValueFactory.fromAnyRef(file.getParent))
}
fromConfig(configWithDatadir)
}
def fromConfig(config: Config): BitcoindInstance = {
val datadirStr = Try(config.getString("datadir"))
.getOrElse(
throw new IllegalArgumentException(
"Provided config does not contain \"datadir\" setting!"))
val datadir = new File(datadirStr)
require(datadir.exists, s"Datadir $datadirStr does not exist!")
require(datadir.isDirectory, s"Datadir $datadirStr is not directory")
fromConfig(config, datadir)
}
def fromConfig(
config: Config,
datadir: File
): BitcoindInstance = {
val network = getNetwork(config)
val uri = getUri(config, network)
val rpcUri = getRpcUri(config, network)
val username = config.getString("rpcuser")
val password = config.getString("rpcpassword")
val authCredentials =
BitcoindAuthCredentials(username = username,
password = password,
rpcPort = rpcUri.getPort,
datadir = datadir)
BitcoindInstance(network,
uri,
rpcUri,
authCredentials,
zmqConfig = ZmqConfig.fromConfig(config))
}
private def isSet(config: Config, path: String): Boolean = {
Try(config.getInt(path))
.map(_ == 1)
.getOrElse(false)
}
private def getNetwork(config: Config): BitcoinNetwork = {
val isTestnet = isSet(config, path = "testnet")
val isRegTest = isSet(config, path = "regtest")
(isRegTest, isTestnet) match {
case (true, true) =>
throw new IllegalArgumentException(
""""Cannot set both "regtest" and "testnet" options""")
case (true, false) => RegTest
case (false, true) => TestNet3
case (false, false) => MainNet
}
}
private def getUri(config: Config, network: NetworkParameters): URI = {
val port = Try(config.getInt("port")).getOrElse(network.port)
val host = Try(config.getString("bind")).getOrElse("localhost")
val uriT = Try {
new URI(s"http://$host:$port")
}
uriT match {
case Success(uriSuccess) => uriSuccess
case Failure(exception) =>
throw new IllegalArgumentException(
s"Could not construct URI from host $host and port $port",
exception)
}
}
private def getRpcUri(config: Config, network: NetworkParameters): URI = {
val rpcPort = Try(config.getInt("rpcport")).getOrElse(network.rpcPort)
val rpcHost = Try(config.getString("rpcbind")).getOrElse("localhost")
val rpcUriT = Try {
new URI(s"http://$rpcHost:$rpcPort")
}
rpcUriT match {
case Success(uriSuccess) => uriSuccess
case Failure(exception) =>
throw new IllegalArgumentException(
s"Could not construct URI from host $rpcHost and port $rpcPort",
exception)
}
}
}

View file

@ -0,0 +1,90 @@
package org.bitcoins.rpc.config
import java.net.URI
import com.typesafe.config.Config
import scala.util.Try
sealed trait ZmqConfig {
def hashBlock: Option[URI]
def rawBlock: Option[URI]
def hashTx: Option[URI]
def rawTx: Option[URI]
}
object ZmqConfig {
private case class ZmqConfigImpl(
hashBlock: Option[URI],
rawBlock: Option[URI],
hashTx: Option[URI],
rawTx: Option[URI]
) extends ZmqConfig
def apply(
hashBlock: Option[URI] = None,
rawBlock: Option[URI] = None,
hashTx: Option[URI] = None,
rawTx: Option[URI] = None
): ZmqConfig =
ZmqConfigImpl(hashBlock = hashBlock,
rawBlock = rawBlock,
hashTx = hashTx,
rawTx = rawTx)
/**
* Creates a `ZmqConfig` with all `URI`s set to
* `localhost` and the same port
*/
def fromPort(port: Int): ZmqConfig = {
val uri = new URI(s"tcp://localhost:$port")
ZmqConfig(hashBlock = Some(uri),
rawBlock = Some(uri),
hashTx = Some(uri),
rawTx = Some(uri))
}
def fromConfig(config: Config): ZmqConfig =
ZmqConfig(hashBlock = hashBlockUri(config),
hashTx = hashTxUri(config),
rawBlock = rawBlockUri(config),
rawTx = rawTxUri(config))
private val RAW_BLOCK_KEY = "zmqpubrawblock"
private val RAW_TX_KEY = "zmqpubrawtx"
private val HASH_BLOCK_KEY = "zmqpubhashblock"
private val HASH_TX_KEY = "zmqpubhashtx"
private val ZMQ_CONFIG_KEYS =
List(RAW_TX_KEY, RAW_BLOCK_KEY, HASH_TX_KEY, HASH_BLOCK_KEY)
private def isValidZmqConfigKey(key: String): Boolean =
ZMQ_CONFIG_KEYS.contains(key)
private def getZmqUri(config: Config, path: String): Option[URI] = {
require(
isValidZmqConfigKey(path),
s"$path is not a valid ZMQ config key. Valid keys: ${ZMQ_CONFIG_KEYS.mkString(", ")}")
if (config.hasPath(path)) {
Try(config.getString(path))
.map(str => Some(new URI(str)))
.getOrElse(throw new IllegalArgumentException(
s"$path in config is not a valid URI"))
} else {
None
}
}
private def rawBlockUri(config: Config): Option[URI] =
getZmqUri(config, RAW_BLOCK_KEY)
private def rawTxUri(config: Config): Option[URI] =
getZmqUri(config, RAW_TX_KEY)
private def hashBlockUri(config: Config): Option[URI] =
getZmqUri(config, HASH_BLOCK_KEY)
private def hashTxUri(config: Config): Option[URI] =
getZmqUri(config, HASH_TX_KEY)
}

View file

@ -0,0 +1,9 @@
testnet=0
regtest=1
rpcuser=user
rpcpassword=password
zmqpubrawblock=tcp://127.0.0.1:29005
zmppubrawtx=tcp://127.0.0.1:29001
rpcport=18336
port=9000

View file

@ -0,0 +1,55 @@
package org.bitcoins.rpc
import java.io.{File, PrintWriter}
import java.nio.file.{Files, Path}
import akka.actor.ActorSystem
import akka.testkit.TestKit
import org.bitcoins.core.currency.Bitcoins
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.config.BitcoindInstance
import org.scalatest.{AsyncFlatSpec, BeforeAndAfterAll}
import scala.concurrent.Future
import scala.io.Source
class BitcoindInstanceTest extends AsyncFlatSpec with BeforeAndAfterAll {
private implicit val actorSystem: ActorSystem = ActorSystem(
"BitcoindInstanceTest")
private val sampleConf: Seq[String] =
Source.fromResource("sample-bitcoin.conf").mkString.split("\n")
private val datadir: Path = Files.createTempDirectory(null)
override protected def beforeAll(): Unit = {
val confFile = new File(datadir.toString + "/bitcoin.conf")
val pw = new PrintWriter(confFile)
sampleConf.foreach(line => pw.write(line + "\n"))
pw.close()
}
override protected def afterAll(): Unit = {
TestKit.shutdownActorSystem(actorSystem)
}
behavior of "BitcoindInstance"
it should "parse a bitcoin.conf file, start bitcoind, mine some blocks and quit" in {
val instance = BitcoindInstance.fromDatadir(datadir.toFile)
val client = new BitcoindRpcClient(instance)
BitcoindRpcTestUtil.startServers(Vector(client))
RpcUtil.awaitServer(client)
for {
_ <- client.generate(101)
balance <- client.getBalance
_ <- {
assert(balance > Bitcoins(0))
client.stop()
}
_ <- Future.successful(RpcUtil.awaitServerShutdown(client))
} yield succeed
}
}

View file

@ -92,7 +92,7 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
}
}
"TestUtil" should "create a temp bitcoin directory when creating a DaemonInstance, and then delete it" in {
"BitcoindRpcUtil" should "create a temp bitcoin directory when creating a DaemonInstance, and then delete it" in {
val instance = BitcoindRpcTestUtil.instance(BitcoindRpcTestUtil.randomPort,
BitcoindRpcTestUtil.randomPort)
val dir = instance.authCredentials.datadir
@ -120,7 +120,7 @@ class RpcUtilTest extends AsyncFlatSpec with BeforeAndAfterAll {
assert(t.isFailure)
}
"TestUtil" should "be able to create a connected node pair with 100 blocks and then delete them" in {
it should "be able to create a connected node pair with 100 blocks and then delete them" in {
BitcoindRpcTestUtil.createNodePair().flatMap {
case (client1, client2) =>
assert(client1.getDaemon.authCredentials.datadir.isDirectory)

View file

@ -17,7 +17,7 @@ import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.eclair.rpc.client.EclairRpcClient
import org.bitcoins.eclair.rpc.config.EclairInstance
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.config.BitcoindInstance
import org.bitcoins.rpc.config.{BitcoindInstance, ZmqConfig}
import org.bitcoins.rpc.{BitcoindRpcTestUtil, RpcUtil}
import scala.concurrent.Future
@ -47,7 +47,7 @@ trait EclairTestUtil extends BitcoinSLogger {
uri = uri,
rpcUri = rpcUri,
authCredentials = auth,
zmqPortOpt = Some(zmqPort))
zmqConfig = ZmqConfig.fromPort(zmqPort))
}
//cribbed from https://github.com/Christewart/eclair/blob/bad02e2c0e8bd039336998d318a861736edfa0ad/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala#L140-L153
@ -65,7 +65,11 @@ trait EclairTestUtil extends BitcoinSLogger {
"eclair.bitcoind.rpcuser" -> bitcoindInstance.authCredentials.username,
"eclair.bitcoind.rpcpassword" -> bitcoindInstance.authCredentials.password,
"eclair.bitcoind.rpcport" -> bitcoindInstance.authCredentials.rpcPort,
"eclair.bitcoind.zmq" -> s"tcp://127.0.0.1:${bitcoindInstance.zmqPortOpt.get}",
// newer versions of Eclair has removed this config setting, in favor of
// the below it. All three are included here for good measure
"eclair.bitcoind.zmq" -> bitcoindInstance.zmqConfig.rawTx.get.toString,
"eclair.bitcoind.zmqblock" -> bitcoindInstance.zmqConfig.rawBlock.get.toString,
"eclair.bitcoind.zmqtx" -> bitcoindInstance.zmqConfig.rawTx.get.toString,
"eclair.api.enabled" -> true,
"eclair.api.binding-ip" -> "127.0.0.1",
"eclair.api.password" -> "abc123",
@ -332,7 +336,6 @@ trait EclairTestUtil extends BitcoinSLogger {
rpcUri = new URI(s"http://localhost:${bitcoindRpcPort}"),
authCredentials =
eclairRpcClient.instance.authCredentials.bitcoinAuthOpt.get,
None
)
new BitcoindRpcClient(bitcoindInstance)
}

View file

@ -5,12 +5,19 @@ import java.net.URI
import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.crypto.DoubleSha256Digest
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.rpc.client.BitcoindRpcClient
import org.bitcoins.rpc.config.{BitcoindAuthCredentials, BitcoindInstance}
import org.bitcoins.rpc.config.{
BitcoindAuthCredentials,
BitcoindInstance,
ZmqConfig
}
import scala.collection.immutable.Map
import scala.collection.JavaConverters.{asScalaSet, mapAsJavaMap}
import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}
@ -20,6 +27,34 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
def randomDirName: String =
0.until(5).map(_ => scala.util.Random.alphanumeric.head).mkString
def config(
uri: URI,
rpcUri: URI,
zmqPort: Int,
pruneMode: Boolean): Config = {
val pass = randomDirName
val username = "random_user_name"
val values = Map(
"rpcuser" -> username,
"rpcpassword" -> pass,
"rpcport" -> rpcUri.getPort,
"port" -> uri.getPort,
"daemon" -> "1",
"server" -> "1",
"debug" -> "1",
"regtest" -> "1",
"walletbroadcast" -> "0",
"zmqpubhashtx" -> s"tcp://127.0.0.1:$zmqPort",
"zmqpubhashblock" -> s"tcp://127.0.0.1:$zmqPort",
"zmqpubrawtx" -> s"tcp://127.0.0.1:$zmqPort",
"zmqpubrawblock" -> s"tcp://127.0.0.1:$zmqPort",
"prune" -> (if (pruneMode) "1" else "0")
)
val javaMap = mapAsJavaMap(values)
ConfigFactory.parseMap(javaMap)
}
/**
* Creates a datadir and places the username/password combo
* in the bitcoin.conf in the datadir
@ -29,39 +64,33 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
rpcUri: URI,
zmqPort: Int,
pruneMode: Boolean): BitcoindAuthCredentials = {
val d = "/tmp/" + randomDirName
val f = new java.io.File(d)
f.mkdir()
val conf = new java.io.File(f.getAbsolutePath + "/bitcoin.conf")
conf.createNewFile()
val username = "random_user_name"
val pass = randomDirName
val pw = new PrintWriter(conf)
pw.write("rpcuser=" + username + "\n")
pw.write("rpcpassword=" + pass + "\n")
pw.write("rpcport=" + rpcUri.getPort + "\n")
pw.write("port=" + uri.getPort + "\n")
pw.write("daemon=1\n")
pw.write("server=1\n")
pw.write("debug=1\n")
pw.write("regtest=1\n")
pw.write("walletbroadcast=0\n")
val conf = config(uri, rpcUri, zmqPort, pruneMode)
val confSet = asScalaSet(conf.entrySet).toSet
val confStr =
confSet
.map(entry => {
val key = entry.getKey
val value = entry.getValue.unwrapped
s"$key=$value"})
.mkString("\n")
pw.write(s"zmqpubhashtx=tcp://127.0.0.1:${zmqPort}\n")
pw.write(s"zmqpubhashblock=tcp://127.0.0.1:${zmqPort}\n")
pw.write(s"zmqpubrawtx=tcp://127.0.0.1:${zmqPort}\n")
pw.write(s"zmqpubrawblock=tcp://127.0.0.1:${zmqPort}\n")
val datadir = new java.io.File("/tmp/" + randomDirName)
datadir.mkdir()
if (pruneMode) {
logger.info(s"Creating pruned node for ${f.getAbsolutePath}")
pw.write("prune=1\n")
}
val confFile = new java.io.File(datadir.getAbsolutePath + "/bitcoin.conf")
confFile.createNewFile()
val pw = new PrintWriter(confFile)
pw.write(confStr)
pw.close()
BitcoindAuthCredentials(username, pass, rpcUri.getPort, f)
val username = conf.getString("rpcuser")
val pass = conf.getString("rpcpassword")
BitcoindAuthCredentials(username, pass, rpcUri.getPort, datadir)
}
lazy val network = RegTest
lazy val network: RegTest.type = RegTest
def instance(
port: Int = randomPort,
@ -75,7 +104,7 @@ trait BitcoindRpcTestUtil extends BitcoinSLogger {
uri = uri,
rpcUri = rpcUri,
authCredentials = auth,
zmqPortOpt = Some(zmqPort))
zmqConfig = ZmqConfig.fromPort(zmqPort))
instance
}