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

@ -56,12 +56,12 @@ This gives us an example of a hex encoded Bitcoin transaction that is deserializ
#### Generating a BIP39 mnemonic phrase and an `xpriv`
BIP39 mnemonic phrases are the most common way of creating backups of wallets.
They are between 12 and 24 words the user writes down, and can later be used to restore
their bitcoins. From the mnemonic phrase we generate a wallet seed, and that seed
can be used to generate what's called an extended private key
They are between 12 and 24 words the user writes down, and can later be used to restore
their bitcoins. From the mnemonic phrase we generate a wallet seed, and that seed
can be used to generate what's called an extended private key
([`ExtPrivateKey`](src/main/scala/org/bitcoins/core/crypto/ExtKey.scala) in Bitcoin-S).
Here's an example:
Here's an example:
```scala
import scodec.bits._
@ -77,22 +77,34 @@ val mnemonicCode = MnemonicCode.fromEntropy(entropy)
mnemonicCode.words // the phrase the user should write down
// the password argument is an optional, extra security
// measure. all MnemonicCode instances will give you a
// valid BIP39 seed, but different passwords will give
// measure. all MnemonicCode instances will give you a
// valid BIP39 seed, but different passwords will give
// you different seeds. So you could have as many wallets
// from the same seed as you'd like, by simply giving them
// different passwords.
val bip39Seed = BIP39Seed.fromMnemonic(mnemonicCode,
// different passwords.
val bip39Seed = BIP39Seed.fromMnemonic(mnemonicCode,
password = "secret password")
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,
depth = UInt8.zero,
fingerprint = fingerprint,
child = UInt32.zero,
chaincode,
masterPrivKey)
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)