diff --git a/hdkeychain/README.md b/hdkeychain/README.md new file mode 100644 index 0000000..52a4a90 --- /dev/null +++ b/hdkeychain/README.md @@ -0,0 +1,74 @@ +hdkeychain +========== + +[![Build Status](https://travis-ci.org/conformal/btcutil.png?branch=master)] +(https://travis-ci.org/conformal/btcutil) + +Package hdkeychain provides an API for bitcoin hierarchical deterministic +extended keys (BIP0032). + +A comprehensive suite of tests is provided to ensure proper functionality. See +`test_coverage.txt` for the gocov coverage report. Alternatively, if you are +running a POSIX OS, you can run the `cov_report.sh` script for a real-time +report. Package hdkeychain is licensed under the liberal ISC license. + +## Feature Overview + +- Full BIP0032 implementation +- Single type for private and public extended keys +- Convenient cryptograpically secure seed generation +- Simple creation of master nodes +- Support for multi-layer derivation +- Easy serialization and deserialization for both private and public extended + keys +- Support for custom networks by registering them with btcnet +- Obtaining the underlying EC pubkeys, EC privkeys, and associated bitcoin + addresses ties in seamlessly with existing btcec and btcutil types which + provide powerful tools for working with them to do things like sign + transations and generate payment scripts +- Uses the btcec package which is highly optimized for secp256k1 +- Code examples including: + - Generating a cryptographically secure random seed and deriving a + master node from it + - Default HD wallet layout as described by BIP0032 + - Audits use case as described by BIP0032 +- Comprehensive test coverage including the BIP0032 test vectors +- Benchmarks + +## Documentation + +[![GoDoc](https://godoc.org/github.com/conformal/btcutil/hdkeychain?status.png)] +(http://godoc.org/github.com/conformal/btcutil/hdkeychain) + +Full `go doc` style documentation for the project can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/conformal/btcutil/hdkeychain + +You can also view the documentation locally once the package is installed with +the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to +http://localhost:6060/pkg/github.com/conformal/btcutil/hdkeychain + +## Installation + +```bash +$ go get github.com/conformal/btcutil/hdkeychain +``` + +## Examples + +* [NewMaster Example] + (http://godoc.org/github.com/conformal/btcutil/hdkeychain#example-NewMaster) + Demonstrates how to generate a cryptographically random seed then use it to + create a new master node (extended key). +* [Default Wallet Layout Example] + (http://godoc.org/github.com/conformal/btcutil/hdkeychain#example--defaultWalletLayout) + Demonstrates the default hierarchical deterministic wallet layout as described + in BIP0032. +* [Audits Use Case Example] + (http://godoc.org/github.com/conformal/btcutil/hdkeychain#example--audits) + Demonstrates the audits use case in BIP0032. + +## License + +Package hdkeychain is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/hdkeychain/bench_test.go b/hdkeychain/bench_test.go new file mode 100644 index 0000000..521818a --- /dev/null +++ b/hdkeychain/bench_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package hdkeychain_test + +import ( + "testing" + + "github.com/conformal/btcutil/hdkeychain" +) + +// bip0032MasterPriv1 is the master private extended key from the first set of +// test vectors in BIP0032. +const bip0032MasterPriv1 = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbP" + + "y6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + +// BenchmarkDeriveHardened benchmarks how long it takes to derive a hardened +// child from a master private extended key. +func BenchmarkDeriveHardened(b *testing.B) { + b.StopTimer() + masterKey, err := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if err != nil { + b.Errorf("Failed to decode master seed: %v", err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + masterKey.Child(hdkeychain.HardenedKeyStart) + } +} + +// BenchmarkDeriveNormal benchmarks how long it takes to derive a normal +// (non-hardened) child from a master private extended key. +func BenchmarkDeriveNormal(b *testing.B) { + b.StopTimer() + masterKey, err := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if err != nil { + b.Errorf("Failed to decode master seed: %v", err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + masterKey.Child(0) + } +} + +// BenchmarkPrivToPub benchmarks how long it takes to convert a private extended +// key to a public extended key. +func BenchmarkPrivToPub(b *testing.B) { + b.StopTimer() + masterKey, err := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if err != nil { + b.Errorf("Failed to decode master seed: %v", err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + masterKey.Neuter() + } +} + +// BenchmarkDeserialize benchmarks how long it takes to deserialize a private +// extended key. +func BenchmarkDeserialize(b *testing.B) { + for i := 0; i < b.N; i++ { + hdkeychain.NewKeyFromString(bip0032MasterPriv1) + } +} + +// BenchmarkSerialize benchmarks how long it takes to serialize a private +// extended key. +func BenchmarkSerialize(b *testing.B) { + b.StopTimer() + masterKey, err := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if err != nil { + b.Errorf("Failed to decode master seed: %v", err) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + masterKey.String() + } +} diff --git a/hdkeychain/cov_report.sh b/hdkeychain/cov_report.sh new file mode 100644 index 0000000..307f05b --- /dev/null +++ b/hdkeychain/cov_report.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# This script uses gocov to generate a test coverage report. +# The gocov tool my be obtained with the following command: +# go get github.com/axw/gocov/gocov +# +# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH. + +# Check for gocov. +type gocov >/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo >&2 "This script requires the gocov tool." + echo >&2 "You may obtain it with the following command:" + echo >&2 "go get github.com/axw/gocov/gocov" + exit 1 +fi +gocov test | gocov report diff --git a/hdkeychain/doc.go b/hdkeychain/doc.go new file mode 100644 index 0000000..d9c83c2 --- /dev/null +++ b/hdkeychain/doc.go @@ -0,0 +1,84 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +/* +Package hdkeychain provides an API for bitcoin hierarchical deterministic +extended keys (BIP0032). + +Overview + +The ability to implement hierarchical deterministic wallets depends on the +ability to create and derive hierarchical deterministic extended keys. + +At a high level, this package provides support for those hierarchical +deterministic extended keys by providing an ExtendedKey type and supporting +functions. Each extended key can either be a private or public extended key +which itself is capable of deriving a child extended key. + +Determining the Extended Key Type + +Whether an extended key is a private or public extended key can be determined +with the IsPrivate function. + +Transaction Signing Keys and Payment Addresses + +In order to create and sign transactions, or provide others with addresses to +send funds to, the underlying key and address material must be accessible. This +package provides the ECPubKey, ECPrivKey, and Address functions for this +purpose. + +The Master Node + +As previously mentioned, the extended keys are hierarchical meaning they are +used to form a tree. The root of that tree is called the master node and this +package provides the NewMaster function to create it from a cryptographically +random seed. The GenerateSeed function is provided as a convenient way to +create a random seed for use with the NewMaster function. + +Deriving Children + +Once you have created a tree root (or have deserialized an extended key as +discussed later), the child extended keys can be derived by using the Child +function. The Child function supports deriving both normal (non-hardened) and +hardened child extended keys. In order to derive a hardened extended key, use +the HardenedKeyStart constant + the hardened key number as the index to the +Child function. This provides the ability to cascade the keys into a tree and +hence generate the hierarchical deterministic key chains. + +Normal vs Hardened Child Extended Keys + +A private extended key can be used to derive both hardened and non-hardened +(normal) child private and public extended keys. A public extended key can only +be used to derive non-hardened child public extended keys. As enumerated in +BIP0032 "knowledge of the extended public key plus any non-hardened private key +descending from it is equivalent to knowing the extended private key (and thus +every private and public key descending from it). This means that extended +public keys must be treated more carefully than regular public keys. It is also +the reason for the existence of hardened keys, and why they are used for the +account level in the tree. This way, a leak of an account-specific (or below) +private key never risks compromising the master or other accounts." + +Neutering a Private Extended Key + +A private extended key can be converted to a new instance of the corresponding +public extended key with the Neuter function. The original extended key is not +modified. A public extended key is still capable of deriving non-hardened child +public extended keys. + +Serializing and Deserializing Extended Keys + +Extended keys are serialized and deserialized with the String and +NewKeyFromString functions. The serialized key is a Base58-encoded string which +looks like the following: + public key: xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw + private key: xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7 + +Network + +Extended keys are much like normal Bitcoin addresses in that they have version +bytes which tie them to a specific network. The SetNet and IsForNet functions +are provided to set and determinine which network an extended key is associated +with. +*/ +package hdkeychain diff --git a/hdkeychain/example_test.go b/hdkeychain/example_test.go new file mode 100644 index 0000000..44c27af --- /dev/null +++ b/hdkeychain/example_test.go @@ -0,0 +1,182 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package hdkeychain_test + +import ( + "fmt" + + "github.com/conformal/btcnet" + "github.com/conformal/btcutil/hdkeychain" +) + +// This example demonstrates how to generate a cryptographically random seed +// then use it to create a new master node (extended key). +func ExampleNewMaster() { + // Generate a random seed at the recommended length. + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + fmt.Println(err) + return + } + + // Generate a new master node using the seed. + key, err := hdkeychain.NewMaster(seed) + if err != nil { + fmt.Println(err) + return + } + + // Show that the generated master node extended key is private. + fmt.Println("Private Extended Key?:", key.IsPrivate()) + + // Output: + // Private Extended Key?: true +} + +// This example demonstrates the default hierarchical deterministic wallet +// layout as described in BIP0032. +func Example_defaultWalletLayout() { + // The default wallet layout described in BIP0032 is: + // + // Each account is composed of two keypair chains: an internal and an + // external one. The external keychain is used to generate new public + // addresses, while the internal keychain is used for all other + // operations (change addresses, generation addresses, ..., anything + // that doesn't need to be communicated). + // + // * m/iH/0/k + // corresponds to the k'th keypair of the external chain of account + // number i of the HDW derived from master m. + // * m/iH/1/k + // corresponds to the k'th keypair of the internal chain of account + // number i of the HDW derived from master m. + + // Ordinarily this would either be read from some encrypted source + // and be decrypted or generated as the NewMaster example shows, but + // for the purposes of this example, the private exteded key for the + // master node is being hard coded here. + master := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jP" + + "PqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + + // Start by getting an extended key instance for the master node. + // This gives the path: + // m + masterKey, err := hdkeychain.NewKeyFromString(master) + if err != nil { + fmt.Println(err) + return + } + + // Derive the extended key for account 0. This gives the path: + // m/0H + acct0, err := masterKey.Child(hdkeychain.HardenedKeyStart + 0) + if err != nil { + fmt.Println(err) + return + } + + // Derive the extended key for the account 0 external chain. This + // gives the path: + // m/0H/0 + acct0Ext, err := acct0.Child(0) + if err != nil { + fmt.Println(err) + return + } + + // Derive the extended key for the account 0 internal chain. This gives + // the path: + // m/0H/1 + acct0Int, err := acct0.Child(1) + if err != nil { + fmt.Println(err) + return + } + + // At this point, acct0Ext and acct0Int are ready to derive the keys for + // the external and internal wallet chains. + + // Derive the 10th extended key for the account 0 external chain. This + // gives the path: + // m/0H/0/10 + acct0Ext10, err := acct0Ext.Child(10) + if err != nil { + fmt.Println(err) + return + } + + // Derive the 1st extended key for the account 0 internal chain. This + // gives the path: + // m/0H/1/0 + acct0Int0, err := acct0Int.Child(0) + if err != nil { + fmt.Println(err) + return + } + + // Get and show the address associated with the extended keys for the + // main bitcoin network. + acct0ExtAddr, err := acct0Ext10.Address(&btcnet.MainNetParams) + if err != nil { + fmt.Println(err) + return + } + acct0IntAddr, err := acct0Int0.Address(&btcnet.MainNetParams) + if err != nil { + fmt.Println(err) + return + } + fmt.Println("Account 0 External Address 10:", acct0ExtAddr) + fmt.Println("Account 0 Internal Address 0:", acct0IntAddr) + + // Output: + // Account 0 External Address 10: 1HVccubUT8iKTapMJ5AnNA4sLRN27xzQ4F + // Account 0 Internal Address 0: 1J5rebbkQaunJTUoNVREDbeB49DqMNFFXk +} + +// This example demonstrates the audits use case in BIP0032. +func Example_audits() { + // The audits use case described in BIP0032 is: + // + // In case an auditor needs full access to the list of incoming and + // outgoing payments, one can share all account public extended keys. + // This will allow the auditor to see all transactions from and to the + // wallet, in all accounts, but not a single secret key. + // + // * N(m/*) + // corresponds to the neutered master extended key (also called + // the master public extended key) + + // Ordinarily this would either be read from some encrypted source + // and be decrypted or generated as the NewMaster example shows, but + // for the purposes of this example, the private exteded key for the + // master node is being hard coded here. + master := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jP" + + "PqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + + // Start by getting an extended key instance for the master node. + // This gives the path: + // m + masterKey, err := hdkeychain.NewKeyFromString(master) + if err != nil { + fmt.Println(err) + return + } + + // Neuter the master key to generate a master public extended key. This + // gives the path: + // N(m/*) + masterPubKey, err := masterKey.Neuter() + if err != nil { + fmt.Println(err) + return + } + + // Share the master public extended key with the auditor. + fmt.Println("Audit key N(m/*):", masterPubKey) + + // Output: + // Audit key N(m/*): xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8 +} diff --git a/hdkeychain/extendedkey.go b/hdkeychain/extendedkey.go new file mode 100644 index 0000000..648ee3e --- /dev/null +++ b/hdkeychain/extendedkey.go @@ -0,0 +1,515 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package hdkeychain + +// References: +// [BIP32]: BIP0032 - Hierarchical Deterministic Wallets +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/conformal/btcec" + "github.com/conformal/btcnet" + "github.com/conformal/btcutil" + "github.com/conformal/btcwire" +) + +const ( + // RecommendedSeedLen is the recommended length in bytes for a seed + // to a master node. + RecommendedSeedLen = 32 // 256 bits + + // HardenedKeyStart is the index at which a hardended key starts. Each + // extended key has 2^31 normal child keys and 2^31 hardned child keys. + // Thus the range for normal child keys is [0, 2^31 - 1] and the range + // for hardened child keys is [2^31, 2^32 - 1]. + HardenedKeyStart = 0x80000000 // 2^31 + + // minSeedBytes is the minimum number of bytes allowed for a seed to + // a master node. + minSeedBytes = 16 // 128 bits + + // maxSeedBytes is the maximum number of bytes allowed for a seed to + // a master node. + maxSeedBytes = 64 // 512 bits + + // serializedKeyLen is the length of a serialized public or private + // extended key. It consists of 4 bytes version, 1 byte depth, 4 bytes + // fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes + // public/private key data. + serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes +) + +var ( + // ErrDeriveHardFromPublic describes an error in which the caller + // attempted to derive a hardened extended key from a public key. + ErrDeriveHardFromPublic = errors.New("cannot derive a hardened key " + + "from a public key") + + // ErrNotPrivExtKey describes an error in which the caller attempted + // to extract a private key from a public extended key. + ErrNotPrivExtKey = errors.New("unable to create private keys from a " + + "public extended key") + + // ErrInvalidChild describes an error in which the child at a specific + // index is invalid due to the derived key falling outside of the valid + // range for secp256k1 private keys. This error indicates the caller + // should simply ignore the invalid child extended key at this index and + // increment to the next index. + ErrInvalidChild = errors.New("the extended key at this index is invalid") + + // ErrUnusableSeed describes an error in which the provided seed is not + // usable due to the derived key falling outside of the valid range for + // secp256k1 private keys. This error indicates the caller must choose + // another seed. + ErrUnusableSeed = errors.New("unusable seed") + + // ErrInvalidSeedLen describes an error in which the provided seed or + // seed length is not in the allowed range. + ErrInvalidSeedLen = fmt.Errorf("seed length must be between %d and %d "+ + "bits", minSeedBytes*8, maxSeedBytes*8) + + // ErrBadChecksum describes an error in which the checksum encoded with + // a serialized extended key does not match the calculated value. + ErrBadChecksum = errors.New("bad extended key checksum") + + // ErrInvalidKeyLen describes an error in which the provided serialized + // key is not the expected length. + ErrInvalidKeyLen = errors.New("the provided serialized extended key " + + "length is invalid") +) + +// masterKey is the master key used along with a random seed used to generate +// the master node in the hierarchical tree. +var masterKey = []byte("Bitcoin seed") + +// ExtendedKey houses all the information needed to support a hierarchical +// deterministic extended key. See the package overview documentation for +// more details on how to use extended keys. +type ExtendedKey struct { + key []byte // This will be the pubkey for extended pub keys + pubKey []byte // This will only be set for extended priv keys + chainCode []byte + depth uint16 + parentFP []byte + childNum uint32 + version []byte + isPrivate bool +} + +// newExtendedKey returns a new instance of an extended key with the given +// fields. No error checking is performed here as it's only intended to be a +// convenience method used to create a populated struct. +func newExtendedKey(version, key, chainCode, parentFP []byte, depth uint16, + childNum uint32, isPrivate bool) *ExtendedKey { + + // NOTE: The pubKey field is intentionally left nil so it is only + // computed and memoized as required. + return &ExtendedKey{ + key: key, + chainCode: chainCode, + depth: depth, + parentFP: parentFP, + childNum: childNum, + version: version, + isPrivate: isPrivate, + } +} + +// pubKeyBytes returns bytes for the serialized compressed public key associated +// with this extended key in an efficient manner including memoization as +// necessary. +// +// When the extended key is already a public key, the key is simply returned as +// is since it's already in the correct form. However, when the extended key is +// a private key, the public key will be calculated and memoized so future +// accesses can simply return the cached result. +func (k *ExtendedKey) pubKeyBytes() []byte { + // Just return the key if it's already an extended public key. + if !k.isPrivate { + return k.key + } + + // This is a private extended key, so calculate and memoize the public + // key if needed. + if len(k.pubKey) == 0 { + pkx, pky := btcec.S256().ScalarBaseMult(k.key) + pubKey := btcec.PublicKey{Curve: btcec.S256(), X: pkx, Y: pky} + k.pubKey = pubKey.SerializeCompressed() + } + + return k.pubKey +} + +// IsPrivate returns whether or not the extended key is a private extended key. +// +// A private extended key can be used to derive both hardened and non-hardened +// child private and public extended keys. A public extended key can only be +// used to derive non-hardened child public extended keys. +func (k *ExtendedKey) IsPrivate() bool { + return k.isPrivate +} + +// ParentFingerprint returns a fingerprint of the parent extended key from which +// this one was derived. +func (k *ExtendedKey) ParentFingerprint() uint32 { + return binary.BigEndian.Uint32(k.parentFP) +} + +// Child returns a derived child extended key at the given index. When this +// extended key is a private extended key (as determined by the IsPrivate +// function), a private extended key will be derived. Otherwise, the derived +// extended key will be also be a public extended key. +// +// When the index is greater to or equal than the HardenedKeyStart constant, the +// derived extended key will be a hardened extended key. It is only possible to +// derive a hardended extended key from a private extended key. Consequently, +// this function will return ErrDeriveHardFromPublic if a hardened child +// extended key is requested from a public extended key. +// +// A hardened extended key is useful since, as previously mentioned, it requires +// a parent private extended key to derive. In other words, normal child +// extended public keys can be derived from a parent public extended key (no +// knowledge of the parent private key) whereas hardened extended keys may not +// be. +// +// NOTE: There is an extremely small chance (< 1 in 2^127) the specific child +// index does not derive to a usable child. The ErrInvalidChild error will be +// returned if this should occur, and the caller is expected to ignore the +// invalid child and simply increment to the next index. +func (k *ExtendedKey) Child(i uint32) (*ExtendedKey, error) { + // There are four scenarios that could happen here: + // 1) Private extended key -> Hardened child private extended key + // 2) Private extended key -> Non-hardened child private extended key + // 3) Public extended key -> Non-hardened child public extended key + // 4) Public extended key -> Hardened child public extended key (INVALID!) + + // Case #4 is invalid, so error out early. + // A hardened child extended key may not be created from a public + // extended key. + isChildHardened := i >= HardenedKeyStart + if !k.isPrivate && isChildHardened { + return nil, ErrDeriveHardFromPublic + } + + // The data used to derive the child key depends on whether or not the + // child is hardened per [BIP32]. + // + // For hardened children: + // 0x00 || ser256(parentKey) || ser32(i) + // + // For normal children: + // serP(parentPubKey) || ser32(i) + keyLen := 33 + data := make([]byte, keyLen+4) + if isChildHardened { + // Case #1. + // When the child is a hardened child, the key is known to be a + // private key due to the above early return. Pad it with a + // leading zero as required by [BIP32] for deriving the child. + copy(data[1:], k.key) + } else { + // Case #2 or #3. + // This is either a public or private extended key, but in + // either case, the data which is used to derive the child key + // starts with the secp256k1 compressed public key bytes. + copy(data, k.pubKeyBytes()) + } + binary.BigEndian.PutUint32(data[keyLen:], i) + + // Take the HMAC-SHA512 of the current key's chain code and the derived + // data: + // I = HMAC-SHA512(Key = chainCode, Data = data) + hmac512 := hmac.New(sha512.New, k.chainCode) + hmac512.Write(data) + ilr := hmac512.Sum(nil) + + // Split "I" into two 32-byte sequences Il and Ir where: + // Il = intermediate key used to derive the child + // Ir = child chain code + il := ilr[:len(ilr)/2] + childChainCode := ilr[len(ilr)/2:] + + // Both derived public or private keys rely on treating the left 32-byte + // sequence calculated above (Il) as a 256-bit integer that must be + // within the valid range for a secp256k1 private key. There is a small + // chance (< 1 in 2^127) this condition will not hold, and in that case, + // a child extended key can't be created for this index and the caller + // should simply increment to the next index. + ilNum := new(big.Int).SetBytes(il) + if ilNum.Cmp(btcec.S256().N) >= 0 || ilNum.Sign() == 0 { + return nil, ErrInvalidChild + } + + // The algorithm used to derive the child key depends on whether or not + // a private or public child is being derived. + // + // For private children: + // childKey = parse256(Il) + parentKey + // + // For public children: + // childKey = serP(point(parse256(Il)) + parentKey) + var isPrivate bool + var childKey []byte + if k.isPrivate { + // Case #1 or #2. + // Add the parent private key to the intermediate private key to + // derive the final child key. + // + // childKey = parse256(Il) + parenKey + keyNum := new(big.Int).SetBytes(k.key) + ilNum.Add(ilNum, keyNum) + ilNum.Mod(ilNum, btcec.S256().N) + childKey = ilNum.Bytes() + isPrivate = true + } else { + // Case #3. + // Calculate the corresponding intermediate public key for + // intermediate private key. + ilx, ily := btcec.S256().ScalarBaseMult(il) + if ilx.Sign() == 0 || ily.Sign() == 0 { + return nil, ErrInvalidChild + } + + // Convert the serialized compressed parent public key into X + // and Y coordinates so it can be added to the intermediate + // public key. + pubKey, err := btcec.ParsePubKey(k.key, btcec.S256()) + if err != nil { + return nil, err + } + + // Add the intermediate public key to the parent public key to + // derive the final child key. + // + // childKey = serP(point(parse256(Il)) + parentKey) + childX, childY := btcec.S256().Add(ilx, ily, pubKey.X, pubKey.Y) + pk := btcec.PublicKey{Curve: btcec.S256(), X: childX, Y: childY} + childKey = pk.SerializeCompressed() + } + + // The fingerprint of the parent for the derived child is the first 4 + // bytes of the RIPEMD160(SHA256(parentPubKey)). + parentFP := btcutil.Hash160(k.pubKeyBytes())[:4] + return newExtendedKey(k.version, childKey, childChainCode, parentFP, + k.depth+1, i, isPrivate), nil +} + +// Neuter returns a new extended public key from this extended private key. The +// same extended key will be returned unaltered if it is already an extended +// public key. +// +// As the name implies, an extended public key does not have access to the +// private key, so it is not capable of signing transactions or deriving +// child extended private keys. However, it is capable of deriving further +// child extended public keys. +func (k *ExtendedKey) Neuter() (*ExtendedKey, error) { + // Already an extended public key. + if !k.isPrivate { + return k, nil + } + + // Get the associated public extended key version bytes. + version, err := btcnet.HDPrivateKeyToPublicKeyID(k.version) + if err != nil { + return nil, err + } + + // Convert it to an extended public key. The key for the new extended + // key will simply be the pubkey of the current extended private key. + // + // This is the function N((k,c)) -> (K, c) from [BIP32]. + return newExtendedKey(version, k.pubKeyBytes(), k.chainCode, k.parentFP, + k.depth, k.childNum, false), nil +} + +// ECPubKey converts the extended key to a btcec public key and returns it. +func (k *ExtendedKey) ECPubKey() (*btcec.PublicKey, error) { + return btcec.ParsePubKey(k.pubKeyBytes(), btcec.S256()) +} + +// ECPrivKey converts the extended key to a btcec private key and returns it. +// As you might imagine this is only possible if the extended key is a private +// extended key (as determined by the IsPrivate function). The ErrNotPrivExtKey +// error will be returned if this function is called on a public extended key. +func (k *ExtendedKey) ECPrivKey() (*btcec.PrivateKey, error) { + if !k.isPrivate { + return nil, ErrNotPrivExtKey + } + + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), k.key) + return privKey, nil +} + +// Address converts the extended key to a standard bitcoin pay-to-pubkey-hash +// address for the passed network. +func (k *ExtendedKey) Address(net *btcnet.Params) (*btcutil.AddressPubKeyHash, error) { + pkHash := btcutil.Hash160(k.pubKeyBytes()) + return btcutil.NewAddressPubKeyHash(pkHash, net) +} + +// String returns the extended key as a human-readable base58-encoded string. +func (k *ExtendedKey) String() string { + var childNumBytes [4]byte + depthByte := byte(k.depth % 256) + binary.BigEndian.PutUint32(childNumBytes[:], k.childNum) + + // The serialized format is: + // version (4) || depth (1) || parent fingerprint (4)) || + // child num (4) || chain code (32) || key data (33) || checksum (4) + serializedBytes := make([]byte, 0, serializedKeyLen+4) + serializedBytes = append(serializedBytes, k.version...) + serializedBytes = append(serializedBytes, depthByte) + serializedBytes = append(serializedBytes, k.parentFP...) + serializedBytes = append(serializedBytes, childNumBytes[:]...) + serializedBytes = append(serializedBytes, k.chainCode...) + if k.isPrivate { + serializedBytes = append(serializedBytes, 0x00) + serializedBytes = append(serializedBytes, k.key...) + } else { + serializedBytes = append(serializedBytes, k.pubKeyBytes()...) + } + + checkSum := btcwire.DoubleSha256(serializedBytes)[:4] + serializedBytes = append(serializedBytes, checkSum...) + return btcutil.Base58Encode(serializedBytes) +} + +// IsForNet returns whether or not the extended key is associated with the +// passed bitcoin network. +func (k *ExtendedKey) IsForNet(net *btcnet.Params) bool { + return bytes.Equal(k.version, net.HDPrivateKeyID[:]) || + bytes.Equal(k.version, net.HDPublicKeyID[:]) +} + +// SetNet associates the extended key, and any child keys yet to be derived from +// it, with the passed network. +func (k *ExtendedKey) SetNet(net *btcnet.Params) { + if k.isPrivate { + k.version = net.HDPrivateKeyID[:] + } else { + k.version = net.HDPublicKeyID[:] + } +} + +// NewMaster creates a new master node for use in creating a hierarchical +// deterministic key chain. The seed must be between 128 and 512 bits and +// should be generated by a cryptographically secure random generation source. +// +// NOTE: There is an extremely small chance (< 1 in 2^127) the provided seed +// will derive to an unusable secret key. The ErrUnusable error will be +// returned if this should occur, so the caller must check for it and generate a +// new seed accordingly. +func NewMaster(seed []byte) (*ExtendedKey, error) { + // Per [BIP32], the seed must be in range [minSeedBytes, maxSeedBytes]. + if len(seed) < minSeedBytes || len(seed) > maxSeedBytes { + return nil, ErrInvalidSeedLen + } + + // First take the HMAC-SHA512 of the master key and the seed data: + // I = HMAC-SHA512(Key = "Bitcoin seed", Data = S) + hmac512 := hmac.New(sha512.New, masterKey) + hmac512.Write(seed) + lr := hmac512.Sum(nil) + + // Split "I" into two 32-byte sequences Il and Ir where: + // Il = master secret key + // Ir = master chain code + secretKey := lr[:len(lr)/2] + chainCode := lr[len(lr)/2:] + + // Ensure the key in usable. + secretKeyNum := new(big.Int).SetBytes(secretKey) + if secretKeyNum.Cmp(btcec.S256().N) >= 0 || secretKeyNum.Sign() == 0 { + return nil, ErrUnusableSeed + } + + parentFP := []byte{0x00, 0x00, 0x00, 0x00} + return newExtendedKey(btcnet.MainNetParams.HDPrivateKeyID[:], secretKey, + chainCode, parentFP, 0, 0, true), nil +} + +// NewKeyFromString returns a new extended key instance from a base58-encoded +// extended key. +func NewKeyFromString(key string) (*ExtendedKey, error) { + // The base58-decoded extended key must consist of a serialized payload + // plus an additional 4 bytes for the checksum. + decoded := btcutil.Base58Decode(key) + if len(decoded) != serializedKeyLen+4 { + return nil, ErrInvalidKeyLen + } + + // The serialized format is: + // version (4) || depth (1) || parent fingerprint (4)) || + // child num (4) || chain code (32) || key data (33) || checksum (4) + + // Split the payload and checksum up and ensure the checksum matches. + payload := decoded[:len(decoded)-4] + checkSum := decoded[len(decoded)-4:] + expectedCheckSum := btcwire.DoubleSha256(payload)[:4] + if !bytes.Equal(checkSum, expectedCheckSum) { + return nil, ErrBadChecksum + } + + // Deserialize each of the payload fields. + version := payload[:4] + depth := uint16(payload[4:5][0]) + parentFP := payload[5:9] + childNum := binary.BigEndian.Uint32(payload[9:13]) + chainCode := payload[13:45] + keyData := payload[45:78] + + // The key data is a private key if it starts with 0x00. Serialized + // compressed pubkeys either start with 0x02 or 0x03. + isPrivate := keyData[0] == 0x00 + if isPrivate { + // Ensure the private key is valid. It must be within the range + // of the order of the secp256k1 curve and not be 0. + keyData = keyData[1:] + keyNum := new(big.Int).SetBytes(keyData) + if keyNum.Cmp(btcec.S256().N) >= 0 || keyNum.Sign() == 0 { + return nil, ErrUnusableSeed + } + } else { + // Ensure the public key parses correctly and is actually on the + // secp256k1 curve. + _, err := btcec.ParsePubKey(keyData, btcec.S256()) + if err != nil { + return nil, err + } + } + + return newExtendedKey(version, keyData, chainCode, parentFP, depth, + childNum, isPrivate), nil +} + +// GenerateSeed returns a cryptographically secure random seed that can be used +// as the input for the NewMaster function to generate a new master node. +// +// The length is in bytes and it must be between 16 and 64 (128 to 512 bits). +// The recommended length is 32 (256 bits) as defined by the RecommendedSeedLen +// constant. +func GenerateSeed(length uint8) ([]byte, error) { + // Per [BIP32], the seed must be in range [minSeedBytes, maxSeedBytes]. + if length < minSeedBytes || length > maxSeedBytes { + return nil, ErrInvalidSeedLen + } + + buf := make([]byte, length) + _, err := rand.Read(buf) + if err != nil { + return nil, err + } + + return buf, nil +} diff --git a/hdkeychain/extendedkey_test.go b/hdkeychain/extendedkey_test.go new file mode 100644 index 0000000..ab0cff4 --- /dev/null +++ b/hdkeychain/extendedkey_test.go @@ -0,0 +1,676 @@ +// Copyright (c) 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package hdkeychain_test + +// References: +// [BIP32]: BIP0032 - Hierarchical Deterministic Wallets +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki + +import ( + "bytes" + "encoding/hex" + "errors" + "reflect" + "testing" + + "github.com/conformal/btcnet" + "github.com/conformal/btcutil/hdkeychain" +) + +// TestBIP0032Vectors tests the vectors provided by [BIP32] to ensure the +// derivation works as intended. +func TestBIP0032Vectors(t *testing.T) { + // The master seeds for each of the two test vectors in [BIP32]. + testVec1MasterHex := "000102030405060708090a0b0c0d0e0f" + testVec2MasterHex := "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" + hkStart := uint32(0x80000000) + + var tests = []struct { + name string + master string + path []uint32 + wantPub string + wantPriv string + }{ + // Test vector 1 + { + name: "test vector 1 chain m", + master: testVec1MasterHex, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + wantPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + }, + { + name: "test vector 1 chain m/0H", + master: testVec1MasterHex, + path: []uint32{hkStart}, + wantPub: "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", + wantPriv: "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", + }, + { + name: "test vector 1 chain m/0H/1", + master: testVec1MasterHex, + path: []uint32{hkStart, 1}, + wantPub: "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", + wantPriv: "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", + }, + { + name: "test vector 1 chain m/0H/1/2H", + master: testVec1MasterHex, + path: []uint32{hkStart, 1, hkStart + 2}, + wantPub: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + wantPriv: "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", + }, + { + name: "test vector 1 chain m/0H/1/2H/2", + master: testVec1MasterHex, + path: []uint32{hkStart, 1, hkStart + 2, 2}, + wantPub: "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + wantPriv: "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", + }, + { + name: "test vector 1 chain m/0H/1/2H/2/1000000000", + master: testVec1MasterHex, + path: []uint32{hkStart, 1, hkStart + 2, 2, 1000000000}, + wantPub: "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + wantPriv: "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", + }, + + // Test vector 2 + { + name: "test vector 2 chain m", + master: testVec2MasterHex, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + wantPriv: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + }, + { + name: "test vector 2 chain m/0", + master: testVec2MasterHex, + path: []uint32{0}, + wantPub: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + wantPriv: "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", + }, + { + name: "test vector 2 chain m/0/2147483647H", + master: testVec2MasterHex, + path: []uint32{0, hkStart + 2147483647}, + wantPub: "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", + wantPriv: "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", + }, + { + name: "test vector 2 chain m/0/2147483647H/1", + master: testVec2MasterHex, + path: []uint32{0, hkStart + 2147483647, 1}, + wantPub: "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", + wantPriv: "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", + }, + { + name: "test vector 2 chain m/0/2147483647H/1/2147483646H", + master: testVec2MasterHex, + path: []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646}, + wantPub: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + wantPriv: "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", + }, + { + name: "test vector 2 chain m/0/2147483647H/1/2147483646H/2", + master: testVec2MasterHex, + path: []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646, 2}, + wantPub: "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", + wantPriv: "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", + }, + } + +tests: + for i, test := range tests { + masterSeed, err := hex.DecodeString(test.master) + if err != nil { + t.Errorf("DecodeString #%d (%s): unexpected error: %v", + i, test.name, err) + continue + } + + extKey, err := hdkeychain.NewMaster(masterSeed) + if err != nil { + t.Errorf("NewMaster #%d (%s): unexpected error when "+ + "creating new master key: %v", i, test.name, + err) + continue + } + + for _, childNum := range test.path { + var err error + extKey, err = extKey.Child(childNum) + if err != nil { + t.Errorf("err: %v", err) + continue tests + } + } + + privStr := extKey.String() + if privStr != test.wantPriv { + t.Errorf("Serialize #%d (%s): mismatched serialized "+ + "private extended key -- got: %s, want: %s", i, + test.name, privStr, test.wantPriv) + continue + } + + pubKey, err := extKey.Neuter() + if err != nil { + t.Errorf("Neuter #%d (%s): unexpected error: %v ", i, + test.name, err) + continue + } + + // Neutering a second time should have no effect. + pubKey, err = pubKey.Neuter() + if err != nil { + t.Errorf("Neuter #%d (%s): unexpected error: %v", i, + test.name, err) + return + } + + pubStr := pubKey.String() + if pubStr != test.wantPub { + t.Errorf("Neuter #%d (%s): mismatched serialized "+ + "public extended key -- got: %s, want: %s", i, + test.name, pubStr, test.wantPub) + continue + } + } +} + +// TestPublicDerivation tests several vectors which derive public keys from +// other public keys works as intended. +func TestPublicDerivation(t *testing.T) { + // The public extended keys for test vectors in [BIP32]. + testVec1MasterPubKey := "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" + testVec2MasterPubKey := "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + + var tests = []struct { + name string + master string + path []uint32 + wantPub string + }{ + // Test vector 1 + { + name: "test vector 1 chain m", + master: testVec1MasterPubKey, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + }, + { + name: "test vector 1 chain m/0", + master: testVec1MasterPubKey, + path: []uint32{0}, + wantPub: "xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1", + }, + { + name: "test vector 1 chain m/0/1", + master: testVec1MasterPubKey, + path: []uint32{0, 1}, + wantPub: "xpub6AvUGrnEpfvJBbfx7sQ89Q8hEMPM65UteqEX4yUbUiES2jHfjexmfJoxCGSwFMZiPBaKQT1RiKWrKfuDV4vpgVs4Xn8PpPTR2i79rwHd4Zr", + }, + { + name: "test vector 1 chain m/0/1/2", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2}, + wantPub: "xpub6BqyndF6rhZqmgktFCBcapkwubGxPqoAZtQaYewJHXVKZcLdnqBVC8N6f6FSHWUghjuTLeubWyQWfJdk2G3tGgvgj3qngo4vLTnnSjAZckv", + }, + { + name: "test vector 1 chain m/0/1/2/2", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2, 2}, + wantPub: "xpub6FHUhLbYYkgFQiFrDiXRfQFXBB2msCxKTsNyAExi6keFxQ8sHfwpogY3p3s1ePSpUqLNYks5T6a3JqpCGszt4kxbyq7tUoFP5c8KWyiDtPp", + }, + { + name: "test vector 1 chain m/0/1/2/2/1000000000", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2, 2, 1000000000}, + wantPub: "xpub6GX3zWVgSgPc5tgjE6ogT9nfwSADD3tdsxpzd7jJoJMqSY12Be6VQEFwDCp6wAQoZsH2iq5nNocHEaVDxBcobPrkZCjYW3QUmoDYzMFBDu9", + }, + + // Test vector 2 + { + name: "test vector 2 chain m", + master: testVec2MasterPubKey, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + }, + { + name: "test vector 2 chain m/0", + master: testVec2MasterPubKey, + path: []uint32{0}, + wantPub: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + }, + { + name: "test vector 2 chain m/0/2147483647", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647}, + wantPub: "xpub6ASAVgeWMg4pmutghzHG3BohahjwNwPmy2DgM6W9wGegtPrvNgjBwuZRD7hSDFhYfunq8vDgwG4ah1gVzZysgp3UsKz7VNjCnSUJJ5T4fdD", + }, + { + name: "test vector 2 chain m/0/2147483647/1", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1}, + wantPub: "xpub6CrnV7NzJy4VdgP5niTpqWJiFXMAca6qBm5Hfsry77SQmN1HGYHnjsZSujoHzdxf7ZNK5UVrmDXFPiEW2ecwHGWMFGUxPC9ARipss9rXd4b", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1, 2147483646}, + wantPub: "xpub6FL2423qFaWzHCvBndkN9cbkn5cysiUeFq4eb9t9kE88jcmY63tNuLNRzpHPdAM4dUpLhZ7aUm2cJ5zF7KYonf4jAPfRqTMTRBNkQL3Tfta", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646/2", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1, 2147483646, 2}, + wantPub: "xpub6H7WkJf547AiSwAbX6xsm8Bmq9M9P1Gjequ5SipsjipWmtXSyp4C3uwzewedGEgAMsDy4jEvNTWtxLyqqHY9C12gaBmgUdk2CGmwachwnWK", + }, + } + +tests: + for i, test := range tests { + extKey, err := hdkeychain.NewKeyFromString(test.master) + if err != nil { + t.Errorf("NewKeyFromString #%d (%s): unexpected error "+ + "creating extended key: %v", i, test.name, + err) + continue + } + + for _, childNum := range test.path { + var err error + extKey, err = extKey.Child(childNum) + if err != nil { + t.Errorf("err: %v", err) + continue tests + } + } + + pubStr := extKey.String() + if pubStr != test.wantPub { + t.Errorf("Child #%d (%s): mismatched serialized "+ + "public extended key -- got: %s, want: %s", i, + test.name, pubStr, test.wantPub) + continue + } + } +} + +// TestGenenerateSeed ensures the GenerateSeed function works as intended. +func TestGenenerateSeed(t *testing.T) { + wantErr := errors.New("seed length must be between 128 and 512 bits") + + var tests = []struct { + name string + length uint8 + err error + }{ + // Test various valid lengths. + {name: "16 bytes", length: 16}, + {name: "17 bytes", length: 17}, + {name: "20 bytes", length: 20}, + {name: "32 bytes", length: 32}, + {name: "64 bytes", length: 64}, + + // Test invalid lengths. + {name: "15 bytes", length: 15, err: wantErr}, + {name: "65 bytes", length: 65, err: wantErr}, + } + + for i, test := range tests { + seed, err := hdkeychain.GenerateSeed(test.length) + if !reflect.DeepEqual(err, test.err) { + t.Errorf("GenerateSeed #%d (%s): unexpected error -- "+ + "want %v, got %v", i, test.name, test.err, err) + continue + } + + if test.err == nil && len(seed) != int(test.length) { + t.Errorf("GenerateSeed #%d (%s): length mismatch -- "+ + "got %d, want %d", i, test.name, len(seed), + test.length) + continue + } + } +} + +// TestExtendedKeyAPI ensures the API on the ExtendedKey type works as intended. +func TestExtendedKeyAPI(t *testing.T) { + var tests = []struct { + name string + extKey string + isPrivate bool + parentFP uint32 + privKey string + privKeyErr error + pubKey string + address string + }{ + { + name: "test vector 1 master node private", + extKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + isPrivate: true, + parentFP: 0, + privKey: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", + pubKey: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2", + address: "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma", + }, + { + name: "test vector 1 chain m/0H/1/2H public", + extKey: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + isPrivate: false, + parentFP: 3203769081, + privKeyErr: hdkeychain.ErrNotPrivExtKey, + pubKey: "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2", + address: "1NjxqbA9aZWnh17q1UW3rB4EPu79wDXj7x", + }, + } + + for i, test := range tests { + key, err := hdkeychain.NewKeyFromString(test.extKey) + if err != nil { + t.Errorf("NewKeyFromString #%d (%s): unexpected "+ + "error: %v", i, test.name, err) + continue + } + + if key.IsPrivate() != test.isPrivate { + t.Errorf("IsPrivate #%d (%s): mismatched key type -- "+ + "want private %v, got private %v", i, test.name, + test.isPrivate, key.IsPrivate()) + continue + } + + parentFP := key.ParentFingerprint() + if parentFP != test.parentFP { + t.Errorf("ParentFingerprint #%d (%s): mismatched "+ + "parent fingerprint -- want %d, got %d", i, + test.name, test.parentFP, parentFP) + continue + } + + serializedKey := key.String() + if serializedKey != test.extKey { + t.Errorf("String #%d (%s): mismatched serialized key "+ + "-- want %s, got %s", i, test.name, test.extKey, + serializedKey) + continue + } + + privKey, err := key.ECPrivKey() + if !reflect.DeepEqual(err, test.privKeyErr) { + t.Errorf("ECPrivKey #%d (%s): mismatched error: want "+ + "%v, got %v", i, test.name, test.privKeyErr, err) + continue + } + if test.privKeyErr == nil { + privKeyStr := hex.EncodeToString(privKey.Serialize()) + if privKeyStr != test.privKey { + t.Errorf("ECPrivKey #%d (%s): mismatched "+ + "private key -- want %s, got %s", i, + test.name, test.privKey, privKeyStr) + continue + } + } + + pubKey, err := key.ECPubKey() + if err != nil { + t.Errorf("ECPubKey #%d (%s): unexpected error: %v", i, + test.name, err) + continue + } + pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed()) + if pubKeyStr != test.pubKey { + t.Errorf("ECPubKey #%d (%s): mismatched public key -- "+ + "want %s, got %s", i, test.name, test.pubKey, + pubKeyStr) + continue + } + + addr, err := key.Address(&btcnet.MainNetParams) + if err != nil { + t.Errorf("Address #%d (%s): unexpected error: %v", i, + test.name, err) + continue + } + if addr.EncodeAddress() != test.address { + t.Errorf("Address #%d (%s): mismatched address -- want "+ + "%s, got %s", i, test.name, test.address, + addr.EncodeAddress()) + continue + } + } +} + +// TestNet ensures the network related APIs work as intended. +func TestNet(t *testing.T) { + var tests = []struct { + name string + key string + origNet *btcnet.Params + newNet *btcnet.Params + newPriv string + newPub string + isPrivate bool + }{ + // Private extended keys. + { + name: "mainnet -> simnet", + key: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + origNet: &btcnet.MainNetParams, + newNet: &btcnet.SimNetParams, + newPriv: "sprv8Erh3X3hFeKunvVdAGQQtambRPapECWiTDtvsTGdyrhzhbYgnSZajRRWbihzvq4AM4ivm6uso31VfKaukwJJUs3GYihXP8ebhMb3F2AHu3P", + newPub: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + isPrivate: true, + }, + { + name: "simnet -> mainnet", + key: "sprv8Erh3X3hFeKunvVdAGQQtambRPapECWiTDtvsTGdyrhzhbYgnSZajRRWbihzvq4AM4ivm6uso31VfKaukwJJUs3GYihXP8ebhMb3F2AHu3P", + origNet: &btcnet.SimNetParams, + newNet: &btcnet.MainNetParams, + newPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: true, + }, + { + name: "mainnet -> regtest", + key: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + origNet: &btcnet.MainNetParams, + newNet: &btcnet.RegressionNetParams, + newPriv: "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m", + newPub: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + isPrivate: true, + }, + { + name: "regtest -> mainnet", + key: "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m", + origNet: &btcnet.RegressionNetParams, + newNet: &btcnet.MainNetParams, + newPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: true, + }, + + // Public extended keys. + { + name: "mainnet -> simnet", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + origNet: &btcnet.MainNetParams, + newNet: &btcnet.SimNetParams, + newPub: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + isPrivate: false, + }, + { + name: "simnet -> mainnet", + key: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + origNet: &btcnet.SimNetParams, + newNet: &btcnet.MainNetParams, + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: false, + }, + { + name: "mainnet -> regtest", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + origNet: &btcnet.MainNetParams, + newNet: &btcnet.RegressionNetParams, + newPub: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + isPrivate: false, + }, + { + name: "regtest -> mainnet", + key: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + origNet: &btcnet.RegressionNetParams, + newNet: &btcnet.MainNetParams, + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: false, + }, + } + + for i, test := range tests { + extKey, err := hdkeychain.NewKeyFromString(test.key) + if err != nil { + t.Errorf("NewKeyFromString #%d (%s): unexpected error "+ + "creating extended key: %v", i, test.name, + err) + continue + } + + if !extKey.IsForNet(test.origNet) { + t.Errorf("IsForNet #%d (%s): key is not for expected "+ + "network %v", i, test.name, test.origNet.Name) + continue + } + + extKey.SetNet(test.newNet) + if !extKey.IsForNet(test.newNet) { + t.Errorf("SetNet/IsForNet #%d (%s): key is not for "+ + "expected network %v", i, test.name, + test.newNet.Name) + continue + } + + if test.isPrivate { + privStr := extKey.String() + if privStr != test.newPriv { + t.Errorf("Serialize #%d (%s): mismatched serialized "+ + "private extended key -- got: %s, want: %s", i, + test.name, privStr, test.newPriv) + continue + } + + extKey, err = extKey.Neuter() + if err != nil { + t.Errorf("Neuter #%d (%s): unexpected error: %v ", i, + test.name, err) + continue + } + } + + pubStr := extKey.String() + if pubStr != test.newPub { + t.Errorf("Neuter #%d (%s): mismatched serialized "+ + "public extended key -- got: %s, want: %s", i, + test.name, pubStr, test.newPub) + continue + } + } +} + +// TestErrors performs some negative tests for various invalid cases to ensure +// the errors are handled properly. +func TestErrors(t *testing.T) { + // Should get an error when seed has too few bytes. + _, err := hdkeychain.NewMaster(bytes.Repeat([]byte{0x00}, 15)) + if err != hdkeychain.ErrInvalidSeedLen { + t.Errorf("NewMaster: mismatched error -- got: %v, want: %v", + err, hdkeychain.ErrInvalidSeedLen) + } + + // Should get an error when seed has too many bytes. + _, err = hdkeychain.NewMaster(bytes.Repeat([]byte{0x00}, 65)) + if err != hdkeychain.ErrInvalidSeedLen { + t.Errorf("NewMaster: mismatched error -- got: %v, want: %v", + err, hdkeychain.ErrInvalidSeedLen) + } + + // Generate a new key and neuter it to a public extended key. + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + t.Errorf("GenerateSeed: unexpected error: %v", err) + return + } + extKey, err := hdkeychain.NewMaster(seed) + if err != nil { + t.Errorf("NewMaster: unexpected error: %v", err) + return + } + pubKey, err := extKey.Neuter() + if err != nil { + t.Errorf("Neuter: unexpected error: %v", err) + return + } + + // Deriving a hardened child extended key should fail from a public key. + _, err = pubKey.Child(hdkeychain.HardenedKeyStart) + if err != hdkeychain.ErrDeriveHardFromPublic { + t.Errorf("Child: mismatched error -- got: %v, want: %v", + err, hdkeychain.ErrDeriveHardFromPublic) + } + + // NewKeyFromString failure tests. + tests := []struct { + name string + key string + err error + neuter bool + neuterErr error + }{ + { + name: "invalid key length", + key: "xpub1234", + err: hdkeychain.ErrInvalidKeyLen, + }, + { + name: "bad checksum", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EBygr15", + err: hdkeychain.ErrBadChecksum, + }, + { + name: "pubkey not on curve", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ1hr9Rwbk95YadvBkQXxzHBSngB8ndpW6QH7zhhsXZ2jHyZqPjk", + err: errors.New("pubkey isn't on secp265k1 curve"), + }, + { + name: "unsupported version", + key: "xbad4LfUL9eKmA66w2GJdVMqhvDmYGJpTGjWRAtjHqoUY17sGaymoMV9Cm3ocn9Ud6Hh2vLFVC7KSKCRVVrqc6dsEdsTjRV1WUmkK85YEUujAPX", + err: nil, + neuter: true, + neuterErr: btcnet.ErrUnknownHDKeyID, + }, + } + + for i, test := range tests { + extKey, err := hdkeychain.NewKeyFromString(test.key) + if !reflect.DeepEqual(err, test.err) { + t.Errorf("NewKeyFromString #%d (%s): mismatched error "+ + "-- got: %v, want: %v", i, test.name, err, + test.err) + continue + } + + if test.neuter { + _, err := extKey.Neuter() + if !reflect.DeepEqual(err, test.neuterErr) { + t.Errorf("Neuter #%d (%s): mismatched error "+ + "-- got: %v, want: %v", i, test.name, + err, test.neuterErr) + continue + } + } + } +} diff --git a/hdkeychain/test_coverage.txt b/hdkeychain/test_coverage.txt new file mode 100644 index 0000000..8673aef --- /dev/null +++ b/hdkeychain/test_coverage.txt @@ -0,0 +1,18 @@ + +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.String 100.00% (16/16) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.pubKeyBytes 100.00% (7/7) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.Neuter 100.00% (6/6) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.ECPrivKey 100.00% (4/4) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.SetNet 100.00% (3/3) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.Address 100.00% (2/2) +github.com/conformal/btcutil/hdkeychain/extendedkey.go newExtendedKey 100.00% (1/1) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.IsPrivate 100.00% (1/1) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.ParentFingerprint 100.00% (1/1) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.ECPubKey 100.00% (1/1) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.IsForNet 100.00% (1/1) +github.com/conformal/btcutil/hdkeychain/extendedkey.go NewKeyFromString 95.83% (23/24) +github.com/conformal/btcutil/hdkeychain/extendedkey.go ExtendedKey.Child 91.67% (33/36) +github.com/conformal/btcutil/hdkeychain/extendedkey.go NewMaster 91.67% (11/12) +github.com/conformal/btcutil/hdkeychain/extendedkey.go GenerateSeed 85.71% (6/7) +github.com/conformal/btcutil/hdkeychain ----------------------------- 95.08% (116/122) +