Merge pull request #516 from torkelrogstad/2019-06-12-trezor-test

Generate test vectors from Trezor
This commit is contained in:
Torkel Rogstad 2019-06-25 13:08:49 +02:00 committed by GitHub
commit 09caeb4854
6 changed files with 887 additions and 8 deletions

View file

@ -82,6 +82,20 @@ object JsonSerializers {
TransactionInputWrites TransactionInputWrites
implicit val uInt32Writes: Writes[UInt32] = UInt32Writes implicit val uInt32Writes: Writes[UInt32] = UInt32Writes
implicit val transactionWrites: Writes[Transaction] = TransactionWrites implicit val transactionWrites: Writes[Transaction] = TransactionWrites
implicit val xpubFormat: Format[ExtPublicKey] = new Format[ExtPublicKey] {
override def reads(json: JsValue): JsResult[ExtPublicKey] =
SerializerUtil.processJsStringOpt(ExtPublicKey.fromString(_).toOption)(
json)
override def writes(key: ExtPublicKey): JsValue = JsString(key.toString)
}
implicit val xprivForamt: Format[ExtPrivateKey] = new Format[ExtPrivateKey] {
override def reads(json: JsValue): JsResult[ExtPrivateKey] =
SerializerUtil.processJsStringOpt(ExtPrivateKey.fromString(_).toOption)(
json)
override def writes(key: ExtPrivateKey): JsValue = JsString(key.toString)
}
// Transaction Models // Transaction Models
implicit val rpcScriptPubKeyReads: Reads[RpcScriptPubKey] = implicit val rpcScriptPubKeyReads: Reads[RpcScriptPubKey] =

View file

@ -0,0 +1,399 @@
// the content here was generated from this mnemonic: stage boring net gather radar radio arrest eye ask risk girl country
[
{
"xpub": "xpub6D36zpm3tLPy3dBCpiScEpmmgsivFBcHxX5oXmPBW982BmLiEkjBEDdswxFUoeXpp272QuSpNKZ3f2TdEMkAHyCz1M7P3gFkYJJVEsM66SE",
"coin": "Bitcoin",
"account": 0,
"pathType": "legacy",
"addresses": [
{
"path": "m/44'/0'/0'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "1G1amKQmmsYT7GjRZS8ATBoYFuYQr3f3Kh"
},
{
"path": "m/44'/0'/0'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "1CDaNirLJWHxxT2NT2JDhwYQHVMhQ8gSht"
},
{
"path": "m/44'/0'/0'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "1E33jxV1xT5AQf19kQJJtFGb6roJyqBpgB"
},
{
"path": "m/44'/0'/0'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "1ifTATKPcALmrpkFnJYDMBB7ebmqKNFU3"
},
{
"path": "m/44'/0'/0'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "1A6Dd4XR39r2xcoGahrmGz5uMCQ17QzRCc"
},
{
"path": "m/44'/0'/0'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "1GdP2HfBsrgYv9vrQyjZdvSPSpAxevVc9L"
}
]
},
{
"xpub": "xpub6D36zpm3tLPy6iLv781yHuCGs26Lqkf6pUfK9HZbJXZ8y3iGNodq1ULjdR6qxgsbooxmogteLntjx4AFZPogLwJ7bv3yYAebdx599CByRCz",
"coin": "Bitcoin",
"account": 1,
"pathType": "legacy",
"addresses": [
{
"path": "m/44'/0'/1'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "1GJU8neesqmnqVmeumD437V6mA5UCwQnDi"
},
{
"path": "m/44'/0'/1'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "1G571d8djxUU1pV1zDJVwZX346GsQxrWvY"
},
{
"path": "m/44'/0'/1'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "1LMcqjZMaVUDgYfssKTt1S3bECw8sUiyUH"
},
{
"path": "m/44'/0'/1'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "1HxSpE3SqafLJJyFB287dQZeggPnyS4qZn"
},
{
"path": "m/44'/0'/1'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "18WepEHrUPXBJcqppM4xcX3yv67BJBV34W"
},
{
"path": "m/44'/0'/1'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "1As8dgw5BuNMUuSXEgQDx3CdGv45mKX3fB"
}
]
},
{
"xpub": "xpub6D36zpm3tLPy88RHj2VLg13qTejH9QAMcsRNuJBXx5fsvAVWJFma6HVT4QR8QCUH6TKqa45KwqRRRR7aSCwWd7t53nPz1GNqBg4KxKTM5po",
"coin": "Bitcoin",
"account": 2,
"pathType": "legacy",
"addresses": [
{
"path": "m/44'/0'/2'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "1Di2FwGqvR4hi4RaonDNLLokDV32LZn7QE"
},
{
"path": "m/44'/0'/2'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "18FWkjsm1FDwFJFtJCLiM7c6fioTPL1DhP"
},
{
"path": "m/44'/0'/2'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "1DhbEDmu61Noa7k2nNVudDAM2JLtbXthc7"
},
{
"path": "m/44'/0'/2'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "1FJvUondBDde5r5Dr9E6aMUPNHFhrETBGs"
},
{
"path": "m/44'/0'/2'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "1Ew26qaDcoBbsFTYnZghB44B4LydmM6EAs"
},
{
"path": "m/44'/0'/2'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "16dRQGjd8bBVrXDYUyKFYWv6j1hSRK3jw"
}
]
},
{
"xpub": "zpub6qXZXyMKeDrNXGGaMFqy6VJghvXY4MVyxdoAxja7MBywVLwcvMJa5EUN28sXDn8RmshqMT7DQhzZZ2ituUnPz2bi1ZL2BFb7LzgQPqaHhSP",
"coin": "Bitcoin",
"account": 0,
"pathType": "segwit",
"addresses": [
{
"path": "m/84'/0'/0'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "bc1q2fy4xu2sthp0rn05ru72suwypjeus6ckqha5nn"
},
{
"path": "m/84'/0'/0'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "bc1qckswz80t5y46eduanr3t3t75k7fxg73vj4u44u"
},
{
"path": "m/84'/0'/0'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "bc1qr2v342xx7nfuqkux68z8w38cwhp0dz6kdz6707"
},
{
"path": "m/84'/0'/0'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "bc1qeh4w6gcalmkhr6kwukl204dge9y6rx7eescnym"
},
{
"path": "m/84'/0'/0'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "bc1qz4hc79d2nmhs7evmuxg7lagt4qndpxkg4zhfn5"
},
{
"path": "m/84'/0'/0'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "bc1qwnnplpjgec7yjjh4ckfpm37y29n93edkzumh3x"
}
]
},
{
"xpub": "zpub6qXZXyMKeDrNYd8T9kN7tx27MD5LjLWf8jz9cpAYDuNY84KriyeyKF42nMsFxmPz5VwxZZks8nKNyTsLYtPJWEqYvRpskbCQgS2NSxhzQas",
"coin": "Bitcoin",
"account": 1,
"pathType": "segwit",
"addresses": [
{
"path": "m/84'/0'/1'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "bc1qglsnna835p65y0rw77nytgjeq60lfp8tpq87ax"
},
{
"path": "m/84'/0'/1'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "bc1qe4mln2pde6afge9ku3t4rehqd6aa3sasrmmumx"
},
{
"path": "m/84'/0'/1'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "bc1q30k83vrg6v2kkkywa7h5ug5d8lyuh6ps2hmkvd"
},
{
"path": "m/84'/0'/1'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "bc1qp4s3wefys5gv9zfk3kqyc2cln8wdw493g54mds"
},
{
"path": "m/84'/0'/1'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "bc1qdjjfpe2x3d396tdyhuktkjmnd08fznfzswy6vj"
},
{
"path": "m/84'/0'/1'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "bc1qsd2uwgfkf3sa00mz4s9ye8lenc0d3wr6sy334j"
}
]
},
{
"xpub": "zpub6qXZXyMKeDrNbhzS9E7rFzgACFHYbrCRt4H7nQfHVxYcPY7Ycpv7aAqAMchJC9B2BU6MJoo24t5j5tE9pC36YyVXX9Yab997P7LqnMyouHz",
"coin": "Bitcoin",
"account": 2,
"pathType": "segwit",
"addresses": [
{
"path": "m/84'/0'/2'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "bc1qa924lwqvd8grfskq50m0pz8n38e8a6j396ju79"
},
{
"path": "m/84'/0'/2'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "bc1qkfh4s4la84j37kwrzfhzzk7ssmhmrvu4m8jz0y"
},
{
"path": "m/84'/0'/2'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "bc1qhs2r9n5x0007ja9zy26rer6dx23uypp6gfzzgk"
},
{
"path": "m/84'/0'/2'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "bc1q667d6n68q865afg7xa7arft4fn8arnp05wdhq5"
},
{
"path": "m/84'/0'/2'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "bc1q6ml85j0cfmzdt75h9fevjdj0afuu8398cmm0de"
},
{
"path": "m/84'/0'/2'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "bc1qa9vytfte4wqj924p3rv8tnxa3mpzxftdftcasw"
}
]
},
{
"xpub": "ypub6XXo8YrYWEi6GvfDaXzjzooA7mGdpjAd4bFXrGoixH42GBkCNQUspsRzbL7EfrBokgp76ixB8zv61rd1S9XwU1AtSyy6Nsfo2VS1KLPHgHy",
"coin": "Bitcoin",
"account": 0,
"pathType": "p2sh-segwit",
"addresses": [
{
"path": "m/49'/0'/0'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "3D5QZsM6ChmANAZiqhYDinMd15eSBcHdUy"
},
{
"path": "m/49'/0'/0'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "3FPUdLjhSiPQ6cvsBasmXPppYkp7mMJCKj"
},
{
"path": "m/49'/0'/0'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "3HisVck33jvwdidJRwictPWFmpQxYtXCEW"
},
{
"path": "m/49'/0'/0'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "3DRtzN2DG7NQVd16XHeiAtdwxissYnZoiM"
},
{
"path": "m/49'/0'/0'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "33wUvLyenVSj3G1chYj7SQ29YgUaTTXGXv"
},
{
"path": "m/49'/0'/0'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "3DDrvx2FGrnJo4AfTmjWp69bLYs8b42Geq"
}
]
},
{
"xpub": "ypub6XXo8YrYWEi6LJHpM8SvfQ5JPXum2grFp9WNTZP6isDihG9apqXE3TtrFKv29D8WzzuMcyRFSizRRKGPrDRJtrVDq9Qjxz5RhGnqsjJrSv6",
"coin": "Bitcoin",
"account": 1,
"pathType": "p2sh-segwit",
"addresses": [
{
"path": "m/49'/0'/1'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "3NBCdmG7M9ygPjMVu4SEVvjS84mkXVJrB1"
},
{
"path": "m/49'/0'/1'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "34igwpwzCGioP5w3XpbQu371Pcnn1buyMy"
},
{
"path": "m/49'/0'/1'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "332sSJULbyUxS3iAP8T3HaD4weBvvcBdqX"
},
{
"path": "m/49'/0'/1'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "39nAgGFEkrmjfZvXePTj2DPtvd72d29oXz"
},
{
"path": "m/49'/0'/1'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "3JwL1TpeukwGFTtxYGhzwhRsARdCoV8mVm"
},
{
"path": "m/49'/0'/1'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "32CUGYBfMZ9fmsHzpTADQUuU6mJeKzrezm"
}
]
},
{
"xpub": "ypub6XXo8YrYWEi6P35ZKSLvfrfPRq9J7QC77TTJWZA2JdgPen9Srt9gAzWRtWGzLTPrKy1fd9fDKref5dorMUiXzaJCrwCVBDYkjRMLevE8ym6",
"coin": "Bitcoin",
"account": 2,
"pathType": "p2sh-segwit",
"addresses": [
{
"path": "m/49'/0'/2'/1/0",
"chain": "Change",
"addressIndex": 0,
"address": "3233QX7LkdHwVcAbL9DtcLX6aWUoBF6ucw"
},
{
"path": "m/49'/0'/2'/1/1",
"chain": "Change",
"addressIndex": 1,
"address": "3BgoEtBf2j7QtiHsRYwfHVxEHE85f2Gjw5"
},
{
"path": "m/49'/0'/2'/1/2",
"chain": "Change",
"addressIndex": 2,
"address": "3HgnuyHiqjLsBtwTpFXX7rgZRHDuE7qmS1"
},
{
"path": "m/49'/0'/2'/0/0",
"chain": "External",
"addressIndex": 0,
"address": "3HBcdc89nCTJiqbj48nY8eGmNqbjc6GJbT"
},
{
"path": "m/49'/0'/2'/0/1",
"chain": "External",
"addressIndex": 1,
"address": "3JbAdrcBjxPmzevSVCHJpx9Joaqr1t2KAn"
},
{
"path": "m/49'/0'/2'/0/2",
"chain": "External",
"addressIndex": 2,
"address": "32jnXCPvXXMfmKtruBBD48CnQLQfVbfK6U"
}
]
}
]

View file

@ -0,0 +1,306 @@
package org.bitcoins.wallet
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.bitcoins.core.crypto.MnemonicCode
import scala.io.Source
import play.api.libs.json.JsValue
import play.api.libs.json.Json
import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.hd.HDCoinType
import org.bitcoins.core.hd.HDPurpose
import org.bitcoins.core.hd.HDPath
import org.bitcoins.core.hd.HDChain
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.rpc.serializers.JsonSerializers._
import play.api.libs.json.Reads
import play.api.libs.json.JsResult
import org.bitcoins.rpc.serializers.SerializerUtil
import play.api.libs.json.JsError
import play.api.libs.json.JsSuccess
import org.bitcoins.core.hd.HDCoin
import org.bitcoins.core.hd.HDChainType
import org.bitcoins.core.hd.HDPurposes
import org.bitcoins.wallet.config.WalletAppConfig
import org.bitcoins.testkit.BitcoinSAppConfig
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import akka.actor.ActorSystem
import scala.concurrent.Future
import org.bitcoins.wallet.api.InitializeWalletSuccess
import org.scalatest.AsyncFlatSpec
import org.bitcoins.testkit.wallet.BitcoinSWalletTest
import org.scalatest.FutureOutcome
import org.bitcoins.testkit.fixtures.EmptyFixture
import org.bitcoins.core.util.FutureUtil
import org.bitcoins.core.hd.HDChainType.Change
import org.bitcoins.core.hd.HDChainType.External
import org.bitcoins.wallet.models.AddressDb
import org.bitcoins.wallet.models.AccountDb
import _root_.akka.actor.Address
import org.scalatest.compatible.Assertion
import scala.concurrent.ExecutionContext
class TrezorAddressTest extends BitcoinSWalletTest with EmptyFixture {
val mnemonic = MnemonicCode.fromWords(
Vector(
"stage",
"boring",
"net",
"gather",
"radar",
"radio",
"arrest",
"eye",
"ask",
"risk",
"girl",
"country"
)
)
lazy val json: JsValue = {
val stream = {
val classLoader = getClass().getClassLoader()
classLoader.getResourceAsStream("trezor-addresses.json")
}
val rawText = Source
.fromInputStream(stream)
.getLines
.drop(1) // first line is a comment
.mkString
Json.parse(rawText)
}
implicit val hdpathReads = new Reads[HDPath] {
override def reads(json: JsValue): JsResult[HDPath] =
json
.validate[String]
.flatMap(HDPath.fromString(_) match {
case None => JsError(s"Could not read $json")
case Some(value) => JsSuccess(value)
})
}
implicit val hdcoinReads = new Reads[HDCoinType] {
override def reads(json: JsValue): JsResult[HDCoinType] =
json.validate[String].map(_.toLowerCase).map {
case "bitcoin" => HDCoinType.Bitcoin
case "testnet" => HDCoinType.Testnet
}
}
implicit val hdpurposeReads = new Reads[HDPurpose] {
override def reads(json: JsValue): JsResult[HDPurpose] =
json.validate[String].map {
case "legacy" => HDPurposes.Legacy
case "segwit" => HDPurposes.SegWit
case "p2sh-segwit" => HDPurposes.NestedSegWit
}
}
implicit val hdchainType = new Reads[HDChainType] {
override def reads(json: JsValue): JsResult[HDChainType] =
json.validate[String].map(_.toLowerCase).map {
case "change" => HDChainType.Change
case "external" => HDChainType.External
}
}
private case class TestAddress(
path: HDPath,
chain: HDChainType,
addressIndex: Int,
address: BitcoinAddress)
private object TestAddress {
implicit val reads = Json.reads[TestAddress]
}
private case class TestVector(
xpub: ExtPublicKey,
coin: HDCoinType,
account: Int,
pathType: HDPurpose,
addresses: Vector[TestAddress])
private object TestVector {
implicit val reads = Json.reads[TestVector]
}
private lazy val vectors = json.validate[Vector[TestVector]] match {
case JsError(errors) => fail(errors.head.toString)
case JsSuccess(value, _) => value
}
private lazy val legacyVectors =
vectors.filter(_.pathType == HDPurposes.Legacy)
private lazy val segwitVectors =
vectors.filter(_.pathType == HDPurposes.SegWit)
private lazy val nestedVectors =
vectors.filter(_.pathType == HDPurposes.NestedSegWit)
private def configForPurpose(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 confStr = s"""bitcoin-s.wallet.defaultAccountType = $purposeStr
|bitcoin-s.network = mainnet""".stripMargin
ConfigFactory.parseString(confStr)
}
private def getWallet(config: WalletAppConfig): Future[Wallet] =
Wallet
.initializeWithMnemonic(mnemonic)(
config, // to make sure we're not passing in the wrong conf by accident
implicitly[ExecutionContext])
.map {
case InitializeWalletSuccess(wallet: Wallet) =>
wallet
case err => fail(s"didn't get wallet: $err")
}
private case class AccountAndAddrsAndVector(
account: AccountDb,
addrs: Seq[AddressDb],
vector: TestVector)
/** Asserts that the given addresses are gthe same as in the given vector */
private def assertSameAddresses(
addrs: Seq[AddressDb],
vector: TestVector): Seq[Assertion] = {
assert(vector.addresses.length == addrs.length)
val sortedAddresses = addrs.sortBy(_.path.toString)
val sortedVectors = vector.addresses.sortBy(_.path.toString)
sortedAddresses
.zip(sortedVectors)
.map {
case (foundAddress, expectedAddress) =>
assert(foundAddress.address == expectedAddress.address)
}
}
private def testAccountType(purpose: HDPurpose): Future[Assertion] = {
val confOverride = configForPurpose(purpose)
implicit val conf: WalletAppConfig =
BitcoinSAppConfig.getTestConfig(confOverride)
val vectors = purpose match {
case HDPurposes.Legacy => legacyVectors
case HDPurposes.SegWit => segwitVectors
case HDPurposes.NestedSegWit => nestedVectors
case other => fail(s"unknown purpose: $other")
}
/** Creates the wallet accounts needed for this test */
def createNeededAccounts(
wallet: Wallet,
existing: Vector[AccountDb]): Future[Unit] = {
val accountsToCreate = existing.length until vectors.length
FutureUtil
.sequentially(accountsToCreate) { _ =>
wallet.createNewAccount(purpose)
}
.map(_ => ())
}
/**
* Iterates over the given list of accounts and test vectors, and
* fetches all the
* addresses needed to verify the test vector
*/
def getAcccountsWithAddressesAndVectors(
wallet: Wallet,
accountsWithVectors: Seq[(AccountDb, TestVector)]): Future[
Seq[AccountAndAddrsAndVector]] =
FutureUtil.sequentially(accountsWithVectors) {
case (acc, vec) =>
val addrFutures: Future[Seq[AddressDb]] =
FutureUtil.sequentially(vec.addresses) { vector =>
val addrFut = vector.chain match {
case Change => wallet.getNewChangeAddress(acc)
case External =>
wallet.getNewAddress(acc)
}
addrFut.flatMap(wallet.addressDAO.findAddress).map {
case Some(addr) => addr
case None =>
fail(s"Did not find address we just generated in DAO!")
}
}
addrFutures.map(AccountAndAddrsAndVector(acc, _, vec))
}
for {
wallet <- getWallet(conf)
existingAccounts <- wallet.listAccounts(purpose)
_ <- createNeededAccounts(wallet, existingAccounts)
accounts <- wallet.listAccounts(purpose)
// we want to find all accounts for the given account type,
// and match it with its corresponding test vector
accountsWithVectors = {
assert(accounts.length == vectors.length)
val accountsWithVectors = vectors.map { vec =>
accounts.find(_.hdAccount.index == vec.account) match {
case None =>
fail(
s"Did not find account in wallet with index ${vec.account}. Accounts: ${accounts.mkString}")
case Some(account) =>
assert(account.xpub == vec.xpub)
account -> vec
}
}
accountsWithVectors
}
// here we generate addresses matching the ones found
// in the accompanying test vector for each account
// at the end we group them all together
accountsWithAddrsWithVecs <- getAcccountsWithAddressesAndVectors(
wallet,
accountsWithVectors)
} yield {
// lastly we loop over all accounts, addresses and vectors
// and verify that they are all the same
val assertions: Seq[Assertion] = {
val nestedAssertions: Seq[Seq[Assertion]] =
accountsWithAddrsWithVecs.map {
case AccountAndAddrsAndVector(account, addresses, vec) =>
val acctIdx = account.hdAccount.index
val vec = vectors.find(_.xpub == account.xpub) match {
case None =>
fail(s"Did not find test vector for account $acctIdx")
case Some(v) => v
}
assertSameAddresses(addresses, vec)
}
nestedAssertions.flatten
}
assert(assertions.forall(_.isCompleted))
}
}
it must "act the same way as Trezor for legacy accounts" in { _ =>
testAccountType(HDPurposes.Legacy)
}
it must "act the same way as Trezor for segwit accounts" in { _ =>
testAccountType(HDPurposes.SegWit)
}
// TODO: implement this when nested segwit addresses are implemented
// in the wallet
it must "act the same way as Trezor for nested segwit accounts" ignore { _ =>
testAccountType(HDPurposes.NestedSegWit)
}
}

View file

@ -0,0 +1,104 @@
package org.bitcoins.wallet.util
import org.bitcoins.core.hd._
import play.api.libs.json._
import scala.sys.process._
/** This program connects to a running Trezor, and gets
* xpubs and addresses from legacy, segwit and nested
* segwit accounts for mainnet. The intention here was
* to also do this for testnet, but for some reason
* my emulator refuses to work when specifying a
* testnet BIP32 path. Something to look into in the
* future.
*
* To replicate this:
* 1) Boot up a Trezor emulator (https://github.com/trezor/trezor-firmware/blob/master/core/docs/emulator.md)
* 2) Run Trezor bridge (instructions in link above)
* 3) Go to trezor.io/start, restore wallet from seed
* 4) Leave emulator open
* 5) Run this program. Result gets printed to stdout.
*/
object GetAddresses extends App {
import scala.language.implicitConversions
implicit def string2Json(str: String): JsString = JsString(str)
implicit def int2Json(int: Int): JsNumber = JsNumber(int)
implicit def seq2Json[T](xs: Seq[T])(implicit conv: T => JsValue): JsArray =
JsArray(xs.map(conv))
def printerr(x: Any): Unit = System.err.println(x.toString())
val accountInfo = for {
constant <- HDPurposes.all
coin <- List(HDCoinType.Bitcoin /*, HDCoinType.Testnet*/ )
accountIndex <- 0 until 3
} yield {
val accountPath = BIP32Path(
BIP32Node(constant.constant, hardened = true),
BIP32Node(coin.toInt, hardened = true),
BIP32Node(accountIndex, hardened = true)
)
val pathType =
constant match {
case HDPurposes.Legacy => "legacy"
case HDPurposes.NestedSegWit => "p2sh-segwit"
case HDPurposes.SegWit => "segwit"
case other => throw new RuntimeException(s"Unexpected purpose $other")
}
val trezorPathType =
constant match {
case HDPurposes.Legacy => "address"
case HDPurposes.NestedSegWit => "p2shsegwit"
case HDPurposes.SegWit => "segwit"
case other => throw new RuntimeException(s"Unexpected purpose $other")
}
val xpubCmd =
s"""trezorctl get-public-node -n $accountPath -t $trezorPathType"""
printerr(s"Executing cmd: $xpubCmd")
val xpub = xpubCmd.!!.split("\n").last.split(": ").last
val addresses = for {
chainType <- List[HDChainType](HDChainType.Change, HDChainType.External)
addressIndex <- 0 until 3
} yield {
val path = BIP32Path(
BIP32Node(constant.constant, hardened = true),
BIP32Node(coin.toInt, hardened = true),
BIP32Node(accountIndex, hardened = true),
BIP32Node(chainType.index, hardened = false),
BIP32Node(addressIndex, hardened = false)
)
val addressCmd = s"trezorctl get-address -n $path -t $trezorPathType"
printerr(s"Executing cmd: $addressCmd")
val address = addressCmd.!!.split("\n").head
val json = Json.toJson(
Map[String, JsValue](
"path" -> path.toString,
"chain" -> chainType.toString,
"addressIndex" -> addressIndex,
"address" -> address
)
)
json
}
val json = JsObject(
Map[String, JsValue](
"coin" -> coin.toString,
"pathType" -> pathType,
"account" -> accountIndex,
"xpub" -> xpub,
"addresses" -> addresses
)
)
json
}
println(Json.stringify(JsArray(accountInfo)))
}

View file

@ -98,6 +98,48 @@ sealed abstract class Wallet
} }
} }
override def createNewAccount(): Future[WalletApi] =
createNewAccount(DEFAULT_HD_COIN.purpose)
// todo: check if there's addresses in the most recent
// account before creating new
override def createNewAccount(purpose: HDPurpose): Future[Wallet] = {
accountDAO
.findAll()
.map(_.filter(_.hdAccount.purpose == purpose))
.map(_.sortBy(_.hdAccount.index))
// we want to the most recently created account,
// to know what the index of our new account
// should be.
.map(_.lastOption)
.flatMap { mostRecentOpt =>
val accountIndex = mostRecentOpt match {
case None => 0 // no accounts present in wallet
case Some(account) => account.hdAccount.index + 1
}
logger.info(
s"Creating new account at index $accountIndex for purpose $purpose")
val newAccount = HDAccount(DEFAULT_HD_COIN, accountIndex)
val xpub = {
val xpriv = xprivForPurpose(newAccount.purpose)
xpriv.deriveChildPubKey(newAccount) match {
case Failure(exception) =>
// this won't happen, because we're deriving from a privkey
// this should really be changed in the method signature
logger.error(s"Unexpected error when deriving xpub: $exception")
throw exception
case Success(xpub) => xpub
}
}
val newAccountDb = AccountDb(xpub, newAccount)
val accountCreationF = accountDAO.create(newAccountDb)
accountCreationF.map(created =>
logger.debug(s"Created new account ${created.hdAccount}"))
accountCreationF
}
.map(_ => this)
}
} }
// todo: create multiple wallets, need to maintain multiple databases // todo: create multiple wallets, need to maintain multiple databases

View file

@ -122,14 +122,9 @@ trait LockedWalletApi extends WalletApi {
def listAccounts(): Future[Vector[AccountDb]] def listAccounts(): Future[Vector[AccountDb]]
/** /** Lists all wallet accounts with the given type */
* Tries to create a new accoun in this wallet. Fails if the def listAccounts(purpose: HDPurpose): Future[Vector[AccountDb]] =
* most recent account has no transaction history, as per listAccounts().map(_.filter(_.hdAccount.purpose == purpose))
* BIP44
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
*/
// def createNewAccount: Future[Try[WalletApi]]
} }
@ -186,4 +181,23 @@ trait UnlockedWalletApi extends LockedWalletApi {
} yield tx } yield tx
} }
/**
* Tries to create a new account in this wallet. Fails if the
* most recent account has no transaction history, as per
* BIP44
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
*/
def createNewAccount(purpose: HDPurpose): Future[WalletApi]
/**
* Tries to create a new account in this wallet for the default
* account type. Fails if the
* most recent account has no transaction history, as per
* BIP44
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account BIP44 account section]]
*/
def createNewAccount(): Future[WalletApi]
} }