820 lines
27 KiB
Go
820 lines
27 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/btcsuite/btcutil/hdkeychain"
|
|
"github.com/conformal/btcnet"
|
|
)
|
|
|
|
// 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)
|
|
|
|
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"
|
|
|
|
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")
|
|
|
|
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) {
|
|
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) {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestZero ensures that zeroing an extended key works as intended.
|
|
func TestZero(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
master string
|
|
extKey string
|
|
}{
|
|
// Test vector 1
|
|
{
|
|
name: "test vector 1 chain m",
|
|
master: "000102030405060708090a0b0c0d0e0f",
|
|
extKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
|
|
},
|
|
|
|
// Test vector 2
|
|
{
|
|
name: "test vector 2 chain m",
|
|
master: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
|
|
extKey: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U",
|
|
},
|
|
}
|
|
|
|
// Use a closure to test that a key is zeroed since the tests create
|
|
// keys in different ways and need to test the same things multiple
|
|
// times.
|
|
testZeroed := func(i int, testName string, key *hdkeychain.ExtendedKey) bool {
|
|
// Zeroing a key should result in it no longer being private
|
|
if key.IsPrivate() != false {
|
|
t.Errorf("IsPrivate #%d (%s): mismatched key type -- "+
|
|
"want private %v, got private %v", i, testName,
|
|
false, key.IsPrivate())
|
|
return false
|
|
}
|
|
|
|
parentFP := key.ParentFingerprint()
|
|
if parentFP != 0 {
|
|
t.Errorf("ParentFingerprint #%d (%s): mismatched "+
|
|
"parent fingerprint -- want %d, got %d", i,
|
|
testName, 0, parentFP)
|
|
return false
|
|
}
|
|
|
|
wantKey := "zeroed extended key"
|
|
serializedKey := key.String()
|
|
if serializedKey != wantKey {
|
|
t.Errorf("String #%d (%s): mismatched serialized key "+
|
|
"-- want %s, got %s", i, testName, wantKey,
|
|
serializedKey)
|
|
return false
|
|
}
|
|
|
|
wantErr := hdkeychain.ErrNotPrivExtKey
|
|
_, err := key.ECPrivKey()
|
|
if !reflect.DeepEqual(err, wantErr) {
|
|
t.Errorf("ECPrivKey #%d (%s): mismatched error: want "+
|
|
"%v, got %v", i, testName, wantErr, err)
|
|
return false
|
|
}
|
|
|
|
wantErr = errors.New("pubkey string is empty")
|
|
_, err = key.ECPubKey()
|
|
if !reflect.DeepEqual(err, wantErr) {
|
|
t.Errorf("ECPubKey #%d (%s): mismatched error: want "+
|
|
"%v, got %v", i, testName, wantErr, err)
|
|
return false
|
|
}
|
|
|
|
wantAddr := "1HT7xU2Ngenf7D4yocz2SAcnNLW7rK8d4E"
|
|
addr, err := key.Address(&btcnet.MainNetParams)
|
|
if err != nil {
|
|
t.Errorf("Addres s #%d (%s): unexpected error: %v", i,
|
|
testName, err)
|
|
return false
|
|
}
|
|
if addr.EncodeAddress() != wantAddr {
|
|
t.Errorf("Address #%d (%s): mismatched address -- want "+
|
|
"%s, got %s", i, testName, wantAddr,
|
|
addr.EncodeAddress())
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
for i, test := range tests {
|
|
// Create new key from seed and get the neutered version.
|
|
masterSeed, err := hex.DecodeString(test.master)
|
|
if err != nil {
|
|
t.Errorf("DecodeString #%d (%s): unexpected error: %v",
|
|
i, test.name, err)
|
|
continue
|
|
}
|
|
key, 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
|
|
}
|
|
neuteredKey, err := key.Neuter()
|
|
if err != nil {
|
|
t.Errorf("Neuter #%d (%s): unexpected error: %v", i,
|
|
test.name, err)
|
|
continue
|
|
}
|
|
|
|
// Ensure both non-neutered and neutered keys are zeroed
|
|
// properly.
|
|
key.Zero()
|
|
if !testZeroed(i, test.name+" from seed not neutered", key) {
|
|
continue
|
|
}
|
|
neuteredKey.Zero()
|
|
if !testZeroed(i, test.name+" from seed neutered", key) {
|
|
continue
|
|
}
|
|
|
|
// Deserialize key and get the neutered version.
|
|
key, err = hdkeychain.NewKeyFromString(test.extKey)
|
|
if err != nil {
|
|
t.Errorf("NewKeyFromString #%d (%s): unexpected "+
|
|
"error: %v", i, test.name, err)
|
|
continue
|
|
}
|
|
neuteredKey, err = key.Neuter()
|
|
if err != nil {
|
|
t.Errorf("Neuter #%d (%s): unexpected error: %v", i,
|
|
test.name, err)
|
|
continue
|
|
}
|
|
|
|
// Ensure both non-neutered and neutered keys are zeroed
|
|
// properly.
|
|
key.Zero()
|
|
if !testZeroed(i, test.name+" deserialized not neutered", key) {
|
|
continue
|
|
}
|
|
neuteredKey.Zero()
|
|
if !testZeroed(i, test.name+" deserialized neutered", key) {
|
|
continue
|
|
}
|
|
}
|
|
}
|