diff --git a/.github/workflows/Linux_2.12_KeyManager_Wallet_DLC_Tests.yml b/.github/workflows/Linux_2.12_KeyManager_Wallet_DLC_Tests.yml index f5a8bd3c01..9a204031db 100644 --- a/.github/workflows/Linux_2.12_KeyManager_Wallet_DLC_Tests.yml +++ b/.github/workflows/Linux_2.12_KeyManager_Wallet_DLC_Tests.yml @@ -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 diff --git a/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala index b90fbb4a61..c339c67eee 100644 --- a/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala +++ b/app/oracle-server/src/main/scala/org/bitcoins/oracle/server/OracleServerMain.scala @@ -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) => diff --git a/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala b/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala index c4c9857a99..d1f8c43088 100644 --- a/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala +++ b/app/server/src/main/scala/org/bitcoins/server/BitcoindRpcBackendUtil.scala @@ -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) diff --git a/crypto-test/src/test/scala/org/bitcoins/crypto/CryptoUtilTest.scala b/crypto-test/src/test/scala/org/bitcoins/crypto/CryptoUtilTest.scala index 89e79992f3..6743dabcf1 100644 --- a/crypto-test/src/test/scala/org/bitcoins/crypto/CryptoUtilTest.scala +++ b/crypto-test/src/test/scala/org/bitcoins/crypto/CryptoUtilTest.scala @@ -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)) + } + } } diff --git a/crypto/src/main/scala/org/bitcoins/crypto/CryptoRuntime.scala b/crypto/src/main/scala/org/bitcoins/crypto/CryptoRuntime.scala index 82940a325b..b8c21cf430 100644 --- a/crypto/src/main/scala/org/bitcoins/crypto/CryptoRuntime.scala +++ b/crypto/src/main/scala/org/bitcoins/crypto/CryptoRuntime.scala @@ -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) } diff --git a/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/DLCOracleTest.scala b/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/DLCOracleTest.scala index a916d65173..29325d6ef0 100644 --- a/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/DLCOracleTest.scala +++ b/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/DLCOracleTest.scala @@ -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" diff --git a/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfigTest.scala b/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfigTest.scala index 7b6ba9831f..f771f6ed48 100644 --- a/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfigTest.scala +++ b/dlc-oracle-test/src/test/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfigTest.scala @@ -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 diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala index c931cb9030..3efd886bd3 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/DLCOracle.scala @@ -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] = { diff --git a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfig.scala b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfig.scala index 4637437b79..3a41a3ce50 100644 --- a/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfig.scala +++ b/dlc-oracle/src/main/scala/org/bitcoins/dlc/oracle/config/DLCOracleAppConfig.scala @@ -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] { diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala index e42385c5b2..895ea7ffe8 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala @@ -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, diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala index c6b728833d..514c8a717e 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala @@ -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) } } diff --git a/docs/chain/chain-query-api.md b/docs/chain/chain-query-api.md index 98b4732067..2be3f109a7 100644 --- a/docs/chain/chain-query-api.md +++ b/docs/chain/chain-query-api.md @@ -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) diff --git a/docs/config/configuration.md b/docs/config/configuration.md index 98cc7e8b03..05aebffb81 100644 --- a/docs/config/configuration.md +++ b/docs/config/configuration.md @@ -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 diff --git a/docs/node/node-api.md b/docs/node/node-api.md index 6e7cb005fd..cffa6e167d 100644 --- a/docs/node/node-api.md +++ b/docs/node/node-api.md @@ -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( diff --git a/docs/wallet/wallet-callbacks.md b/docs/wallet/wallet-callbacks.md index eca41b6336..709c0bb9ab 100644 --- a/docs/wallet/wallet-callbacks.md +++ b/docs/wallet/wallet-callbacks.md @@ -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) diff --git a/docs/wallet/wallet-sync.md b/docs/wallet/wallet-sync.md index ae54662364..eaccb161ca 100644 --- a/docs/wallet/wallet-sync.md +++ b/docs/wallet/wallet-sync.md @@ -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, diff --git a/docs/wallet/wallet.md b/docs/wallet/wallet.md index a9c28d7614..6933fefc1a 100644 --- a/docs/wallet/wallet.md +++ b/docs/wallet/wallet.md @@ -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 diff --git a/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala b/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala index 3c42d3c2d1..1300fa50c3 100644 --- a/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala +++ b/key-manager-test/src/test/scala/org/bitcoins/keymanager/WalletStorageTest.scala @@ -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 diff --git a/key-manager-test/src/test/scala/org/bitcoins/keymanager/config/KeyManagerAppConfigTest.scala b/key-manager-test/src/test/scala/org/bitcoins/keymanager/config/KeyManagerAppConfigTest.scala index 17096ba9fd..a92c43a73d 100644 --- a/key-manager-test/src/test/scala/org/bitcoins/keymanager/config/KeyManagerAppConfigTest.scala +++ b/key-manager-test/src/test/scala/org/bitcoins/keymanager/config/KeyManagerAppConfigTest.scala @@ -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 + } + } } diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala index 3d961fb14e..0c495331e6 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/bip39/BIP39KeyManager.scala @@ -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, diff --git a/key-manager/src/main/scala/org/bitcoins/keymanager/config/KeyManagerAppConfig.scala b/key-manager/src/main/scala/org/bitcoins/keymanager/config/KeyManagerAppConfig.scala index abb4a90a90..f688966e26 100644 --- a/key-manager/src/main/scala/org/bitcoins/keymanager/config/KeyManagerAppConfig.scala +++ b/key-manager/src/main/scala/org/bitcoins/keymanager/config/KeyManagerAppConfig.scala @@ -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] { diff --git a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/CryptoGenerators.scala b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/CryptoGenerators.scala index b316073148..439155d83a 100644 --- a/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/CryptoGenerators.scala +++ b/testkit-core/src/main/scala/org/bitcoins/testkitcore/gen/CryptoGenerators.scala @@ -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 diff --git a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala index 6d998fe184..d14f32bdf0 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/fixtures/DLCOracleFixture.scala @@ -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 => { diff --git a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala index f82d194bca..8a8e9a759c 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/wallet/BitcoinSWalletTest.scala @@ -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 = diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala index cf9a9f22ea..a3f9d040b1 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/TrezorAddressTest.scala @@ -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 diff --git a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala index 5b9b736a8c..4b942be7d0 100644 --- a/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala +++ b/wallet-test/src/test/scala/org/bitcoins/wallet/WalletUnitTest.scala @@ -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() diff --git a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala index 84f255cc47..badfc360df 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/Wallet.scala @@ -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 */ diff --git a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala index 4791a45883..30e8eaf672 100644 --- a/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala +++ b/wallet/src/main/scala/org/bitcoins/wallet/config/WalletAppConfig.scala @@ -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)