diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeyTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeyTest.scala index 0b649dd9d5..5f2263e0bf 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeyTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/ExtKeyTest.scala @@ -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") diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/bip32/BIP32PathTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/bip32/BIP32PathTest.scala new file mode 100644 index 0000000000..2719dd6730 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/bip32/BIP32PathTest.scala @@ -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)) + } + } +} diff --git a/core/README.md b/core/README.md index 289be3b3ed..04f2fd862e 100644 --- a/core/README.md +++ b/core/README.md @@ -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 diff --git a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala index 67955dcaf9..31a7aafa23 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/ExtKey.scala @@ -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 { diff --git a/core/src/main/scala/org/bitcoins/core/crypto/bip32/BIP32Path.scala b/core/src/main/scala/org/bitcoins/core/crypto/bip32/BIP32Path.scala new file mode 100644 index 0000000000..e481b11590 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/crypto/bip32/BIP32Path.scala @@ -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) +} diff --git a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala index dd1979f3c0..ceeebc9a4c 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala @@ -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 diff --git a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/NumberGenerator.scala b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/NumberGenerator.scala index 21fa782177..8e26de6cc0 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/NumberGenerator.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/NumberGenerator.scala @@ -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)