Skip to content

Commit

Permalink
hdkeychain: add CloneWithVersion to set custom HD version bytes
Browse files Browse the repository at this point in the history
This adds a new method to the ExtendedKey type that allows cloning the
extended key with custom HD version bytes. It does not mutate the
original extended key on which the method is called.

Added some tests to demonstrate the utility of this method, i.e.,
conversion between standard and SLIP-0132 extended keys.
  • Loading branch information
onyb authored and jcvernaleo committed Sep 21, 2020
1 parent 4232759 commit 063c411
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
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
}
}
}
}

0 comments on commit 063c411

Please sign in to comment.