mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
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:
parent
e83b40ea93
commit
06e5b6b450
7 changed files with 519 additions and 39 deletions
|
@ -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")
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue