e6c5ca2a6a
This commit adds a new sub-package named hdkeychain which can be used to derive hierarchical deterministic key chains which form the foundation of hd wallets. - Support for private and public extended keys - Convenient cryptographically 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 transactions and generate payment scripts - Makes use of 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
676 lines
23 KiB
Go
676 lines
23 KiB
Go
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|