Add BIP44 and BIP32 path support (#379)

* Add BIP44 and BIP32 path support

Make paths from strings, indexes/coins/chains,
BIP32 children types.

Derive xprivs and xpubs from paths.

* Address code review on BIP44 from Chris

* Rename children -> path, bip32child -> bip32node

* update README with bip44 name changes
This commit is contained in:
Torkel Rogstad 2019-03-19 19:09:11 +01:00 committed by Chris Stewart
parent e83b40ea93
commit 06e5b6b450
7 changed files with 519 additions and 39 deletions

View file

@ -1,17 +1,90 @@
package org.bitcoins.core.crypto
import org.bitcoins.core.crypto.ExtKeyVersion._
import org.bitcoins.core.crypto.bip32.BIP32Path
import org.bitcoins.core.number.UInt32
import org.scalatest.{FlatSpec, MustMatchers}
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator}
import org.bitcoins.testkit.util.BitcoinSUnitTest
import scodec.bits.HexStringSyntax
class ExtKeyTest extends FlatSpec with MustMatchers {
import scala.util.{Failure, Success, Try}
class ExtKeyTest extends BitcoinSUnitTest {
override implicit val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "ExtKey"
it must "derive similar keys for UInt32s and primitive numbers" in {
forAll(CryptoGenerators.extPrivateKey, NumberGenerator.uInt32s) {
(priv, index) =>
val derivedPrivUInt32 = priv.deriveChildPrivKey(index)
val derivedPrivPrimitive = priv.deriveChildPrivKey(index.toLong)
assert(Success(derivedPrivUInt32) == derivedPrivPrimitive)
val pub = priv.extPublicKey
val derivedPubUInt32: Try[ExtPublicKey] = pub.deriveChildPubKey(index)
val derivedPubPrimitive: Try[ExtPublicKey] =
pub.deriveChildPubKey(index.toLong)
(derivedPubUInt32, derivedPubPrimitive) match {
case (Success(_), Success(_)) => succeed
case (Failure(exc1), Failure(exc2)) =>
assert(exc1.getMessage == exc2.getMessage)
case _: (Try[ExtPublicKey], Try[ExtPublicKey]) => fail
}
}
}
it must "fail to make a private key out of a public key" in {
forAll(CryptoGenerators.extPublicKey) { pub =>
val attempt = ExtPrivateKey.fromString(pub.toString)
attempt match {
case Success(_) => fail
case Failure(exc) => assert(exc.getMessage.contains("expected private"))
}
}
}
it must "derive private keys from a BIP32 path and an xpriv" in {
forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.bip32Path) {
(priv, path) =>
priv.deriveChildPrivKey(path)
succeed
}
}
it must "derive public keys from a BIP32 path and an xpriv" in {
forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.bip32Path) {
(priv, path) =>
val pub = priv.deriveChildPubKey(path)
pub match {
case Failure(exc) => fail(exc.getMessage)
case Success(_) => succeed
}
}
}
it must "fail to derive public keys from a hardened public key" in {
forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.hardBip32Child) {
(priv, child) =>
val pub = priv.extPublicKey
val derivedPub = pub.deriveChildPubKey(child.toUInt32)
derivedPub match {
case Success(_) => fail
case Failure(exc) => assert(exc.getMessage.contains("hardened"))
}
}
}
//https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vectors
"ExtKey" must "pass the test vectors in BIP32" in {
it must "pass the test vectors in BIP32" in {
//master key
val seedBytes = hex"000102030405060708090a0b0c0d0e0f"
val masterPriv = ExtPrivateKey(MainNetPriv, Some(seedBytes))
val path = BIP32Path.empty
val masterPriv = ExtPrivateKey(MainNetPriv, Some(seedBytes), path)
masterPriv.toString must be(
"xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi")
@ -21,8 +94,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8")
//derive child
val hidx = ExtKey.hardenedIdx
val m0h = masterPriv.deriveChildPrivKey(hidx)
val m0hPath = BIP32Path.fromString("m/0'")
val m0h = masterPriv.deriveChildPrivKey(m0hPath)
m0h.toString must be(
"xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7")
@ -30,7 +103,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m0hPub.toString must be(
"xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw")
val m0h1 = m0h.deriveChildPrivKey(UInt32.one)
val m0h1Path = BIP32Path.fromString("m/0'/1")
val m0h1 = masterPriv.deriveChildPrivKey(m0h1Path)
m0h1.toString must be(
"xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs")
@ -38,7 +112,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m0h1Pub.toString must be(
"xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ")
val m0h12h = m0h1.deriveChildPrivKey(UInt32(2) + ExtKey.hardenedIdx)
val m0h1P2hath = BIP32Path.fromString("m/0'/1/2'")
val m0h12h = masterPriv.deriveChildPrivKey(m0h1P2hath)
m0h12h.toString must be(
"xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM")
@ -46,7 +121,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m0h12hPub.toString must be(
"xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5")
val m0h12h2 = m0h12h.deriveChildPrivKey(UInt32(2))
val m0h12h2Path = BIP32Path.fromString("m/0'/1/2'/2")
val m0h12h2 = masterPriv.deriveChildPrivKey(m0h12h2Path)
m0h12h2.toString must be(
"xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334")
@ -54,7 +130,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m0h12h2Pub.toString must be(
"xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV")
val m0h12h21000000000 = m0h12h2.deriveChildPrivKey(UInt32(1000000000))
val m0h12h21000000000Path = BIP32Path.fromString("m/0'/1/2'/2/1000000000")
val m0h12h21000000000 = masterPriv.deriveChildPrivKey(m0h12h21000000000Path)
m0h12h21000000000.toString must be(
"xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76")
@ -67,7 +144,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
val seedBytes =
hex"fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
val masterPriv = ExtPrivateKey(MainNetPriv, Some(seedBytes))
val masterPriv =
ExtPrivateKey(MainNetPriv, Some(seedBytes), BIP32Path.empty)
masterPriv.toString must be(
"xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U")
@ -75,7 +153,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
masterPub.toString must be(
"xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB")
val m0 = masterPriv.deriveChildPrivKey(UInt32.zero)
val m0Path = BIP32Path.fromString("m/0")
val m0 = masterPriv.deriveChildPrivKey(m0Path)
m0.toString must be(
"xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt")
@ -83,8 +162,9 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m0Pub.toString must be(
"xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH")
val m02147483647hPath = BIP32Path.fromString("m/0/2147483647'")
val m02147483647h =
m0.deriveChildPrivKey(ExtKey.hardenedIdx + UInt32(2147483647))
masterPriv.deriveChildPrivKey(m02147483647hPath)
m02147483647h.toString must be(
"xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9")
@ -92,7 +172,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m02147483647hPub.toString must be(
"xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a")
val m02147483647h1 = m02147483647h.deriveChildPrivKey(UInt32.one)
val m02147483647h1Path = BIP32Path.fromString("m/0/2147483647'/1")
val m02147483647h1 = masterPriv.deriveChildPrivKey(m02147483647h1Path)
m02147483647h1.toString must be(
"xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef")
@ -100,8 +181,10 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m02147483647h1Pub.toString must be(
"xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon")
val m02147483647h12147483646hPath =
BIP32Path.fromString("m/0/2147483647'/1/2147483646'")
val m02147483647h12147483646h =
m02147483647h1.deriveChildPrivKey(ExtKey.hardenedIdx + UInt32(2147483646))
masterPriv.deriveChildPrivKey(m02147483647h12147483646hPath)
m02147483647h12147483646h.toString must be(
"xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc")
@ -109,8 +192,10 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
m02147483647h12147483646hPub.toString must be(
"xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL")
val m02147483647h12147483646h2Path =
BIP32Path.fromString("m/0/2147483647'/1/2147483646'/2")
val m02147483647h12147483646h2 =
m02147483647h12147483646h.deriveChildPrivKey(UInt32(2))
masterPriv.deriveChildPrivKey(m02147483647h12147483646h2Path)
m02147483647h12147483646h2.toString must be(
"xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j")
@ -123,7 +208,8 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
val seedBytes =
hex"4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"
val masterPrivKey = ExtPrivateKey(MainNetPriv, Some(seedBytes))
val masterPrivKey =
ExtPrivateKey(MainNetPriv, Some(seedBytes), BIP32Path.empty)
masterPrivKey.toString must be(
"xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6")
@ -131,7 +217,7 @@ class ExtKeyTest extends FlatSpec with MustMatchers {
masterPubKey.toString must be(
"xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13")
val m0h = masterPrivKey.deriveChildPrivKey(ExtKey.hardenedIdx)
val m0h = masterPrivKey.deriveChildPrivKey(BIP32Path.fromString("m/0'"))
m0h.toString must be(
"xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L")

View file

@ -0,0 +1,125 @@
package org.bitcoins.core.crypto.bip32
import org.bitcoins.core.crypto.ExtKey
import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.testkit.core.gen.{CryptoGenerators, NumberGenerator}
import org.bitcoins.testkit.util.BitcoinSUnitTest
import org.scalacheck.{Gen, Shrink}
import org.scalatest.path
import scala.util.{Success, Try}
class BIP32PathTest extends BitcoinSUnitTest {
override implicit val generatorDrivenConfig: PropertyCheckConfiguration =
generatorDrivenConfigNewCode
behavior of "BIP32Child"
it must "fail to make children of out negative integers" in {
forAll(NumberGenerator.negativeInts, Gen.oneOf(true, false)) { (i, bool) =>
assertThrows[IllegalArgumentException](BIP32Node(i, bool))
}
}
behavior of "BIP32Path"
it must "derive children with the empty path" in {
forAll(CryptoGenerators.extPrivateKey) { priv =>
assert(priv.deriveChildPrivKey(BIP32Path.empty) == priv)
}
}
it must "have varargs and vector constructors what work the same way" in {
forAll(CryptoGenerators.bip32Path) { bip32 =>
assert(BIP32Path(bip32.path) == BIP32Path(bip32.path: _*))
}
}
it must "derive public keys" in {
forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.bip32Path) {
(priv, path) =>
val pub = priv.deriveChildPubKey(path)
// we should always be able to derive pubkeys from privs, even with hard paths
assert(pub.isSuccess)
}
}
it must "derive public and private keys symmetrically" in {
forAll(CryptoGenerators.extPrivateKey, CryptoGenerators.softBip32Path) {
(priv, path) =>
val derivedPubFromPriv: Try[ExtPublicKey] = priv.deriveChildPubKey(path)
val pubFromDerivedPriv: ExtPublicKey =
priv.deriveChildPrivKey(path).extPublicKey
val derivedPubFromPub: Try[ExtPublicKey] =
priv.extPublicKey.deriveChildPubKey(path)
assert(Success(pubFromDerivedPriv) == derivedPubFromPriv)
assert(Success(pubFromDerivedPriv) == derivedPubFromPub)
assert(derivedPubFromPub == derivedPubFromPriv)
}
}
it must "parse the empty path" in {
val fromString = BIP32Path.fromString("m")
assert(fromString == BIP32Path.empty)
}
it must "fail to parse a path beginning with the wrong character" in {
forAll(CryptoGenerators.bip32Path, Gen.alphaChar.suchThat(_ != 'm')) {
(path, char) =>
val badPathString = char + path.toString.drop(1)
assertThrows[IllegalArgumentException](
BIP32Path.fromString(badPathString))
}
}
it must "parse a hardened path" in {
val fromString = BIP32Path.fromString("m/0'")
assert(fromString.path.length == 1)
assert(fromString.path.head.toUInt32 == ExtKey.hardenedIdx)
}
it must "parse the paths from the BIP32 test vectors" in {
val expected1 = BIP32Path(
Vector(BIP32Node(0, hardened = true), BIP32Node(1, hardened = false)))
assert(BIP32Path.fromString("m/0'/1") == expected1)
val expected2 = BIP32Path(
Vector(BIP32Node(0, hardened = true),
BIP32Node(1, hardened = false),
BIP32Node(2, hardened = true)))
assert(BIP32Path.fromString("m/0'/1/2'") == expected2)
val expected3 = BIP32Path(
Vector(BIP32Node(0, hardened = true),
BIP32Node(1, hardened = false),
BIP32Node(2, hardened = true)))
assert(BIP32Path.fromString("m/0'/1/2'") == expected3)
val expected4 = BIP32Path(
Vector(BIP32Node(0, hardened = true),
BIP32Node(1, hardened = false),
BIP32Node(2, hardened = true),
BIP32Node(2, hardened = false)))
assert(BIP32Path.fromString("m/0'/1/2'/2") == expected4)
val expected5 = BIP32Path(
Vector(
BIP32Node(0, hardened = true),
BIP32Node(1, hardened = false),
BIP32Node(2, hardened = true),
BIP32Node(2, hardened = false),
BIP32Node(1000000000, hardened = false)
))
assert(BIP32Path.fromString("m/0'/1/2'/2/1000000000") == expected5)
}
it must "have fromString and toString symmetry" in {
implicit val noShrink: Shrink[Nothing] = Shrink.shrinkAny
forAll(CryptoGenerators.bip32Path) { path =>
val toString = path.toString
assert(path == BIP32Path.fromString(toString))
}
}
}

View file

@ -87,12 +87,24 @@ val bip39Seed = BIP39Seed.fromMnemonic(mnemonicCode,
val xpriv = ExtPrivateKey.fromBIP39Seed(ExtKeyVersion.MainNetPriv, // or testnet/regtest
bip39Seed)
val xpub = xpriv.extPublicKey
// you can now use the generated xpriv to derive further
// private or public keys
```
// this can be done with BIP32 or BIP44 paths:
import bip32._
val bip32Path = BIP32Path(BIP32Node(2, hardened = false), BIP32Node(5, hardened = false))
val derivedPriv = xpriv.deriveChildPrivKey(bip32Path)
val derivedPub = xpub.deriveChildPubKey(bip32Path)
import bip44._
val bip44Path = BIP44Path(coin = BIP44Coin.Bitcoin,
accountIndex = 0,
addressIndex = 1,
chainType = BIP44ChainType.Change)
val derivedBip44Priv = xpriv.deriveChildPrivKey(bip44Path)
```
### Building a signed transaction

View file

@ -1,11 +1,14 @@
package org.bitcoins.core.crypto
import org.bitcoin.NativeSecp256k1
import org.bitcoins.core.crypto.bip32.{BIP32Node, BIP32Path}
import org.bitcoins.core.number.{UInt32, UInt8}
import org.bitcoins.core.protocol.NetworkElement
import org.bitcoins.core.util._
import scodec.bits.ByteVector
import scodec.bits.HexStringSyntax
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/**
@ -42,16 +45,56 @@ sealed abstract class ExtKey extends NetworkElement {
/** The key at this path */
def key: BaseECKey
/**
* Derives the child pubkey at the specified index
*/
def deriveChildPubKey(idx: UInt32): Try[ExtPublicKey] = this match {
case priv: ExtPrivateKey =>
Success(priv.deriveChildPrivKey(idx).extPublicKey)
case pub: ExtPublicKey => pub.deriveChildPubKey(idx)
}
/**
* Derives the child pubkey at the specified index
*/
def deriveChildPubKey(idx: Long): Try[ExtPublicKey] = {
Try(UInt32(idx)).flatMap(deriveChildPubKey)
}
/**
* Derives the child pubkey at the specified index and
* hardening value
*/
def deriveChildPubKey(child: BIP32Node): Try[ExtPublicKey] = {
deriveChildPubKey(child.toUInt32)
}
/**
* Derives the child pubkey at the specified path
*/
def deriveChildPubKey(path: BIP32Path): Try[ExtPublicKey] = {
this match {
case priv: ExtPrivateKey =>
Success(priv.deriveChildPrivKey(path).extPublicKey)
case pub: ExtPublicKey =>
@tailrec
def loop(
remainingPath: List[BIP32Node],
accum: ExtPublicKey): Try[ExtPublicKey] = {
remainingPath match {
case h :: t =>
accum.deriveChildPubKey(h) match {
case Success(derivedPub) => loop(t, derivedPub)
case failure: Failure[_] => failure
}
case Nil => Success(accum)
}
}
loop(path.path.toList, pub)
}
}
override def bytes: ByteVector = key match {
case priv: ECPrivateKey =>
version.bytes ++ depth.bytes ++ fingerprint ++
@ -119,10 +162,23 @@ sealed abstract class ExtPrivateKey extends ExtKey {
override def key: ECPrivateKey
/**
* Derives the child key corresponding to the given path. The given path
* could signify account levels, one sublevel for each currency, or
* how to derive change addresses.
*
* @see [[org.bitcoins.core.crypto.bip44.BIP44Path BIP44Path]] for a more
* specialized version of a BIP32 path
*/
def deriveChildPrivKey(path: BIP32Path): ExtPrivateKey = {
path.path.foldLeft(this)((accum: ExtPrivateKey, curr: BIP32Node) =>
accum.deriveChildPrivKey(curr.toUInt32))
}
def deriveChildPrivKey(idx: UInt32): ExtPrivateKey = {
val data: ByteVector = if (idx >= ExtKey.hardenedIdx) {
//derive hardened key
0.toByte +: (key.bytes ++ idx.bytes)
hex"0" ++ key.bytes ++ idx.bytes
} else {
//derive non hardened key
key.publicKey.bytes ++ idx.bytes
@ -225,7 +281,8 @@ object ExtPrivateKey extends Factory[ExtPrivateKey] {
*/
def apply(
version: ExtKeyVersion,
seedOpt: Option[ByteVector] = None): ExtPrivateKey = {
seedOpt: Option[ByteVector] = None,
path: BIP32Path = BIP32Path.empty): ExtPrivateKey = {
val seed: ByteVector = seedOpt match {
case Some(bytes) => bytes
case None => ECPrivateKey().bytes
@ -236,17 +293,23 @@ object ExtPrivateKey extends Factory[ExtPrivateKey] {
val masterPrivKey = ECPrivateKey(masterPrivBytes)
val chaincode = ChainCode(chaincodeBytes)
val fingerprint = UInt32.zero.bytes
ExtPrivateKey(version,
val root = ExtPrivateKey(version,
depth = UInt8.zero,
fingerprint = fingerprint,
child = UInt32.zero,
chaincode,
masterPrivKey)
path.path.foldLeft(root)((accum, curr) =>
accum.deriveChildPrivKey(curr.toUInt32))
}
/** Generates a extended private key from the provided seed and version */
def fromBIP39Seed(version: ExtKeyVersion, seed: BIP39Seed) =
ExtPrivateKey(version, Some(seed.bytes))
def fromBIP39Seed(
version: ExtKeyVersion,
seed: BIP39Seed,
path: BIP32Path = BIP32Path.empty) =
ExtPrivateKey(version, Some(seed.bytes), path)
}
sealed abstract class ExtPublicKey extends ExtKey {

View file

@ -0,0 +1,87 @@
package org.bitcoins.core.crypto.bip32
import org.bitcoins.core.crypto.ExtKey
import org.bitcoins.core.number.UInt32
abstract class BIP32Path {
def path: Vector[BIP32Node]
override def toString: String =
path
.map {
case BIP32Node(index, hardened) =>
index.toString + (if (hardened) "'" else "")
}
.fold("m")((accum, curr) => accum + "/" + curr)
}
object BIP32Path {
private case class BIP32PathImpl(path: Vector[BIP32Node]) extends BIP32Path
/**
* The empty BIP32 path "m", i.e. a path that does no
* child key derivation
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#the-key-tree BIP44]]
* section on key trees
*/
val empty: BIP32Path = BIP32PathImpl(Vector.empty)
def apply(path: Vector[BIP32Node]): BIP32Path = BIP32PathImpl(path)
def apply(path: BIP32Node*): BIP32Path = BIP32Path(Vector(path: _*))
/**
* Parses a string representation of a BIP32 path. This is on the form
* of
*
* {{{
* m/level/hardenedLevel'/...
* }}}
*
* Where `level` is an integer index and hardenedLevel is an integer
* index followed by a `'`. Different notation is used in BIP32, but this
* is the most common way of writing down BIP32 paths.
*
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki BIP43]]
* and [[https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki BIP44]]
* for examples of this notation.
*/
def fromString(string: String): BIP32Path = {
val parts = string
.split("/")
.toVector
// BIP32 path segments are written both with whitespace between (https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#examples)
// and without (https://wiki.trezor.io/Standard_derivation_paths)
.map(_.trim)
val head +: rest = parts
require(head == "m",
"""The first element in a BIP32 path string must be "m"""")
val path = rest.map { str =>
val (index: String, hardened: Boolean) =
if (str.endsWith("'")) {
(str.dropRight(1), true)
} else {
(str, false)
}
BIP32Node(index.toInt, hardened)
}
BIP32PathImpl(path)
}
}
case class BIP32Node(index: Int, hardened: Boolean) {
require(index >= 0, s"BIP32 node index must be positive! Got $index")
/**
* Converts this node to a BIP32 notation
* unsigned 32 bit integer
*/
def toUInt32: UInt32 =
if (hardened) ExtKey.hardenedIdx + UInt32(index.toLong)
else UInt32(index)
}

View file

@ -1,6 +1,8 @@
package org.bitcoins.testkit.core.gen
import org.bitcoins.core.crypto._
import org.bitcoins.core.crypto.bip32.{BIP32Node, BIP32Path}
import org.bitcoins.core.crypto.bip44._
import org.bitcoins.core.script.crypto.HashType
import org.bitcoins.core.util.CryptoUtil
import org.scalacheck.Gen
@ -223,6 +225,96 @@ sealed abstract class CryptoGenerators {
def extKey: Gen[ExtKey] = Gen.oneOf(extPrivateKey, extPublicKey)
/**
* Generates a BIP 32 path segment
*/
def bip32Child: Gen[BIP32Node] = Gen.oneOf(softBip32Child, hardBip32Child)
/**
* Generates a non-hardened BIP 32 path segment
*/
def softBip32Child: Gen[BIP32Node] =
for {
index <- NumberGenerator.positiveInts
} yield BIP32Node(index, hardened = false)
/**
* Generates a hardened BIP 32 path segment
*/
def hardBip32Child: Gen[BIP32Node] =
for {
soft <- softBip32Child
} yield soft.copy(hardened = true)
/**
* Generates a BIP32 path
*/
def bip32Path: Gen[BIP32Path] =
for {
children <- Gen.listOf(bip32Child)
} yield BIP32Path(children.toVector)
/**
* Generates a non-hardened BIP 32 path
*/
def softBip32Path: Gen[BIP32Path] =
for {
children <- Gen.listOf(softBip32Child)
} yield BIP32Path(children.toVector)
/**
* Generates a valid BIP44 chain type (external/internal change)
*/
def bip44ChainType: Gen[BIP44ChainType] =
Gen.oneOf(BIP44ChainType.Change, BIP44ChainType.External)
/**
* Generates a valid BIP44 chain path
*/
def bip44Chain: Gen[BIP44Chain] =
for {
chainType <- bip44ChainType
account <- bip44Account
} yield BIP44Chain(chainType, account)
/**
* Generates a valid BIP44 coin path
*/
def bip44Coin: Gen[BIP44Coin] =
Gen.oneOf(BIP44Coin.Testnet, BIP44Coin.Bitcoin)
/**
* Generates a valid BIP44 account path
*/
def bip44Account: Gen[BIP44Account] =
for {
coin <- bip44Coin
int <- NumberGenerator.positiveInts
} yield BIP44Account(coin = coin, index = int)
/**
* Generates a valid BIP44 adddress path
*/
def bip44Address: Gen[BIP44Address] =
for {
chain <- bip44Chain
int <- NumberGenerator.positiveInts
} yield BIP44Address(chain, int)
/**
* Generates a valid BIP44 path
*/
def bip44Path: Gen[BIP44Path] =
for {
coin <- bip44Coin
accountIndex <- NumberGenerator.positiveInts
addressIndex <- NumberGenerator.positiveInts
chainType <- bip44ChainType
} yield
BIP44Path(coin = coin,
addressIndex = addressIndex,
accountIndex = accountIndex,
chainType = chainType)
}
object CryptoGenerators extends CryptoGenerators

View file

@ -20,6 +20,21 @@ trait NumberGenerator {
/** Creates a generator that generates positive long numbers */
def positiveLongs: Gen[Long] = Gen.choose(0, Long.MaxValue)
/**
* Integers between 0 and Int.MaxValue
*/
val positiveInts: Gen[Int] = Gen.choose(0, Int.MaxValue)
/**
* Integers between Int.MinValue and -1
*/
val negativeInts: Gen[Int] = Gen.choose(Int.MinValue, -1)
/**
* Random integers
*/
val ints: Gen[Int] = Gen.choose(Int.MinValue, Int.MaxValue)
/** Creates a generator for positive longs without the number zero */
def positiveLongsNoZero: Gen[Long] = Gen.choose(1, Long.MaxValue)