Merge pull request #497 from torkelrogstad/2019-06-05-legacy-addresses

Legacy addresses
This commit is contained in:
Torkel Rogstad 2019-06-06 16:08:49 +02:00 committed by GitHub
commit bd10f1c35e
13 changed files with 319 additions and 68 deletions

View file

@ -12,12 +12,12 @@ import scala.concurrent.Promise
import scala.util.Success
import scala.util.Failure
case class ChainAppConfig(val confs: Config*) extends AppConfig {
case class ChainAppConfig(private val confs: Config*) extends AppConfig {
override protected val configOverrides: List[Config] = confs.toList
override protected val moduleName: String = "chain"
override protected type ConfigType = ChainAppConfig
override protected def newConfigOfType(
configs: List[Config]): ChainAppConfig = ChainAppConfig(configs: _*)
override protected def newConfigOfType(configs: Seq[Config]): ChainAppConfig =
ChainAppConfig(configs: _*)
/**
* Checks whether or not the chain project is initialized by

View file

@ -1,4 +1,9 @@
bitcoin-s {
datadir = ${HOME}/.bitcoin-s
network = regtest # regtest, testnet3, mainnet
# settings for wallet module
wallet {
defaultAccountType = legacy # legacy, segwit, nested-segwit
}
}

View file

@ -54,7 +54,7 @@ abstract class AppConfig extends BitcoinSLogger {
protected type ConfigType <: AppConfig
/** Constructor to make a new instance of this config type */
protected def newConfigOfType(configOverrides: List[Config]): ConfigType
protected def newConfigOfType(configOverrides: Seq[Config]): ConfigType
/** List of user-provided configs that should
* override defaults
@ -88,9 +88,23 @@ abstract class AppConfig extends BitcoinSLogger {
logger.debug(oldConfStr)
}
val newConf = newConfigOfType(
configOverrides = List(firstOverride) ++ configs
)
val configOverrides = firstOverride +: configs
if (logger.isTraceEnabled()) {
configOverrides.zipWithIndex.foreach {
case (c, idx) => logger.trace(s"Override no. $idx: ${c.asReadableJson}")
}
}
val newConf = {
// the idea here is that after resolving the configuration,
// we extract the value under the 'bitcoin-s' key and use
// that as our config. here we have to do the reverse, to
// get the keys to resolve correctly
val reconstructedStr = s"""
bitcoin-s: ${this.config.asReadableJson}
"""
val reconstructed = ConfigFactory.parseString(reconstructedStr)
newConfigOfType(reconstructed +: configOverrides)
}
// to avoid non-necessary lazy load
if (logger.isDebugEnabled()) {
@ -229,7 +243,8 @@ abstract class AppConfig extends BitcoinSLogger {
.reduce(_.withFallback(_))
val interestingOverrides = overrides.getConfig("bitcoin-s")
logger.trace(s"User-overrides for bitcoin-s config:")
logger.trace(
s"${configOverrides.length} user-overrides for bitcoin-s config:")
logger.trace(interestingOverrides.asReadableJson)
// to make the overrides actually override
@ -282,7 +297,7 @@ object AppConfig extends BitcoinSLogger {
*/
private[bitcoins] def throwIfDefaultDatadir(config: AppConfig): Unit = {
val datadirStr = config.datadir.toString()
AppConfig.defaultDatadirRegex.findFirstMatchIn(datadirStr) match {
defaultDatadirRegex.findFirstMatchIn(datadirStr) match {
case None => () // pass
case Some(_) =>
val errMsg =
@ -291,6 +306,8 @@ object AppConfig extends BitcoinSLogger {
s"Your data directory is $datadirStr. This would cause tests to potentially",
"overwrite your existing data, which you probably don't want."
).mkString(" ")
logger.error(errMsg)
logger.error(s"Configuration: ${config.config.asReadableJson}")
throw new RuntimeException(errMsg)
}
}

View file

@ -8,11 +8,11 @@ import org.bitcoins.node.db.NodeDbManagement
import scala.util.Failure
import scala.util.Success
case class NodeAppConfig(confs: Config*) extends AppConfig {
case class NodeAppConfig(private val confs: Config*) extends AppConfig {
override val configOverrides: List[Config] = confs.toList
override protected def moduleName: String = "node"
override protected type ConfigType = NodeAppConfig
override protected def newConfigOfType(configs: List[Config]): NodeAppConfig =
override protected def newConfigOfType(configs: Seq[Config]): NodeAppConfig =
NodeAppConfig(configs: _*)
/**

View file

@ -0,0 +1,36 @@
package org.bitcoins.wallet
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.bitcoins.wallet.util.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.bitcoins.wallet.api.UnlockWalletError.BadPassword
import org.bitcoins.wallet.api.UnlockWalletError.JsonParsingError
import org.bitcoins.wallet.api.UnlockWalletSuccess
import org.bitcoins.core.crypto.AesPassword
import org.bitcoins.wallet.api.UnlockWalletError.MnemonicNotFound
import com.typesafe.config.ConfigFactory
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.core.hd.HDPurposes
class LegacyWalletTest extends BitcoinSWalletTest {
override type FixtureParam = UnlockedWalletApi
override def withFixture(test: OneArgAsyncTest): FutureOutcome =
withLegacyWallet(test)
it should "generate legacy addresses" in { wallet: UnlockedWalletApi =>
for {
addr <- wallet.getNewAddress()
account <- wallet.getDefaultAccount()
otherAddr <- wallet.getNewAddress()
allAddrs <- wallet.listAddresses()
} yield {
assert(account.hdAccount.purpose == HDPurposes.Legacy)
assert(allAddrs.forall(_.address.isInstanceOf[P2PKHAddress]))
assert(allAddrs.length == 2)
assert(allAddrs.exists(_.address == addr))
assert(allAddrs.exists(_.address == otherAddr))
}
}
}

View file

@ -0,0 +1,38 @@
package org.bitcoins.wallet
import org.bitcoins.wallet.api.UnlockedWalletApi
import org.bitcoins.wallet.util.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.bitcoins.wallet.api.UnlockWalletError.BadPassword
import org.bitcoins.wallet.api.UnlockWalletError.JsonParsingError
import org.bitcoins.wallet.api.UnlockWalletSuccess
import org.bitcoins.core.crypto.AesPassword
import org.bitcoins.wallet.api.UnlockWalletError.MnemonicNotFound
import com.typesafe.config.ConfigFactory
import org.bitcoins.core.protocol.P2PKHAddress
import org.bitcoins.core.protocol.Bech32Address
import org.bitcoins.core.hd.HDPurposes
class SegwitWalletTest extends BitcoinSWalletTest {
override type FixtureParam = UnlockedWalletApi
override def withFixture(test: OneArgAsyncTest): FutureOutcome = {
withSegwitWallet(test)
}
it should "generate segwit addresses" in { wallet: UnlockedWalletApi =>
for {
addr <- wallet.getNewAddress()
account <- wallet.getDefaultAccount()
otherAddr <- wallet.getNewAddress()
allAddrs <- wallet.listAddresses()
} yield {
assert(account.hdAccount.purpose == HDPurposes.SegWit)
assert(allAddrs.forall(_.address.isInstanceOf[Bech32Address]))
assert(allAddrs.length == 2)
assert(allAddrs.exists(_.address == addr))
assert(allAddrs.exists(_.address == otherAddr))
}
}
}

View file

@ -8,6 +8,8 @@ import com.typesafe.config.ConfigFactory
import org.bitcoins.core.config.RegTest
import org.bitcoins.core.config.MainNet
import org.bitcoins.wallet.config.WalletAppConfig
import java.nio.file.Paths
import org.bitcoins.core.hd.HDPurposes
class WalletAppConfigTest extends BitcoinSUnitTest {
val config = WalletAppConfig()
@ -24,6 +26,40 @@ class WalletAppConfigTest extends BitcoinSUnitTest {
assert(mainnet.network == MainNet)
}
it should "not matter how the overrides are passed in" in {
val dir = Paths.get("/", "bar", "biz")
val overrider = ConfigFactory.parseString(s"""
|bitcoin-s {
| datadir = $dir
| network = mainnet
|}
|""".stripMargin)
val throughConstuctor = WalletAppConfig(overrider)
val throughWithOverrides = config.withOverrides(overrider)
assert(throughWithOverrides.network == MainNet)
assert(throughWithOverrides.network == throughConstuctor.network)
assert(throughWithOverrides.datadir.startsWith(dir))
assert(throughWithOverrides.datadir == throughConstuctor.datadir)
}
it must "be overridable without screwing up other options" in {
val dir = Paths.get("/", "foo", "bar")
val otherConf = ConfigFactory.parseString(s"bitcoin-s.datadir = $dir")
val thirdConf = ConfigFactory.parseString(
s"bitcoin-s.wallet.defaultAccountType = nested-segwit")
val overriden = config.withOverrides(otherConf)
val twiceOverriden = overriden.withOverrides(thirdConf)
assert(overriden.datadir.startsWith(dir))
assert(twiceOverriden.datadir.startsWith(dir))
assert(twiceOverriden.defaultAccountKind == HDPurposes.NestedSegWit)
}
it must "be overridable with multiple levels" in {
val testnet = ConfigFactory.parseString("bitcoin-s.network = testnet3")
val mainnet = ConfigFactory.parseString("bitcoin-s.network = mainnet")

View file

@ -23,14 +23,11 @@ class WalletUnitTest extends BitcoinSWalletTest {
accounts <- wallet.listAccounts()
addresses <- wallet.listAddresses()
} yield {
assert(accounts.length == 1)
assert(accounts.length == 3) // legacy, segwit and nested segwit
assert(addresses.isEmpty)
}
}
// eventually this test should NOT succeed, as BIP44
// requires a limit to addresses being generated when
// they haven't received any funds
it should "generate addresses" in { wallet: UnlockedWalletApi =>
for {
addr <- wallet.getNewAddress()

View file

@ -23,6 +23,8 @@ import scala.concurrent.{ExecutionContext, Future}
import org.bitcoins.db.AppConfig
import org.bitcoins.testkit.BitcoinSAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig._
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
trait BitcoinSWalletTest
extends fixture.AsyncFlatSpec
@ -57,21 +59,64 @@ trait BitcoinSWalletTest
.map(_ => ())
}
def createNewWallet(): Future[UnlockedWalletApi] = {
for {
_ <- config.initialize()
wallet <- Wallet.initialize().map {
case InitializeWalletSuccess(wallet) => wallet
case err: InitializeWalletError =>
logger.error(s"Could not initialize wallet: $err")
fail(err)
/** Returns a function that can be used to create a wallet fixture.
* If you pass in a configuration to this method that configuration
* is given to the wallet as user-provided overrides. You could for
* example use this to override the default data directory, network
* or account type.
*/
private def createNewWallet(
extraConfig: Option[Config]): () => Future[UnlockedWalletApi] =
() => {
val defaultConf = config.walletConf
val walletConfig = extraConfig match {
case None => defaultConf
case Some(c) => defaultConf.withOverrides(c)
}
} yield wallet
// we want to check we're not overwriting
// any user data
AppConfig.throwIfDefaultDatadir(walletConfig)
walletConfig.initialize().flatMap { _ =>
Wallet
.initialize()(implicitly[ExecutionContext], walletConfig)
.map {
case InitializeWalletSuccess(wallet) => wallet
case err: InitializeWalletError =>
logger.error(s"Could not initialize wallet: $err")
fail(err)
}
}
}
/** Creates a wallet with the default configuration */
private def createDefaultWallet(): Future[UnlockedWalletApi] =
createNewWallet(None)() // get the standard config
/** Lets you customize the parameters for the created wallet */
val withNewConfiguredWallet: Config => OneArgAsyncTest => FutureOutcome =
walletConfig =>
makeDependentFixture(build = createNewWallet(Some(walletConfig)),
destroy = destroyWallet)
/** Fixture for an initialized wallet which produce legacy addresses */
def withLegacyWallet(test: OneArgAsyncTest): FutureOutcome = {
val confOverride =
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = legacy")
withNewConfiguredWallet(confOverride)(test)
}
/** Fixture for an initialized wallet which produce segwit addresses */
def withSegwitWallet(test: OneArgAsyncTest): FutureOutcome = {
val confOverride =
ConfigFactory.parseString("bitcoin-s.wallet.defaultAccountType = segwit")
withNewConfiguredWallet(confOverride)(test)
}
def withNewWallet(test: OneArgAsyncTest): FutureOutcome =
makeDependentFixture(build = createNewWallet, destroy = destroyWallet)(test)
makeDependentFixture(build = createDefaultWallet, destroy = destroyWallet)(
test)
case class WalletWithBitcoind(
wallet: UnlockedWalletApi,
@ -96,7 +141,7 @@ trait BitcoinSWalletTest
def withNewWalletAndBitcoind(test: OneArgAsyncTest): FutureOutcome = {
val builder: () => Future[WalletWithBitcoind] = composeBuildersAndWrap(
createNewWallet,
createDefaultWallet,
createWalletWithBitcoind,
(_: UnlockedWalletApi, walletWithBitcoind: WalletWithBitcoind) =>
walletWithBitcoind

View file

@ -48,6 +48,12 @@ object WalletTestUtil {
HDChainType.External,
addressIndex = 0)
/** Sample legacy HD path */
lazy val sampleLegacyPath = LegacyHDPath(hdCoinType,
accountIndex = 0,
HDChainType.Change,
addressIndex = 0)
def freshXpub: ExtPublicKey =
CryptoGenerators.extPublicKey.sample.getOrElse(freshXpub)

View file

@ -62,12 +62,15 @@ sealed abstract class Wallet
.get
.toUTXOSpendingInfo(fromAccount, seed))
logger.info(s"Spending UTXOs: ${utxos
.map { utxo =>
import utxo.outPoint
s"${outPoint.txId.hex}:${outPoint.vout.toInt}"
}
.mkString(", ")}")
logger.info({
val utxosStr = utxos
.map { utxo =>
import utxo.outPoint
s"${outPoint.txId.hex}:${outPoint.vout.toInt}"
}
.mkString(", ")
s"Spending UTXOs: $utxosStr"
})
utxos.zipWithIndex.foreach {
case (utxo, index) =>
@ -100,11 +103,6 @@ sealed abstract class Wallet
// todo: create multiple wallets, need to maintain multiple databases
object Wallet extends CreateWalletApi with BitcoinSLogger {
// The default HD purpose of the bitcoin-s wallet. Can be
// one of segwit, nested segwit or legacy. Hard coded for
// now, could be make configurable in the future
private[wallet] val DEFAULT_HD_PURPOSE: HDPurpose = HDPurposes.SegWit
private case class WalletImpl(
mnemonicCode: MnemonicCode
)(
@ -168,24 +166,33 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
encrypted <- encryptedMnemonicE
} yield {
val wallet = WalletImpl(mnemonic)
val coin =
HDCoin(DEFAULT_HD_PURPOSE, HDUtil.getCoinType(config.network))
val account = HDAccount(coin, 0)
val xpriv = wallet.xprivForPurpose(DEFAULT_HD_PURPOSE)
// safe since we're deriving from a priv
val xpub = xpriv.deriveChildPubKey(account).get
val accountDb = AccountDb(xpub, account)
val mnemonicPath =
WalletStorage.writeMnemonicToDisk(encrypted)
logger.debug(s"Saved encrypted wallet mnemonic to $mnemonicPath")
for {
_ <- config.initialize()
_ <- wallet.accountDAO
.create(accountDb)
.map(_ => logger.trace(s"Saved account to DB"))
_ = {
val mnemonicPath =
WalletStorage.writeMnemonicToDisk(encrypted)
logger.debug(s"Saved encrypted wallet mnemonic to $mnemonicPath")
}
_ <- {
// We want to make sure all level 0 accounts are created,
// so the user can change the default account kind later
// and still have their wallet work
val createAccountFutures =
HDPurposes.all.map(createRootAccount(wallet, _))
val accountCreationF = Future.sequence(createAccountFutures)
accountCreationF.foreach(_ =>
logger.debug(s"Created root level accounts for wallet"))
accountCreationF.failed.foreach { err =>
logger.error(s"Failed to create root level accounts: $err")
}
accountCreationF
}
} yield wallet
}
@ -199,4 +206,26 @@ object Wallet extends CreateWalletApi with BitcoinSLogger {
case Left(err) => err
}
}
/** Creates the level 0 account for the given HD purpose */
private def createRootAccount(wallet: Wallet, purpose: HDPurpose)(
implicit config: WalletAppConfig,
ec: ExecutionContext): Future[AccountDb] = {
val coin =
HDCoin(purpose, HDUtil.getCoinType(config.network))
val account = HDAccount(coin, 0)
val xpriv = wallet.xprivForPurpose(purpose)
// safe since we're deriving from a priv
val xpub = xpriv.deriveChildPubKey(account).get
val accountDb = AccountDb(xpub, account)
logger.debug(s"Creating account with constant prefix $purpose")
wallet.accountDAO
.create(accountDb)
.map { written =>
logger.debug(s"Saved account with constant prefix $purpose to DB")
written
}
}
}

View file

@ -7,14 +7,26 @@ import org.bitcoins.wallet.db.WalletDbManagement
import scala.util.Failure
import scala.util.Success
import java.nio.file.Files
import org.bitcoins.core.hd.HDPurpose
import org.bitcoins.core.hd.HDPurposes
case class WalletAppConfig(conf: Config*) extends AppConfig {
case class WalletAppConfig(private val conf: Config*) extends AppConfig {
override val configOverrides: List[Config] = conf.toList
override def moduleName: String = "wallet"
override type ConfigType = WalletAppConfig
override def newConfigOfType(configs: List[Config]): WalletAppConfig =
override def newConfigOfType(configs: Seq[Config]): WalletAppConfig =
WalletAppConfig(configs: _*)
lazy val defaultAccountKind: HDPurpose =
config.getString("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!")
}
override def initialize()(implicit ec: ExecutionContext): Future[Unit] = {
logger.debug(s"Initializing wallet setup")

View file

@ -18,26 +18,51 @@ import org.bitcoins.core.hd.SegWitHDPath
import org.bitcoins.core.crypto.BIP39Seed
import org.bitcoins.core.util.BitcoinSLogger
import org.bitcoins.core.hd.LegacyHDPath
import org.bitcoins.core.hd.NestedSegWitHDPath
case class SegWitUTOXSpendingInfodb(
/**
* DB representation of a native V0
* SegWit UTXO
*/
case class NativeV0UTXOSpendingInfoDb(
id: Option[Long],
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: SegWitHDPath,
scriptWitness: ScriptWitness
) extends UTXOSpendingInfoDb {
override def redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override val redeemScriptOpt: Option[ScriptPubKey] = None
override val scriptWitnessOpt: Option[ScriptWitness] = Some(scriptWitness)
override type PathType = SegWitHDPath
override def copyWithId(id: Long): SegWitUTOXSpendingInfodb =
override def copyWithId(id: Long): NativeV0UTXOSpendingInfoDb =
copy(id = Some(id))
}
case class LegacyUTXOSpendingInfoDb(
id: Option[Long],
outPoint: TransactionOutPoint,
output: TransactionOutput,
privKeyPath: LegacyHDPath
) extends UTXOSpendingInfoDb {
override val redeemScriptOpt: Option[ScriptPubKey] = None
override def scriptWitnessOpt: Option[ScriptWitness] = None
override type PathType = LegacyHDPath
override def copyWithId(id: Long): LegacyUTXOSpendingInfoDb =
copy(id = Some(id))
}
// TODO add case for nested segwit
// and legacy
/**
* The database level representation of a UTXO.
* When storing a UTXO we don't want to store
* sensitive material such as private keys.
* We instead store the necessary information
* we need to derive the private keys, given
* the root wallet seed.
*/
sealed trait UTXOSpendingInfoDb
extends DbRowAutoInc[UTXOSpendingInfoDb]
with BitcoinSLogger {
@ -55,6 +80,9 @@ sealed trait UTXOSpendingInfoDb
def value: CurrencyUnit = output.value
/** Converts a non-sensitive DB representation of a UTXO into
* a signable (and sensitive) real-world UTXO
*/
def toUTXOSpendingInfo(
account: AccountDb,
walletSeed: BIP39Seed): BitcoinUTXOSpendingInfo = {
@ -114,20 +142,22 @@ case class UTXOSpendingInfoTable(tag: Tag)
outpoint,
output,
path: SegWitHDPath,
None,
None, // ReedemScript
Some(scriptWitness)) =>
SegWitUTOXSpendingInfodb(id, outpoint, output, path, scriptWitness)
.asInstanceOf[UTXOSpendingInfoDb]
NativeV0UTXOSpendingInfoDb(id, outpoint, output, path, scriptWitness)
case (id,
outpoint,
output,
path @ (_: LegacyHDPath | _: NestedSegWitHDPath),
spkOpt,
swOpt) =>
path: LegacyHDPath,
None, // RedeemScript
None // ScriptWitness
) =>
LegacyUTXOSpendingInfoDb(id, outpoint, output, path)
case (id, outpoint, output, path, spkOpt, swOpt) =>
throw new IllegalArgumentException(
"Could not construct UtxoSpendingInfoDb from bad tuple:"
+ s" ($id, $outpoint, $output, $path, $spkOpt, $swOpt) . Note: Only Segwit is implemented")
+ s" ($id, $outpoint, $output, $path, $spkOpt, $swOpt) . Note: Nested Segwit is not implemented")
}