hdkeychain: add CloneWithVersion to set custom HD version bytes

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.
This commit is contained in:
Anirudha Bose 2020-08-28 01:14:04 +02:00 committed by John C. Vernaleo
parent 4232759481
commit 063c4115b3
2 changed files with 98 additions and 0 deletions

View file

@ -378,6 +378,36 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
k.depth, k.childNum, false), nil 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. // ECPubKey converts the extended key to a btcec public key and returns it.
func (k *ExtendedKey) ECPubKey() (*btcec.PublicKey, error) { func (k *ExtendedKey) ECPubKey() (*btcec.PublicKey, error) {
return btcec.ParsePubKey(k.pubKeyBytes(), btcec.S256()) return btcec.ParsePubKey(k.pubKeyBytes(), btcec.S256())

View file

@ -1088,3 +1088,71 @@ func TestMaximumDepth(t *testing.T) {
t.Fatal("Child: deriving 256th key should not succeed") 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
}
}
}
}