Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hdkeychain: add CloneWithVersion to set custom HD version bytes #181

Merged
merged 1 commit into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions hdkeychain/extendedkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,36 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
k.depth, k.childNum, false), nil
}

// CloneWithVersion returns a new extended key cloned from this extended key,
// but using the provided HD version bytes. The version must be a private HD
// key ID for an extended private key, and a public HD key ID for an extended
// public key.
//
// This method creates a new copy and therefore does not mutate the original
// extended key instance.
//
// Unlike Neuter(), this does NOT convert an extended private key to an
// extended public key. It is particularly useful for converting between
// standard BIP0032 extended keys (serializable to xprv/xpub) and keys based
// on the SLIP132 standard (serializable to yprv/ypub, zprv/zpub, etc.).
//
// References:
// [SLIP132]: SLIP-0132 - Registered HD version bytes for BIP-0032
// https://github.com/satoshilabs/slips/blob/master/slip-0132.md
func (k *ExtendedKey) CloneWithVersion(version []byte) (*ExtendedKey, error) {
if len(version) != 4 {
// TODO: The semantically correct error to return here is
// ErrInvalidHDKeyID (introduced in btcsuite/btcd#1617). Update the
// error type once available in a stable btcd / chaincfg release.
return nil, chaincfg.ErrUnknownHDKeyID
}

// Initialize a new extended key instance with the same fields as the
// current extended private/public key and the provided HD version bytes.
return NewExtendedKey(version, k.key, k.chainCode, k.parentFP,
k.depth, k.childNum, k.isPrivate), 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())
Expand Down
68 changes: 68 additions & 0 deletions hdkeychain/extendedkey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1088,3 +1088,71 @@ func TestMaximumDepth(t *testing.T) {
t.Fatal("Child: deriving 256th key should not succeed")
}
}

// TestCloneWithVersion ensures proper conversion between standard and SLIP132
// extended keys.
//
// The following tool was used for generating the tests:
// https://jlopp.github.io/xpub-converter
func TestCloneWithVersion(t *testing.T) {
tests := []struct {
name string
key string
version []byte
want string
wantErr error
}{
{
name: "test xpub to zpub",
key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
version: []byte{0x04, 0xb2, 0x47, 0x46},
want: "zpub6jftahH18ngZxUuv6oSniLNrBCSSE1B4EEU59bwTCEt8x6aS6b2mdfLxbS4QS53g85SWWP6wexqeer516433gYpZQoJie2tcMYdJ1SYYYAL",
},
{
name: "test zpub to xpub",
key: "zpub6jftahH18ngZxUuv6oSniLNrBCSSE1B4EEU59bwTCEt8x6aS6b2mdfLxbS4QS53g85SWWP6wexqeer516433gYpZQoJie2tcMYdJ1SYYYAL",
version: []byte{0x04, 0x88, 0xb2, 0x1e},
want: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
},
{
name: "test xprv to zprv",
key: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
version: []byte{0x04, 0xb2, 0x43, 0x0c},
want: "zprvAWgYBBk7JR8GjzqSzmunMCS7dAbwpYTCs1YUMDXqduMA5JFHZ3iX5s2UkAR6vBdcCYYa1S5o1fVLrKsrnpCQ4WpUd6aVUWP1bS2Yy5DoaKv",
},
{
name: "test zprv to xprv",
key: "zprvAWgYBBk7JR8GjzqSzmunMCS7dAbwpYTCs1YUMDXqduMA5JFHZ3iX5s2UkAR6vBdcCYYa1S5o1fVLrKsrnpCQ4WpUd6aVUWP1bS2Yy5DoaKv",
version: []byte{0x04, 0x88, 0xad, 0xe4},
want: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi",
},
{
name: "test invalid key id",
key: "zprvAWgYBBk7JR8GjzqSzmunMCS7dAbwpYTCs1YUMDXqduMA5JFHZ3iX5s2UkAR6vBdcCYYa1S5o1fVLrKsrnpCQ4WpUd6aVUWP1bS2Yy5DoaKv",
version: []byte{0x4B, 0x1D},
wantErr: chaincfg.ErrUnknownHDKeyID,
},
}

for i, test := range tests {
extKey, err := NewKeyFromString(test.key)
if err != nil {
panic(err) // This is never expected to fail.
}

got, err := extKey.CloneWithVersion(test.version)
if !reflect.DeepEqual(err, test.wantErr) {
t.Errorf("CloneWithVersion #%d (%s): unexpected error -- "+
"want %v, got %v", i, test.name, test.wantErr, err)
continue
}

if test.wantErr == nil {
if k := got.String(); k != test.want {
t.Errorf("CloneWithVersion #%d (%s): "+
"got %s, want %s", i, test.name, k, test.want)
continue
}
}
}
}