Implement ability to provide external entropy to bitcoin-s (#3672)

* Encapsulate initialization of DLCOracle.start() method

* Use internal WalletAppConfig.kmConf rather than passing in custom key manager parameters

* Add KeyManagerAppConfig.defaultAccountType

* Get all tests passing besides TrezorAddressTest

* Get TrezorAddressTest passing with provided entropy

* Add unit test to make sure we can always derive the seed

* Get docs compiling

* Fix dlcWalletTest test cases

* Add more test cases to keymanager

* Add the new configuration to the example configuration

* Add more test cases

* Remove coverage on 2.12 as it isn't accurate

* Rework DLCOracleAppConfig.start() to call kmConf.start() so the oracle can use entropy provided via bitcoin-s.conf
This commit is contained in:
Chris Stewart 2021-09-18 09:49:11 -05:00 committed by GitHub
parent 3c64af39d9
commit 132479d271
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 465 additions and 362 deletions

View file

@ -28,4 +28,4 @@ jobs:
~/.bitcoin-s/binaries
key: ${{ runner.os }}-cache
- name: run tests
run: sbt ++2.12.14 downloadBitcoind coverage keyManagerTest/test keyManager/coverageReport keyManager/coverageAggregate keyManager/coveralls feeProviderTest/test walletTest/test dlcWalletTest/test wallet/coverageReport wallet/coverageAggregate wallet/coveralls dlcOracleTest/test asyncUtilsTestJVM/test asyncUtilsTestJS/test oracleExplorerClient/test dlcOracle/coverageReport dlcOracle/coverageAggregate dlcOracle/coveralls
run: sbt ++2.12.14 downloadBitcoind keyManagerTest/test feeProviderTest/test walletTest/test dlcWalletTest/test dlcOracleTest/test asyncUtilsTestJVM/test asyncUtilsTestJS/test oracleExplorerClient/test

View file

@ -2,6 +2,7 @@ package org.bitcoins.oracle.server
import akka.actor.ActorSystem
import org.bitcoins.commons.util.{DatadirParser, ServerArgParser}
import org.bitcoins.dlc.oracle.DLCOracle
import org.bitcoins.dlc.oracle.config.DLCOracleAppConfig
import org.bitcoins.server.routes.{BitcoinSServerRunner, Server}
import org.bitcoins.server.util.BitcoinSAppScalaDaemon
@ -22,8 +23,7 @@ class OracleServerMain(override val serverArgParser: ServerArgParser)(implicit
for {
_ <- conf.start()
oracle <- conf.initialize()
oracle = new DLCOracle()
routes = Seq(OracleRoutes(oracle))
server = serverArgParser.rpcPortOpt match {
case Some(rpcport) =>

View file

@ -117,13 +117,11 @@ object BitcoindRpcBackendUtil extends Logging {
val walletCallbackP = Promise[Wallet]()
val pairedWallet = Wallet(
keyManager = wallet.keyManager,
nodeApi =
BitcoindRpcBackendUtil.getNodeApiWalletCallback(bitcoind,
walletCallbackP.future),
chainQueryApi = bitcoind,
feeRateApi = wallet.feeRateApi,
creationTime = wallet.keyManager.creationTime
feeRateApi = wallet.feeRateApi
)(wallet.walletConfig, wallet.ec)
walletCallbackP.success(pairedWallet)
@ -178,13 +176,11 @@ object BitcoindRpcBackendUtil extends Logging {
val walletCallbackP = Promise[Wallet]()
val pairedWallet = DLCWallet(
keyManager = wallet.keyManager,
nodeApi =
BitcoindRpcBackendUtil.getNodeApiWalletCallback(bitcoind,
walletCallbackP.future),
chainQueryApi = bitcoind,
feeRateApi = wallet.feeRateApi,
creationTime = wallet.keyManager.creationTime
feeRateApi = wallet.feeRateApi
)(wallet.walletConfig, wallet.dlcConfig, wallet.ec)
walletCallbackP.success(pairedWallet)

View file

@ -220,4 +220,22 @@ class CryptoUtilTest extends BitcoinSCryptoTest {
})
}
}
it must "do basic sanity checks on entropy" in {
assert(!CryptoUtil.checkEntropy(BitVector.empty))
val sameBytes1 = ByteVector.fill(32)(0x0)
val sameBytes2 = ByteVector.fill(32)(0xff)
assert(!CryptoUtil.checkEntropy(sameBytes1.toBitVector))
assert(!CryptoUtil.checkEntropy(sameBytes2.toBitVector))
//to short of entropy
val toShort = ByteVector.fromValidHex("0123456789abcdef")
assert(!CryptoUtil.checkEntropy(toShort.toBitVector))
}
it must "always pass our basic sanity tests for entropy with our real PRNG" in {
forAll(Gen.const(CryptoUtil.randomBytes(32))) { bytes =>
assert(CryptoUtil.checkEntropy(bytes))
}
}
}

View file

@ -396,4 +396,25 @@ trait CryptoRuntime {
derivedKeyLength: Int): ByteVector
def randomBytes(n: Int): ByteVector
/** Implements basic sanity tests for checking entropy like
* making sure it isn't all the same bytes,
* it isn't all 0x00...00
* or it isn't all 0xffff...fff
*/
def checkEntropy(bitVector: BitVector): Boolean = {
val byteArr = bitVector.toByteArray
if (bitVector.length < 128) {
//not enough entropy
false
} else if (byteArr.toSet.size == 1) {
//means all byte were the same
//we need more diversity with entropy
false
} else {
true
}
}
def checkEntropy(bytes: ByteVector): Boolean = checkEntropy(bytes.toBitVector)
}

View file

@ -109,7 +109,8 @@ class DLCOracleTest extends DLCOracleFixture {
Vector(ConfigFactory.parseString("bitcoin-s.network = mainnet"),
ConfigFactory.parseString("bitcoin-s.oracle.db.name = oracle1")))
newConf.initialize().flatMap { oracleB =>
newConf.start().flatMap { _ =>
val oracleB = new DLCOracle()(newConf)
assert(oracleA.publicKey == oracleB.publicKey)
val eventName = "test"

View file

@ -1,5 +1,6 @@
package org.bitcoins.dlc.oracle.config
import org.bitcoins.dlc.oracle.DLCOracle
import org.bitcoins.keymanager.WalletStorage
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.fixtures.DLCOracleAppConfigFixture
@ -10,14 +11,15 @@ class DLCOracleAppConfigTest extends DLCOracleAppConfigFixture {
behavior of "DLCOracleAppConfig"
it must "initialize the same oracle twice" in {
it must "start the same oracle twice" in {
dlcOracleAppConfig: DLCOracleAppConfig =>
val dlcOracle1F = dlcOracleAppConfig.initialize()
val dlcOracle2F = dlcOracleAppConfig.initialize()
val started1F = dlcOracleAppConfig.start()
val started2F = dlcOracleAppConfig.start()
for {
dlcOracle1 <- dlcOracle1F
dlcOracle2 <- dlcOracle2F
_ <- started1F
_ <- started2F
dlcOracle1 = new DLCOracle()(dlcOracleAppConfig)
dlcOracle2 = new DLCOracle()(dlcOracleAppConfig)
} yield {
assert(dlcOracle1.publicKey == dlcOracle2.publicKey)
}
@ -26,12 +28,17 @@ class DLCOracleAppConfigTest extends DLCOracleAppConfigFixture {
it must "initialize the oracle, move the seed somewhere else, and then start the oracle again and get the same pubkeys" in {
dlcOracleAppConfig: DLCOracleAppConfig =>
val seedFile = dlcOracleAppConfig.seedPath
val dlcOracle1F = dlcOracleAppConfig.initialize()
val pubKeyBeforeMoveF = dlcOracle1F.map(_.publicKey)
val startedF = dlcOracleAppConfig.start()
val pubKeyBeforeMoveF = for {
_ <- startedF
dlcOracle = new DLCOracle()(dlcOracleAppConfig)
} yield {
dlcOracle.publicKey
}
//stop old oracle
val stoppedF = for {
_ <- dlcOracle1F
_ <- startedF
_ <- dlcOracleAppConfig.stop()
} yield ()
@ -43,13 +50,21 @@ class DLCOracleAppConfigTest extends DLCOracleAppConfigFixture {
//create seed directory
Files.createDirectories(newSeedPath.getParent)
//copy seed file to new directory
Files.copy(seedFile, newSeedPath)
val copyF = startedF.map { _ =>
//copy seed file to new directory
Files.copy(seedFile, newSeedPath)
}
//start the new app config from the new datadir
val dlcOracle2F = DLCOracleAppConfig
val appConfig = DLCOracleAppConfig
.fromDatadir(newDatadir)
.initialize()
val started2F = for {
_ <- copyF
_ <- appConfig.start()
} yield ()
val dlcOracle2F = started2F.map(_ => new DLCOracle()(appConfig))
for {
_ <- stoppedF

View file

@ -6,7 +6,7 @@ import org.bitcoins.core.api.dlcoracle._
import org.bitcoins.core.api.dlcoracle.db._
import org.bitcoins.core.config.BitcoinNetwork
import org.bitcoins.core.crypto.ExtKeyVersion.SegWitMainNetPriv
import org.bitcoins.core.crypto.{ExtPrivateKeyHardened, MnemonicCode}
import org.bitcoins.core.crypto.ExtPrivateKeyHardened
import org.bitcoins.core.hd._
import org.bitcoins.core.number._
import org.bitcoins.core.protocol.Bech32Address
@ -19,7 +19,7 @@ import org.bitcoins.crypto._
import org.bitcoins.dlc.oracle.config.DLCOracleAppConfig
import org.bitcoins.dlc.oracle.storage._
import org.bitcoins.dlc.oracle.util.EventDbUtil
import org.bitcoins.keymanager.{DecryptedMnemonic, WalletStorage}
import org.bitcoins.keymanager.WalletStorage
import scodec.bits.ByteVector
import java.nio.file.Path
@ -436,20 +436,6 @@ object DLCOracle {
// 585 is a random one I picked, unclaimed in https://github.com/satoshilabs/slips/blob/master/slip-0044.md
val R_VALUE_PURPOSE = 585
def apply(mnemonicCode: MnemonicCode)(implicit
conf: DLCOracleAppConfig): DLCOracle = {
val decryptedMnemonic = DecryptedMnemonic(mnemonicCode, TimeUtil.now)
val toWrite = conf.aesPasswordOpt match {
case Some(password) => decryptedMnemonic.encrypt(password)
case None => decryptedMnemonic
}
if (!conf.seedExists()) {
WalletStorage.writeSeedToDisk(conf.kmConf.seedPath, toWrite)
}
new DLCOracle()
}
/** Gets the DLC oracle from the given datadir */
def fromDatadir(path: Path, configs: Vector[Config])(implicit
ec: ExecutionContext): Future[DLCOracle] = {

View file

@ -55,11 +55,13 @@ case class DLCOracleAppConfig(
override def start(): Future[Unit] = {
logger.debug(s"Initializing dlc oracle setup")
super.start().flatMap { _ =>
val migrationsF = for {
_ <- super.start()
_ <- kmConf.start()
} yield {
if (Files.notExists(datadir)) {
Files.createDirectories(datadir)
}
val networkDir = {
val lastDirname = network match {
case MainNet => "mainnet"
@ -69,7 +71,6 @@ case class DLCOracleAppConfig(
}
baseDatadir.resolve(lastDirname)
}
// Move old db in network folder to oracle folder
val oldNetworkLocation = networkDir.resolve("oracle.sqlite")
if (!exists() && Files.exists(oldNetworkLocation)) {
@ -80,32 +81,17 @@ case class DLCOracleAppConfig(
logger.info(s"Applied $numMigrations to the dlc oracle project")
val migrations = migrationsApplied()
val migrationWorkAroundF =
if (migrations == 2 || migrations == 3) { // For V2/V3 migrations
logger.debug(s"Doing V2/V3 Migration")
migrations
}
val dummyMigrationTLV = EnumEventDescriptorV0TLV.dummy
migrationsF.flatMap { migrations =>
val migrationWorkAroundF = v2V3MigrationWorkaround(migrations)
val eventDAO = EventDAO()(ec, appConfig)
for {
// get all old events
allEvents <- eventDAO.findByEventDescriptor(dummyMigrationTLV)
allOutcomes <- EventOutcomeDAO()(ec, appConfig).findAll()
outcomesByNonce = allOutcomes.groupBy(_.nonce)
// Update them to have the correct event descriptor
updated = allEvents.map { eventDb =>
val outcomeDbs = outcomesByNonce(eventDb.nonce)
val descriptor =
EventOutcomeDbHelper.createEnumEventDescriptor(outcomeDbs)
eventDb.copy(eventDescriptorTLV = descriptor)
}
_ <- eventDAO.upsertAll(updated)
} yield ()
} else Future.unit
migrationWorkAroundF.map { _ =>
val initializeF = initializeKeyManager()
for {
_ <- initializeF
_ <- migrationWorkAroundF
} yield {
if (isHikariLoggingEnabled) {
//.get is safe because hikari logging is enabled
startHikariLogger(hikariLoggingInterval.get)
@ -146,7 +132,7 @@ case class DLCOracleAppConfig(
seedExists() && hasDb
}
def initialize(): Future[DLCOracle] = {
private def initializeKeyManager(): Future[DLCOracle] = {
if (!seedExists()) {
BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = kmParams,
@ -157,7 +143,8 @@ case class DLCOracleAppConfig(
}
}
DLCOracle.fromDatadir(directory, confs.toVector)
val o = new DLCOracle()(this)
Future.successful(o)
}
private lazy val rValueTable: TableQuery[Table[_]] = {
@ -174,6 +161,38 @@ case class DLCOracleAppConfig(
override def allTables: List[TableQuery[Table[_]]] =
List(rValueTable, eventTable, eventOutcomeTable)
/** @param migrations - The number of migrations we have run */
private def v2V3MigrationWorkaround(migrations: Int): Future[Unit] = {
val migrationWorkAroundF: Future[Unit] = {
if (migrations == 2 || migrations == 3) { // For V2/V3 migrations
logger.debug(s"Doing V2/V3 Migration")
val dummyMigrationTLV = EnumEventDescriptorV0TLV.dummy
val eventDAO = EventDAO()(ec, appConfig)
for {
// get all old events
allEvents <- eventDAO.findByEventDescriptor(dummyMigrationTLV)
allOutcomes <- EventOutcomeDAO()(ec, appConfig).findAll()
outcomesByNonce = allOutcomes.groupBy(_.nonce)
// Update them to have the correct event descriptor
updated = allEvents.map { eventDb =>
val outcomeDbs = outcomesByNonce(eventDb.nonce)
val descriptor =
EventOutcomeDbHelper.createEnumEventDescriptor(outcomeDbs)
eventDb.copy(eventDescriptorTLV = descriptor)
}
_ <- eventDAO.upsertAll(updated)
} yield ()
} else {
Future.unit
}
}
migrationWorkAroundF
}
}
object DLCOracleAppConfig extends AppConfigFactory[DLCOracleAppConfig] {

View file

@ -6,10 +6,8 @@ import org.bitcoins.core.api.chain.ChainQueryApi
import org.bitcoins.core.api.feeprovider.FeeRateApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.keymanagement.KeyManagerInitializeError
import org.bitcoins.db.DatabaseDriver._
import org.bitcoins.db._
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.{Wallet, WalletLogger}
@ -114,42 +112,17 @@ object DLCAppConfig extends AppConfigFactory[DLCAppConfig] with WalletLogger {
walletConf: WalletAppConfig,
dlcConf: DLCAppConfig,
ec: ExecutionContext): Future[DLCWallet] = {
val aesPasswordOpt = walletConf.aesPasswordOpt
val bip39PasswordOpt = walletConf.bip39PasswordOpt
walletConf.hasWallet().flatMap { walletExists =>
if (walletExists) {
logger.info(s"Using pre-existing wallet")
// TODO change me when we implement proper password handling
BIP39LockedKeyManager.unlock(aesPasswordOpt,
bip39PasswordOpt,
walletConf.kmParams) match {
case Right(km) =>
val wallet =
DLCWallet(km, nodeApi, chainQueryApi, feeRateApi, km.creationTime)
Future.successful(wallet)
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
val wallet =
DLCWallet(nodeApi, chainQueryApi, feeRateApi)
Future.successful(wallet)
} else {
logger.info(s"Initializing key manager")
val keyManagerE: Either[KeyManagerInitializeError, BIP39KeyManager] =
BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = walletConf.kmParams,
bip39PasswordOpt = bip39PasswordOpt)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
logger.info(s"Creating new wallet")
val unInitializedWallet =
DLCWallet(keyManager,
nodeApi,
chainQueryApi,
feeRateApi,
keyManager.creationTime)
DLCWallet(nodeApi, chainQueryApi, feeRateApi)
Wallet
.initialize(wallet = unInitializedWallet,

View file

@ -29,12 +29,10 @@ import org.bitcoins.crypto._
import org.bitcoins.dlc.wallet.internal._
import org.bitcoins.dlc.wallet.models._
import org.bitcoins.dlc.wallet.util.DLCStatusBuilder
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.{Wallet, WalletLogger}
import scodec.bits.ByteVector
import java.time.Instant
import scala.concurrent.{ExecutionContext, Future}
/** A [[Wallet]] with full DLC Functionality */
@ -1504,11 +1502,9 @@ abstract class DLCWallet
object DLCWallet extends WalletLogger {
private case class DLCWalletImpl(
keyManager: BIP39KeyManager,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi,
override val creationTime: Instant
feeRateApi: FeeRateApi
)(implicit
val walletConfig: WalletAppConfig,
val dlcConfig: DLCAppConfig,
@ -1516,14 +1512,12 @@ object DLCWallet extends WalletLogger {
) extends DLCWallet
def apply(
keyManager: BIP39KeyManager,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi,
creationTime: Instant)(implicit
feeRateApi: FeeRateApi)(implicit
config: WalletAppConfig,
dlcConfig: DLCAppConfig,
ec: ExecutionContext): DLCWallet = {
DLCWalletImpl(keyManager, nodeApi, chainQueryApi, feeRateApi, creationTime)
DLCWalletImpl(nodeApi, chainQueryApi, feeRateApi)
}
}

View file

@ -14,7 +14,6 @@ import org.bitcoins.core.protocol.blockchain.Block
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee._
import org.bitcoins.feeprovider._
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.node._
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
import org.bitcoins.rpc.config._
@ -23,8 +22,6 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.config.WalletAppConfig
import java.time.Instant
import scala.concurrent.{ExecutionContextExecutor, Future}
```
@ -81,17 +78,6 @@ val instance = BitcoindInstanceLocal.fromConfigFile(BitcoindConfig.DEFAULT_CONF_
val bitcoind = BitcoindV19RpcClient(instance)
val nodeApi = BitcoinSWalletTest.MockNodeApi
// Create our key manager
val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = Some(AesPassword.fromString("password")),
kmParams = walletConf.kmParams,
bip39PasswordOpt = None)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
throw new RuntimeException(s"Cannot initialize key manager err=$err")
}
// This function can be used to create a callback for when our chain api receives a transaction, block, or
// a block filter, the returned NodeCallbacks will contain the necessary items to initialize the callbacks
def createCallbacks(
@ -198,7 +184,7 @@ val chainApi = new ChainQueryApi {
// Finally, we can initialize our wallet with our own node api
val wallet =
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi, feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one), creationTime = Instant.now)
Wallet(nodeApi = nodeApi, chainQueryApi = chainApi, feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one))
// Then to trigger one of the events we can run
wallet.chainQueryApi.getFiltersBetweenHeights(100, 150)

View file

@ -265,6 +265,11 @@ bitcoin-s {
# Password that your seed is encrypted with
aesPassword = changeMe
# At least 16 bytes of entropy encoded in hex
# This will be used as the seed for any
# project that is dependent on the keymanager
entropy = ""
}
# Bitcoin-S provides manny different fee providers

View file

@ -11,7 +11,6 @@ import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee._
import org.bitcoins.core.util._
import org.bitcoins.feeprovider._
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.node._
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
import org.bitcoins.rpc.config._
@ -20,7 +19,6 @@ import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.wallet.Wallet
import org.bitcoins.wallet.config.WalletAppConfig
import java.time.Instant
import scala.concurrent.{ExecutionContextExecutor, Future}
```
@ -60,17 +58,6 @@ val bitcoind = BitcoindV19RpcClient(instance)
val chainApi = BitcoinSWalletTest.MockChainQueryApi
val aesPasswordOpt = Some(AesPassword.fromString("password"))
// Create our key manager
val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = walletConf.kmParams,
bip39PasswordOpt = None)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
throw new RuntimeException(s"Cannot initialize key manager err=$err")
}
// This function can be used to create a callback for when our node api calls downloadBlocks,
// more specifically it will call the function every time we receive a block, the returned
// NodeCallbacks will contain the necessary items to initialize the callbacks
@ -105,7 +92,7 @@ val exampleCallback = createCallback(exampleProcessBlock)
// Finally, we can initialize our wallet with our own node api
val wallet =
Wallet(keyManager = keyManager, nodeApi = nodeApi, chainQueryApi = chainApi, feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one), creationTime = Instant.now)
Wallet(nodeApi = nodeApi, chainQueryApi = chainApi, feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one))
// Then to trigger the event we can run
val exampleBlock = DoubleSha256Digest(

View file

@ -27,13 +27,11 @@ import org.bitcoins.crypto._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee._
import org.bitcoins.feeprovider._
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.rpc.client.v19.BitcoindV19RpcClient
import org.bitcoins.rpc.config.BitcoindInstanceLocal
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.wallet._
import org.bitcoins.wallet.config.WalletAppConfig
import java.time.Instant
import scala.concurrent.{ExecutionContextExecutor, Future}
```
@ -50,17 +48,6 @@ implicit val walletConf: WalletAppConfig =
val bitcoind = BitcoindV19RpcClient(BitcoindInstanceLocal.fromConfFile())
val aesPasswordOpt = Some(AesPassword.fromString("password"))
// Create our key manager
val keyManagerE = BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = walletConf.kmParams,
bip39PasswordOpt = None)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
throw new RuntimeException(s"Cannot initialize key manager err=$err")
}
// Here is a super simple example of a callback, this could be replaced with anything, from
// relaying the transaction on the network, finding relevant wallet outputs, verifying the transaction,
// or writing it to disk
@ -73,11 +60,10 @@ val exampleCallbacks = WalletCallbacks(
// Now we can create a wallet
val wallet =
Wallet(keyManager = keyManager,
Wallet(
nodeApi = bitcoind,
chainQueryApi = bitcoind,
feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one),
creationTime = Instant.now)
feeRateApi = ConstantFeeRateProvider(SatoshisPerVirtualByte.one))
// Finally, we can add the callbacks to our wallet config
walletConf.addCallbacks(exampleCallbacks)

View file

@ -115,12 +115,9 @@ val getBlockFunc = {hash: DoubleSha256DigestBE => bitcoind.getBlockRaw(hash) }
//yay! We are now all setup. Using our 3 functions above and a wallet, we can now sync
//a fresh wallet
implicit val walletAppConfig = WalletAppConfig.fromDefaultDatadir()
implicit val kmAppConfig = KeyManagerAppConfig.fromDefaultDatadir()
val keyManager: BIP39KeyManager = {
BIP39KeyManager.fromParams(walletAppConfig.kmParams,None,None).right.get
}
val feeRateProvider: FeeRateApi = MempoolSpaceProvider.fromBlockTarget(6, proxyParams = None)
val wallet = Wallet(keyManager, bitcoind, bitcoind, feeRateProvider, keyManager.creationTime)
val wallet = Wallet(bitcoind, bitcoind, feeRateProvider)
//yay! we have a synced wallet
val syncedWalletF = WalletSync.syncFullBlocks(wallet,

View file

@ -143,23 +143,14 @@ val syncF: Future[ChainApi] = configF.flatMap { _ =>
ChainSync.sync(chainHandler, getBlockHeaderFunc, getBestBlockHashFunc)
}
//initialize our key manager, where we store our keys
val aesPasswordOpt = Some(AesPassword.fromString("password"))
//you can add a password here if you want
//val bip39PasswordOpt = Some("my-password-here")
val bip39PasswordOpt = None
val keyManager = BIP39KeyManager.initialize(aesPasswordOpt, walletConfig.kmParams, bip39PasswordOpt).getOrElse {
throw new RuntimeException(s"Failed to initalize key manager")
}
// once this future completes, we have a initialized
// wallet
val wallet = Wallet(keyManager, new NodeApi {
val wallet = Wallet(new NodeApi {
override def broadcastTransactions(txs: Vector[Transaction]): Future[Unit] = Future.successful(())
override def downloadBlocks(blockHashes: Vector[DoubleSha256Digest]): Future[Unit] = Future.successful(())
}, chainApi, ConstantFeeRateProvider(SatoshisPerVirtualByte.one), creationTime = Instant.now)
}, chainApi, ConstantFeeRateProvider(SatoshisPerVirtualByte.one))
val walletF: Future[WalletApi] = configF.flatMap { _ =>
Wallet.initialize(wallet,bip39PasswordOpt)
Wallet.initialize(wallet, None)
}
// when this future completes, ww have sent a transaction

View file

@ -20,7 +20,7 @@ class WalletStorageTest extends BitcoinSWalletTest with BeforeAndAfterEach {
override type FixtureParam = WalletAppConfig
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
withWalletConfig(test)
withWalletConfigNotStarted(test)
def getSeedPath(config: WalletAppConfig): Path = {
config.kmConf.seedPath

View file

@ -2,12 +2,14 @@ package org.bitcoins.keymanager.config
import com.typesafe.config.ConfigFactory
import org.bitcoins.core.config.{MainNet, RegTest, TestNet3}
import org.bitcoins.core.crypto.{BIP39Seed, ExtKeyVersion, MnemonicCode}
import org.bitcoins.core.util.TimeUtil
import org.bitcoins.crypto.CryptoUtil
import org.bitcoins.keymanager.{DecryptedMnemonic, WalletStorage}
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import org.bitcoins.testkitcore.Implicits.GeneratorOps
import org.bitcoins.testkitcore.gen.CryptoGenerators
import org.bitcoins.testkit.util.BitcoinSAsyncTest
import java.nio.file.{Files, Path}
@ -107,4 +109,108 @@ class KeyManagerAppConfigTest extends BitcoinSAsyncTest {
.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)))
}
}
it must "initialize the keymanager with external entropy" in {
val tmpDir2 = BitcoinSTestAppConfig.tmpDir()
val tempFile = Files.createFile(tmpDir2.resolve("bitcoin-s.conf"))
val entropy = CryptoUtil.randomBytes(16)
val confStr = s"""
| bitcoin-s {
| network = testnet3
| keymanager.entropy=${entropy.toHex}
| }
""".stripMargin
val _ = Files.write(tempFile, confStr.getBytes())
val appConfig1 = KeyManagerAppConfig(directory = tmpDir2)
val appConfig2 = KeyManagerAppConfig(directory = tmpDir2)
val started1F = appConfig1.start()
val started2F = appConfig2.start()
for {
_ <- started1F
_ <- started2F
} yield {
//make sure they are internally consistent
assert(
appConfig1.toBip39KeyManager.getRootXPub == appConfig2.toBip39KeyManager.getRootXPub)
//manually build the xpub to make sure we are correct
val mnemonic = MnemonicCode.fromEntropy(entropy)
val bip39Seed = BIP39Seed.fromMnemonic(mnemonic, None)
val xpriv = bip39Seed.toExtPrivateKey(ExtKeyVersion.LegacyTestNet3Priv)
val xpub = xpriv.extPublicKey
assert(xpub == appConfig1.toBip39KeyManager.getRootXPub)
}
}
it must "initialize correctly with entropy set in the config file" in {
val tmpDir2 = BitcoinSTestAppConfig.tmpDir()
val tempFile = Files.createFile(tmpDir2.resolve("bitcoin-s.conf"))
val confStr = s"""
| bitcoin-s {
| network = testnet3
| }
""".stripMargin
val _ = Files.write(tempFile, confStr.getBytes())
val appConfig1 = KeyManagerAppConfig(directory = tmpDir2)
appConfig1
.start()
.map(_ => succeed)
}
it must "fail to start the key manager when there isn't enough entropy" in {
val tmpDir2 = BitcoinSTestAppConfig.tmpDir()
val tempFile = Files.createFile(tmpDir2.resolve("bitcoin-s.conf"))
val entropy = CryptoUtil.randomBytes(15)
val confStr = s"""
| bitcoin-s {
| network = testnet3
| keymanager.entropy=${entropy.toHex}
| }
""".stripMargin
val _ = Files.write(tempFile, confStr.getBytes())
val appConfig1 = KeyManagerAppConfig(directory = tmpDir2)
assertThrows[RuntimeException] {
appConfig1.start()
}
}
it must "fail to start the key manager when the entropy isn't hex chars" in {
val tmpDir2 = BitcoinSTestAppConfig.tmpDir()
val tempFile = Files.createFile(tmpDir2.resolve("bitcoin-s.conf"))
val confStr = s"""
| bitcoin-s {
| network = testnet3
| keymanager.entropy=invalidhexcharactersfortestcase
| }
""".stripMargin
val _ = Files.write(tempFile, confStr.getBytes())
val appConfig1 = KeyManagerAppConfig(directory = tmpDir2)
assertThrows[RuntimeException] {
appConfig1.start()
}
}
it must "fail to get bip39KeyManager when we haven't called start" in {
val tmpDir2 = BitcoinSTestAppConfig.tmpDir()
val tempFile = Files.createFile(tmpDir2.resolve("bitcoin-s.conf"))
val confStr = s"""
| bitcoin-s {
| network = testnet3
| keymanager.entropy=invalidhexcharactersfortestcase
| }
""".stripMargin
val _ = Files.write(tempFile, confStr.getBytes())
val appConfig1 = KeyManagerAppConfig(directory = tmpDir2)
assertThrows[RuntimeException] {
appConfig1.toBip39KeyManager
}
}
}

View file

@ -98,7 +98,7 @@ object BIP39KeyManager
val time = TimeUtil.now
val writtenToDiskE: Either[KeyManagerInitializeError, KeyManagerApi] =
val writtenToDiskE: Either[KeyManagerInitializeError, KeyManagerApi] = {
if (Files.notExists(seedPath)) {
logger.info(
s"Seed path parent directory does not exist, creating ${seedPath.getParent}")
@ -159,6 +159,7 @@ object BIP39KeyManager
JsonParsingError(err.toString)))
}
}
}
//verify we can unlock it for a sanity check
val unlocked = BIP39LockedKeyManager.unlock(passphraseOpt = aesPasswordOpt,

View file

@ -3,8 +3,13 @@ package org.bitcoins.keymanager.config
import com.typesafe.config.Config
import org.bitcoins.commons.config.{AppConfig, AppConfigFactory, ConfigOps}
import org.bitcoins.core.config.NetworkParameters
import org.bitcoins.crypto.AesPassword
import org.bitcoins.keymanager.WalletStorage
import org.bitcoins.core.crypto.MnemonicCode
import org.bitcoins.core.hd.{HDPurpose, HDPurposes}
import org.bitcoins.core.wallet.keymanagement.KeyManagerParams
import org.bitcoins.crypto.{AesPassword, CryptoUtil}
import org.bitcoins.keymanager.{ReadMnemonicError, WalletStorage}
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import scodec.bits.BitVector
import java.nio.file.{Files, Path}
import scala.concurrent.{ExecutionContext, Future}
@ -46,6 +51,29 @@ case class KeyManagerAppConfig(
seedFolder.resolve(s"$prefix${WalletStorage.ENCRYPTED_SEED_FILE_NAME}")
}
private lazy val defaultAccountKind: HDPurpose =
config.getString("bitcoin-s.wallet.defaultAccountType") match {
case "legacy" => HDPurposes.Legacy
case "segwit" => HDPurposes.SegWit
case "nested-segwit" => HDPurposes.NestedSegWit
// todo: validate this pre-app startup
case other: String =>
throw new RuntimeException(s"$other is not a valid account type!")
}
/** Entropy provided by the a user in their bitcoin-s.conf
* configuration file. This should be used to seed the keymanager
* rather than randomly generating entropy.
*/
private lazy val externalEntropy: Option[String] = {
val opt = config.getStringOrNone("bitcoin-s.keymanager.entropy")
opt
}
private val kmParams: KeyManagerParams = {
KeyManagerParams(seedPath, defaultAccountKind, network)
}
override def start(): Future[Unit] = {
val oldDefaultFile =
baseDatadir.resolve(WalletStorage.ENCRYPTED_SEED_FILE_NAME)
@ -59,8 +87,16 @@ case class KeyManagerAppConfig(
// Create directory
Files.createDirectories(newDefaultFile.getParent)
Files.copy(oldDefaultFile, newDefaultFile)
logger.info(
s"Migrated keymanager seed from=${oldDefaultFile.toAbsolutePath} to=${newDefaultFile.toAbsolutePath}")
Future.unit
} else if (!Files.exists(newDefaultFile)) {
initializeKeyManager()
} else {
logger.info(
s"Starting keymanager with seedPath=${seedPath.toAbsolutePath}")
Future.unit
}
Future.unit
}
override def stop(): Future[Unit] = Future.unit
@ -78,6 +114,66 @@ case class KeyManagerAppConfig(
def seedExists(): Boolean = {
WalletStorage.seedExists(seedPath)
}
/** Creates a [[BIP39KeyManager]] from the seed referenced by this [[KeyManagerAppConfig]]
* with the given wallet purpose
*/
def toBip39KeyManager: BIP39KeyManager = {
val kmE: Either[ReadMnemonicError, BIP39KeyManager] =
BIP39KeyManager.fromParams(kmParams = kmParams,
passwordOpt = aesPasswordOpt,
bip39PasswordOpt = bip39PasswordOpt)
kmE match {
case Left(err) =>
sys.error(
s"Could not create a BIP39KeyManager from the KeyManagerAppConfig, err=$err")
case Right(km) =>
km
}
}
/** Initializes the key manager. Takes into consideration if external entropy
* has been provided to bitcoin-s via the bitcoin-s.conf file
*/
private def initializeKeyManager(): Future[Unit] = {
val entropy: BitVector = externalEntropy match {
case Some(entropy) =>
logger.info(
s"Initializing new mnemonic seed at path=${seedPath.toAbsolutePath} with external entropy")
val hexOpt = BitVector.fromHex(entropy)
hexOpt match {
case Some(hex) => hex
case None =>
sys.error(
s"Entropy provided by bitcoin-s.keymanager.entropy was not valid hex, got=${entropy}")
}
case None =>
logger.info(
s"Initializing new mnemonic seed at path=${seedPath.toAbsolutePath}")
MnemonicCode.getEntropy256Bits
}
if (!CryptoUtil.checkEntropy(entropy)) {
sys.error(
s"The entropy used by bitcoin-s does not pass basic entropy sanity checks, got=$entropy")
}
val initE = BIP39KeyManager.initializeWithEntropy(
aesPasswordOpt = aesPasswordOpt,
entropy = entropy,
bip39PasswordOpt = bip39PasswordOpt,
kmParams = kmParams)
initE match {
case Right(km) =>
logger.info(
s"Successfully initialize seed at path with root xpub=${km.getRootXPub}")
Future.unit
case Left(err) =>
Future.failed(
new RuntimeException(
s"Failed to initialize mnemonic seed in keymanager with err=$err"))
}
}
}
object KeyManagerAppConfig extends AppConfigFactory[KeyManagerAppConfig] {

View file

@ -114,7 +114,7 @@ sealed abstract class CryptoGenerators {
* @see https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#From_mnemonic_to_seed
*/
def bip39Password: Gen[String] = {
Gen.asciiStr
Gen.alphaNumStr
}
/** Generates a valid BIP39 seed from

View file

@ -18,8 +18,9 @@ trait DLCOracleFixture extends BitcoinSFixture with EmbeddedPg {
BitcoinSTestAppConfig.getDLCOracleWithEmbeddedDbTestConfig(pgUrl)
val _ = conf.migrate()
val oracleF: Future[DLCOracle] = conf.initialize()
oracleF
val oracleConfF: Future[Unit] = conf.start()
oracleConfF.map(_ => new DLCOracle()(conf))
}
val destroy: DLCOracle => Future[Unit] = dlcOracle => {

View file

@ -15,7 +15,6 @@ import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee._
import org.bitcoins.crypto.{DoubleSha256Digest, DoubleSha256DigestBE}
import org.bitcoins.dlc.wallet.{DLCAppConfig, DLCWallet}
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.node.{
NodeCallbacks,
OnBlockReceived,
@ -73,11 +72,9 @@ trait BitcoinSWalletTest
walletConfig =>
implicit val newWalletConf =
getFreshWalletAppConfig.withOverrides(walletConfig)
val km = createNewKeyManager()(newWalletConf)
val bip39PasswordOpt = KeyManagerTestUtil.bip39PasswordOpt
makeDependentFixture(
build = createNewWallet(keyManager = km,
bip39PasswordOpt = bip39PasswordOpt,
build = createNewWallet(bip39PasswordOpt = bip39PasswordOpt,
extraConfig = Some(walletConfig),
nodeApi = nodeApi,
chainQueryApi = chainQueryApi),
@ -239,6 +236,17 @@ trait BitcoinSWalletTest
}
makeDependentFixture(builder, destroy = destroy)(test)
}
def withWalletConfigNotStarted(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletAppConfig] = () => {
createWalletAppConfigNotStarted(pgUrl, Vector.empty)
}
val destroy: WalletAppConfig => Future[Unit] = walletAppConfig => {
destroyWalletAppConfig(walletAppConfig)
}
makeDependentFixture(builder, destroy = destroy)(test)
}
}
object BitcoinSWalletTest extends WalletLogger {
@ -293,20 +301,6 @@ object BitcoinSWalletTest extends WalletLogger {
Future.successful(0)
}
private def createNewKeyManager(
bip39PasswordOpt: Option[String] = KeyManagerTestUtil.bip39PasswordOpt)(
implicit config: WalletAppConfig): BIP39KeyManager = {
val keyManagerE = BIP39KeyManager.initialize(config.aesPasswordOpt,
kmParams = config.kmParams,
bip39PasswordOpt =
bip39PasswordOpt)
keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
throw new RuntimeException(s"Cannot initialize key manager err=${err}")
}
}
private[bitcoins] class RandomFeeProvider extends FeeRateApi {
// Useful for tests
var lastFeeRate: Option[FeeUnit] = None
@ -324,6 +318,17 @@ object BitcoinSWalletTest extends WalletLogger {
configs: Vector[Config])(implicit
system: ActorSystem): Future[WalletAppConfig] = {
import system.dispatcher
val walletAppConfigF = createWalletAppConfigNotStarted(pgUrl, configs)
for {
appConfig <- walletAppConfigF
_ <- appConfig.start()
} yield appConfig
}
def createWalletAppConfigNotStarted(
pgUrl: () => Option[String],
configs: Vector[Config])(implicit
system: ActorSystem): Future[WalletAppConfig] = {
val baseConf = BaseWalletTest.getFreshWalletAppConfig(pgUrl, configs)
val walletNameOpt = if (NumberGenerator.bool.sampleSome) {
Some(StringGenerators.genNonEmptyString.sampleSome)
@ -341,7 +346,7 @@ object BitcoinSWalletTest extends WalletLogger {
case None => baseConf
}
walletConf.start().map(_ => walletConf)
Future.successful(walletConf)
}
/** Returns a function that can be used to create a wallet fixture.
@ -351,66 +356,81 @@ object BitcoinSWalletTest extends WalletLogger {
* or account type.
*/
private def createNewWallet(
keyManager: BIP39KeyManager,
bip39PasswordOpt: Option[String],
extraConfig: Option[Config],
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi)(implicit
config: WalletAppConfig,
ec: ExecutionContext): () => Future[Wallet] =
() => {
ec: ExecutionContext): () => Future[Wallet] = { () =>
{
val walletConfig = extraConfig match {
case None => config
case Some(c) => config.withOverrides(c)
}
val walletConfigWithBip39Pw = bip39PasswordOpt match {
case Some(pw) =>
val str = s"""bitcoin-s.keymanager.bip39password="$pw""""
val bip39Config = ConfigFactory.parseString(str)
walletConfig.withOverrides(bip39Config)
case None => walletConfig
}
// we want to check we're not overwriting
// any user data
AppConfig.throwIfDefaultDatadir(walletConfig)
walletConfig.start().flatMap { _ =>
walletConfigWithBip39Pw.start().flatMap { _ =>
val wallet =
Wallet(keyManager,
nodeApi,
chainQueryApi,
new RandomFeeProvider,
keyManager.creationTime)(walletConfig, ec)
Wallet.initialize(wallet, bip39PasswordOpt)
Wallet(nodeApi, chainQueryApi, new RandomFeeProvider)(
walletConfigWithBip39Pw,
ec)
Wallet.initialize(wallet, bip39PasswordOpt)(walletConfigWithBip39Pw, ec)
}
}
}
private def createDLCWallet(
keyManager: BIP39KeyManager,
bip39PasswordOpt: Option[String],
extraConfig: Option[Config],
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi)(implicit
config: BitcoinSAppConfig,
ec: ExecutionContext): Future[DLCWallet] = {
val defaultConf = config.walletConf
val walletConfig = extraConfig match {
case None => defaultConf
case Some(c) => defaultConf.withOverrides(c)
case None => config
case Some(c) => config.withOverrides(c)
}
val walletConfigWithBip39Pw = bip39PasswordOpt match {
case Some(pw) =>
val str = s"""bitcoin-s.keymanager.bip39password="$pw""""
val bip39Config = ConfigFactory.parseString(str)
walletConfig.withOverrides(bip39Config)
case None => walletConfig
}
// we want to check we're not overwriting
// any user data
AppConfig.throwIfDefaultDatadir(walletConfig)
AppConfig.throwIfDefaultDatadir(walletConfigWithBip39Pw.walletConf)
val initConfs = for {
_ <- walletConfig.start()
_ <- walletConfigWithBip39Pw.walletConf.start()
_ <- config.dlcConf.start()
} yield ()
initConfs.flatMap { _ =>
val wallet =
DLCWallet(keyManager,
nodeApi,
chainQueryApi,
new RandomFeeProvider,
keyManager.creationTime)(walletConfig, config.dlcConf, ec)
DLCWallet(nodeApi, chainQueryApi, new RandomFeeProvider)(
walletConfigWithBip39Pw.walletConf,
config.dlcConf,
ec)
Wallet
.initialize(wallet, bip39PasswordOpt)(walletConfig, ec)
.initialize(wallet, bip39PasswordOpt)(
walletConfigWithBip39Pw.walletConf,
ec)
.map(_.asInstanceOf[DLCWallet])
}
}
@ -429,14 +449,12 @@ object BitcoinSWalletTest extends WalletLogger {
case Some(walletConf) =>
config.withOverrides(walletConf)
}
val km =
createNewKeyManager(bip39PasswordOpt = bip39PasswordOpt)(newWalletConf)
createNewWallet(
keyManager = km,
bip39PasswordOpt = bip39PasswordOpt,
extraConfig = extraConfig,
nodeApi = nodeApi,
chainQueryApi = chainQueryApi)(config, ec)() // get the standard config
createNewWallet(bip39PasswordOpt = bip39PasswordOpt,
extraConfig = extraConfig,
nodeApi = nodeApi,
chainQueryApi = chainQueryApi)(newWalletConf,
ec
)() // get the standard config
}
/** Creates a default wallet with bitcoind where the [[ChainQueryApi]] fed to the wallet
@ -462,12 +480,10 @@ object BitcoinSWalletTest extends WalletLogger {
//create the wallet with the appropriate callbacks now that
//we have them
walletWithCallback = Wallet(
keyManager = wallet.keyManager,
nodeApi =
SyncUtil.getNodeApiWalletCallback(bitcoind, walletCallbackP.future),
chainQueryApi = bitcoind,
feeRateApi = new RandomFeeProvider,
creationTime = wallet.keyManager.creationTime
feeRateApi = new RandomFeeProvider
)(wallet.walletConfig, wallet.ec)
//complete the walletCallbackP so we can handle the callbacks when they are
//called without hanging forever.
@ -508,15 +524,11 @@ object BitcoinSWalletTest extends WalletLogger {
config: BitcoinSAppConfig,
system: ActorSystem): Future[DLCWallet] = {
implicit val ec: ExecutionContextExecutor = system.dispatcher
val km =
createNewKeyManager(bip39PasswordOpt = bip39PasswordOpt)(
config.walletConf)
for {
wallet <- createDLCWallet(km,
bip39PasswordOpt,
extraConfig,
nodeApi,
chainQueryApi)
wallet <- createDLCWallet(bip39PasswordOpt = bip39PasswordOpt,
extraConfig = extraConfig,
nodeApi = nodeApi,
chainQueryApi = chainQueryApi)
account1 = WalletTestUtil.getHdAccount1(wallet.walletConfig)
newAccountWallet <- wallet.createNewAccount(hdAccount = account1,
kmParams =

View file

@ -1,16 +1,14 @@
package org.bitcoins.wallet
import com.typesafe.config.{Config, ConfigFactory}
import org.bitcoins.commons.serializers.JsonSerializers._
import org.bitcoins.core.api.wallet.db._
import org.bitcoins.core.crypto.{ExtPublicKey, MnemonicCode}
import org.bitcoins.core.hd._
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.util.{FutureUtil, TimeUtil}
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.keymanagement.KeyManagerParams
import org.bitcoins.feeprovider.ConstantFeeRateProvider
import org.bitcoins.keymanager.bip39.BIP39KeyManager
import org.bitcoins.testkit.BitcoinSTestAppConfig
import org.bitcoins.testkit.fixtures.EmptyFixture
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
@ -26,6 +24,7 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.io.Source
class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
import org.bitcoins.commons.serializers.JsonSerializers._
val mnemonic = MnemonicCode.fromWords(
Vector(
@ -131,42 +130,35 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
lazy val nestedVectors =
vectors.filter(_.pathType == HDPurposes.NestedSegWit)
def configForPurpose(purpose: HDPurpose): Config = {
def configForPurposeAndSeed(purpose: HDPurpose): Config = {
val purposeStr = purpose match {
case HDPurposes.Legacy => "legacy"
case HDPurposes.SegWit => "segwit"
case HDPurposes.NestedSegWit => "nested-segwit"
case other => fail(s"unexpected purpose: $other")
}
val entropy = mnemonic.toEntropy.toHex
val confStr = s"""bitcoin-s.wallet.defaultAccountType = $purposeStr
|bitcoin-s.network = mainnet""".stripMargin
|bitcoin-s.network = mainnet
|bitcoin-s.keymanager.entropy=${entropy}
|""".stripMargin
ConfigFactory.parseString(confStr)
}
private def getWallet(config: WalletAppConfig)(implicit
ec: ExecutionContext): Future[Wallet] = {
import system.dispatcher
val bip39PasswordOpt = None
val kmE = BIP39KeyManager.initializeWithEntropy(
aesPasswordOpt = config.aesPasswordOpt,
entropy = mnemonic.toEntropy,
bip39PasswordOpt = bip39PasswordOpt,
kmParams = config.kmParams)
kmE match {
case Left(err) =>
Future.failed(
new RuntimeException(s"Failed to initialize km with err=${err}"))
case Right(km) =>
val wallet =
Wallet(km,
MockNodeApi,
MockChainQueryApi,
ConstantFeeRateProvider(SatoshisPerVirtualByte.one),
TimeUtil.now)(config, ec)
val walletF =
Wallet.initialize(wallet = wallet,
bip39PasswordOpt = bip39PasswordOpt)(config, ec)
walletF
}
val startedF = config.start()
for {
_ <- startedF
wallet =
Wallet(MockNodeApi,
MockChainQueryApi,
ConstantFeeRateProvider(SatoshisPerVirtualByte.one))(config, ec)
init <- Wallet.initialize(wallet = wallet,
bip39PasswordOpt = bip39PasswordOpt)(config, ec)
} yield init
}
case class AccountAndAddrsAndVector(
@ -230,7 +222,7 @@ class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
}
private def testAccountType(purpose: HDPurpose): Future[Assertion] = {
val confOverride = configForPurpose(purpose)
val confOverride = configForPurposeAndSeed(purpose)
implicit val conf: WalletAppConfig =
BitcoinSTestAppConfig.getSpvTestConfig(confOverride).walletConf

View file

@ -7,9 +7,7 @@ import org.bitcoins.core.hd.{AddressType, HDAccount, HDChainType}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.script._
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError
import org.bitcoins.core.wallet.keymanagement.KeyManagerUnlockError.MnemonicNotFound
import org.bitcoins.crypto.{AesPassword, DoubleSha256DigestBE, ECPublicKey}
import org.bitcoins.crypto.{DoubleSha256DigestBE, ECPublicKey}
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.bitcoins.testkitcore.util.TransactionTestUtil._
import org.scalatest.FutureOutcome
@ -126,21 +124,6 @@ class WalletUnitTest extends BitcoinSWalletTest {
} yield res
}
it should "fail to unlock the wallet with a bad aes password" in {
wallet: Wallet =>
val badPassphrase = Some(AesPassword.fromNonEmptyString("bad"))
val errorType = wallet.unlock(badPassphrase, None) match {
case Right(_) => fail("Unlocked wallet with bad password!")
case Left(err) => err
}
errorType match {
case KeyManagerUnlockError.MnemonicNotFound => fail(MnemonicNotFound)
case KeyManagerUnlockError.BadPassword => succeed
case KeyManagerUnlockError.JsonParsingError(message) => fail(message)
}
}
it should "match block filters" in { wallet: Wallet =>
for {
height <- wallet.chainQueryApi.getFilterCount()

View file

@ -27,14 +27,11 @@ import org.bitcoins.core.script.control.OP_RETURN
import org.bitcoins.core.util.{BitcoinScriptUtil, FutureUtil, HDUtil}
import org.bitcoins.core.wallet.builder._
import org.bitcoins.core.wallet.fee._
import org.bitcoins.core.wallet.keymanagement.{
KeyManagerParams,
KeyManagerUnlockError
}
import org.bitcoins.core.wallet.keymanagement.{KeyManagerParams}
import org.bitcoins.core.wallet.utxo.TxoState._
import org.bitcoins.core.wallet.utxo._
import org.bitcoins.crypto._
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
import org.bitcoins.keymanager.bip39.{BIP39KeyManager}
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.wallet.internal._
import org.bitcoins.wallet.models._
@ -54,7 +51,9 @@ abstract class Wallet
with RescanHandling
with WalletLogger {
override def keyManager: BIP39KeyManager
override def keyManager: BIP39KeyManager = {
walletConfig.kmConf.toBip39KeyManager
}
implicit val ec: ExecutionContext
@ -230,29 +229,6 @@ abstract class Wallet
}
}
def unlock(
passphraseOpt: Option[AesPassword],
bip39PasswordOpt: Option[String]): Either[
KeyManagerUnlockError,
Wallet] = {
val kmParams = walletConfig.kmParams
val unlockedKeyManagerE =
BIP39LockedKeyManager.unlock(passphraseOpt = passphraseOpt,
bip39PasswordOpt = bip39PasswordOpt,
kmParams = kmParams)
unlockedKeyManagerE match {
case Right(km) =>
val w = Wallet(keyManager = km,
nodeApi = nodeApi,
chainQueryApi = chainQueryApi,
feeRateApi = feeRateApi,
creationTime = km.creationTime)
Right(w)
case Left(err) => Left(err)
}
}
override def broadcastTransaction(transaction: Transaction): Future[Unit] =
for {
_ <- nodeApi.broadcastTransaction(transaction)
@ -962,25 +938,21 @@ abstract class Wallet
object Wallet extends WalletLogger {
private case class WalletImpl(
keyManager: BIP39KeyManager,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi,
override val creationTime: Instant
feeRateApi: FeeRateApi
)(implicit
val walletConfig: WalletAppConfig,
val ec: ExecutionContext
) extends Wallet
def apply(
keyManager: BIP39KeyManager,
nodeApi: NodeApi,
chainQueryApi: ChainQueryApi,
feeRateApi: FeeRateApi,
creationTime: Instant)(implicit
feeRateApi: FeeRateApi)(implicit
config: WalletAppConfig,
ec: ExecutionContext): Wallet = {
WalletImpl(keyManager, nodeApi, chainQueryApi, feeRateApi, creationTime)
WalletImpl(nodeApi, chainQueryApi, feeRateApi)
}
/** Creates the level 0 account for the given HD purpose, if the root account exists do nothing */

View file

@ -8,14 +8,10 @@ import org.bitcoins.core.api.feeprovider.FeeRateApi
import org.bitcoins.core.api.node.NodeApi
import org.bitcoins.core.hd._
import org.bitcoins.core.util.Mutable
import org.bitcoins.core.wallet.keymanagement.{
KeyManagerInitializeError,
KeyManagerParams
}
import org.bitcoins.core.wallet.keymanagement.KeyManagerParams
import org.bitcoins.crypto.AesPassword
import org.bitcoins.db.DatabaseDriver.{PostgreSQL, SQLite}
import org.bitcoins.db._
import org.bitcoins.keymanager.bip39.{BIP39KeyManager, BIP39LockedKeyManager}
import org.bitcoins.keymanager.config.KeyManagerAppConfig
import org.bitcoins.tor.config.TorAppConfig
import org.bitcoins.wallet.config.WalletAppConfig.RebroadcastTransactionsRunnable
@ -24,14 +20,7 @@ import org.bitcoins.wallet.models.AccountDAO
import org.bitcoins.wallet.{Wallet, WalletCallbacks, WalletLogger}
import java.nio.file.{Files, Path, Paths}
import java.util.concurrent.{
ExecutorService,
Executors,
ScheduledExecutorService,
ScheduledFuture,
ThreadFactory,
TimeUnit
}
import java.util.concurrent._
import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}
import scala.concurrent.{Await, ExecutionContext, Future}
@ -182,6 +171,7 @@ case class WalletAppConfig(
override def start(): Future[Unit] = {
for {
_ <- super.start()
_ <- kmConf.start()
} yield {
logger.debug(s"Initializing wallet setup")
@ -343,42 +333,17 @@ object WalletAppConfig
walletConf: WalletAppConfig,
ec: ExecutionContext): Future[Wallet] = {
walletConf.hasWallet().flatMap { walletExists =>
val aesPasswordOpt = walletConf.aesPasswordOpt
val bip39PasswordOpt = walletConf.bip39PasswordOpt
if (walletExists) {
logger.info(s"Using pre-existing wallet")
// TODO change me when we implement proper password handling
BIP39LockedKeyManager.unlock(aesPasswordOpt,
bip39PasswordOpt,
walletConf.kmParams) match {
case Right(km) =>
val wallet =
Wallet(km, nodeApi, chainQueryApi, feeRateApi, km.creationTime)
Future.successful(wallet)
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
val wallet =
Wallet(nodeApi, chainQueryApi, feeRateApi)
Future.successful(wallet)
} else {
logger.info(s"Initializing key manager")
val keyManagerE: Either[KeyManagerInitializeError, BIP39KeyManager] =
BIP39KeyManager.initialize(aesPasswordOpt = aesPasswordOpt,
kmParams = walletConf.kmParams,
bip39PasswordOpt = bip39PasswordOpt)
val keyManager = keyManagerE match {
case Right(keyManager) => keyManager
case Left(err) =>
sys.error(s"Error initializing key manager, err=${err}")
}
logger.info(s"Creating new wallet")
val unInitializedWallet =
Wallet(keyManager,
nodeApi,
chainQueryApi,
feeRateApi,
keyManager.creationTime)
Wallet(nodeApi, chainQueryApi, feeRateApi)
Wallet.initialize(wallet = unInitializedWallet,
bip39PasswordOpt = bip39PasswordOpt)