From 2592291626cd55d25b20b9cf2b2078e82a0e4408 Mon Sep 17 00:00:00 2001 From: Mark Smith Date: Mon, 23 Aug 2021 16:45:11 +0100 Subject: [PATCH 1/3] adding helper methods to generate new extendedKeys and compressed public keys from a parent extended key using a specified derivation path --- bip32/derivationpaths.go | 57 +++++++++++++++++++++++++++ bip32/derivationpaths_test.go | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/bip32/derivationpaths.go b/bip32/derivationpaths.go index 076522d..c792a58 100644 --- a/bip32/derivationpaths.go +++ b/bip32/derivationpaths.go @@ -3,10 +3,15 @@ package bip32 import ( "errors" "fmt" + "regexp" "strconv" "strings" ) +var ( + numericPlusTick = regexp.MustCompile(`^[0-9]+'{0,1}$`) +) + // DerivePath given an uint64 number will generate a hardened BIP32 path 3 layers deep. // // This is achieved by the following process: @@ -44,3 +49,55 @@ func DeriveNumber(path string) (uint64, error) { seed += d3 - (1 << 31) return seed, nil } + +// DeriveKeyFromPath will generate a new extended key derived from the key k using the +// bip32 path provided, ie "1234/0/123" +func (k *ExtendedKey) DeriveChildFromPath(path string) (*ExtendedKey, error) { + key := k + if path != "" { + children := strings.Split(path, "/") + for _, child := range children { + if !numericPlusTick.MatchString(child) { + return nil, fmt.Errorf("invalid path: %q", path) + } + childInt, err := getChildInt(child) + if err != nil { + return nil, fmt.Errorf("derive key failed %w", err) + } + var childErr error + key, childErr = key.Child(childInt) + if childErr != nil { + return nil, fmt.Errorf("derive key failed %w", childErr) + } + } + } + return key, nil +} + +// DerivePublicKeyFromPath will generate a new extended key derived from the key k using the +// bip32 path provided, ie "1234/0/123". It will then transform to an bec.PublicKey before +// serialising the bytes and returning. +func (k *ExtendedKey) DerivePublicKeyFromPath(path string) ([]byte, error) { + key, err := k.DeriveChildFromPath(path) + if err != nil { + return nil, err + } + pubKey, err := key.ECPubKey() + if err != nil { + return nil, fmt.Errorf("failed to generate public key %w", err) + } + return pubKey.SerialiseCompressed(), nil +} + +func getChildInt(child string) (uint32, error) { + var suffix uint32 + if strings.HasSuffix(child, "'") { + child = strings.TrimRight(child, "'") + suffix = 2147483648 // 2^32 + } + t, err := strconv.ParseUint(child, 10, 32) + if err != nil { + return 0, fmt.Errorf("failed to get child int %w", err) + } + return uint32(t) + suffix, nil +} diff --git a/bip32/derivationpaths_test.go b/bip32/derivationpaths_test.go index 0ca38a0..a9c5e43 100644 --- a/bip32/derivationpaths_test.go +++ b/bip32/derivationpaths_test.go @@ -101,3 +101,76 @@ func TestDeriveSeed(t *testing.T) { }) } } + +func Test_DeriveChildFromPath(t *testing.T) { + t.Parallel() + tests := map[string]struct { + key *ExtendedKey + path string + expPriv string + expPub string + err error + }{ + "successful run, 1 level child, should return no errors": { + key: func() *ExtendedKey { + k, err := NewKeyFromString("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + assert.NoError(t, err) + return k + }(), + path: "0/1", + expPriv: "xprv9ww7sMFLzJMzy7bV1qs7nGBxgKYrgcm3HcJvGb4yvNhT9vxXC7eX7WVULzCfxucFEn2TsVvJw25hH9d4mchywguGQCZvRgsiRaTY1HCqN8G", + expPub: "xpub6AvUGrnEpfvJBbfx7sQ89Q8hEMPM65UteqEX4yUbUiES2jHfjexmfJoxCGSwFMZiPBaKQT1RiKWrKfuDV4vpgVs4Xn8PpPTR2i79rwHd4Zr", + err: nil, + }, "successful run, 2 level child, should return no errors": { + key: func() *ExtendedKey { + k, err := NewKeyFromString("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + assert.NoError(t, err) + return k + }(), + path: "0/1/100000", + expPriv: "xprv9xrdP7iD2MKJthXr1NiyGJ5KqmD2sLbYYFi49AMq9bXrKJGKBnjx5ivSzXRfLhXxzQNsqCi51oUjniwGemvfAZpzpAGohpzFkat42ohU5bR", + expPub: "xpub6BqyndF6risc7BcK7QFydS24Po3XGoKPuUdewYmShw4qC6bTjL4CdXEvqow6yhsfAtvU8e6kHPNFM2LzeWwKQoJm6hrYttTcxVQrk42WRE3", + err: nil, + }, "successful run, 10 level child, should return no errors": { + key: func() *ExtendedKey { + k, err := NewKeyFromString("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + assert.NoError(t, err) + return k + }(), + path: "0/1/1/1/1/1/1/1/1/2147483647", + expPriv: "xprvAD89K3nZjaG8NqELN8Ce2ATWTcRADLH6JTbrXoVJT6eBRbMwbG7J75v3ym4tGC7X3Mih5krQF77pGi6GNdvxfNcr6WqYacHCSa6uzotoAx2", + expPub: "xpub6S7ViZKTZwpRbKJoU9jePJQF1eFecnzwfgXTLBtv1SBAJPh68oRYetEXq1RvGzsYnTzeikfdM5UM3WDrSZxuBrJi5nLpGxsuSE6cDE8pB2o", + err: nil, + }, "successful run, 1 level, hardened": { + key: func() *ExtendedKey { + k, err := NewKeyFromString("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + assert.NoError(t, err) + return k + }(), + path: "0/1'", + expPriv: "xprv9ww7sMFVKxty8iXvY7Yn2NyvHZ2CgEoAYXmvf2a4XvkhzBUBmYmaMWyjyAhSxgyKK4zYzbJT6hT4JeGW5fFcNaYsBsBR9a8TxVX1LJQiZ1P", + expPub: "xpub6AvUGrnPALTGMCcPe95nPWveqarh5hX1ukhXTQyg6GHgryoLK65puKJDpTcMBKJKdtXQYVwbK3zMgydcTcf5qpLpJcULu9hKUxx5rzgYhrk", + err: nil, + }, "successful run, 3 level, hardened": { + key: func() *ExtendedKey { + k, err := NewKeyFromString("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi") + assert.NoError(t, err) + return k + }(), + path: "10/1'/1000'/15'", + expPriv: "xprvA1bKm9LnkQbMvUW6kwKDLFapT9V9vTeh9D9VnVSJhRf8KmqQTc9W5YboNYcUUkZLreNq1NmeuPpw8x86C87gGyxyV6jNBV4kztFrPdSWz2t", + expPub: "xpub6EagAesgan9f8xaZrxrDhPXZ1BKeKvNYWS56asqvFmC7CaAZ19TkdLvHDrzubSMiC6tAqTMcumVFkgT2duhZncV3KieshEDHNc4jPWkRMGD", + err: nil, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + k, err := test.key.DeriveChildFromPath(test.path) + assert.NoError(t, err) + assert.Equal(t, test.expPriv, k.String()) + pubKey, err := k.Neuter() + assert.NoError(t, err) + assert.Equal(t, test.expPub, pubKey.String()) + }) + } +} From dd0aaaa544247c610143f7c78469ff24e7c6be29 Mon Sep 17 00:00:00 2001 From: Mark Smith Date: Mon, 23 Aug 2021 16:54:14 +0100 Subject: [PATCH 2/3] updating names and cleaning up a wee bit to make clearer new functions --- bip32/derivationpaths.go | 40 ++++++++++++++++++----------------- bip32/derivationpaths_test.go | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/bip32/derivationpaths.go b/bip32/derivationpaths.go index c792a58..d8bf08e 100644 --- a/bip32/derivationpaths.go +++ b/bip32/derivationpaths.go @@ -52,23 +52,25 @@ func DeriveNumber(path string) (uint64, error) { // DeriveKeyFromPath will generate a new extended key derived from the key k using the // bip32 path provided, ie "1234/0/123" -func (k *ExtendedKey) DeriveChildFromPath(path string) (*ExtendedKey, error) { +// Child keys must be ints or hardened keys followed by '. +// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +func (k *ExtendedKey) DeriveKeyFromPath(derivationPath string) (*ExtendedKey, error) { + if derivationPath == "" { + return k, nil + } key := k - if path != "" { - children := strings.Split(path, "/") - for _, child := range children { - if !numericPlusTick.MatchString(child) { - return nil, fmt.Errorf("invalid path: %q", path) - } - childInt, err := getChildInt(child) - if err != nil { - return nil, fmt.Errorf("derive key failed %w", err) - } - var childErr error - key, childErr = key.Child(childInt) - if childErr != nil { - return nil, fmt.Errorf("derive key failed %w", childErr) - } + children := strings.Split(derivationPath, "/") + for _, child := range children { + if !numericPlusTick.MatchString(child) { + return nil, fmt.Errorf("invalid path: %q", derivationPath) + } + childInt, err := childInt(child) + if err != nil { + return nil, fmt.Errorf("derive key failed %w", err) + } + key, err = key.Child(childInt) + if err != nil { + return nil, fmt.Errorf("derive key failed %w", err) } } return key, nil @@ -77,8 +79,8 @@ func (k *ExtendedKey) DeriveChildFromPath(path string) (*ExtendedKey, error) { // DerivePublicKeyFromPath will generate a new extended key derived from the key k using the // bip32 path provided, ie "1234/0/123". It will then transform to an bec.PublicKey before // serialising the bytes and returning. -func (k *ExtendedKey) DerivePublicKeyFromPath(path string) ([]byte, error) { - key, err := k.DeriveChildFromPath(path) +func (k *ExtendedKey) DerivePublicKeyFromPath(derivationPath string) ([]byte, error) { + key, err := k.DeriveKeyFromPath(derivationPath) if err != nil { return nil, err } @@ -89,7 +91,7 @@ func (k *ExtendedKey) DerivePublicKeyFromPath(path string) ([]byte, error) { return pubKey.SerialiseCompressed(), nil } -func getChildInt(child string) (uint32, error) { +func childInt(child string) (uint32, error) { var suffix uint32 if strings.HasSuffix(child, "'") { child = strings.TrimRight(child, "'") diff --git a/bip32/derivationpaths_test.go b/bip32/derivationpaths_test.go index a9c5e43..4636fda 100644 --- a/bip32/derivationpaths_test.go +++ b/bip32/derivationpaths_test.go @@ -165,7 +165,7 @@ func Test_DeriveChildFromPath(t *testing.T) { } for name, test := range tests { t.Run(name, func(t *testing.T) { - k, err := test.key.DeriveChildFromPath(test.path) + k, err := test.key.DeriveKeyFromPath(test.path) assert.NoError(t, err) assert.Equal(t, test.expPriv, k.String()) pubKey, err := k.Neuter() From 61964dbbb29a283f6318d51e1adc5eb66302aacf Mon Sep 17 00:00:00 2001 From: Mark Smith Date: Mon, 23 Aug 2021 17:19:19 +0100 Subject: [PATCH 3/3] name DeriveChildFromPath --- bip32/derivationpaths.go | 6 +++--- bip32/derivationpaths_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bip32/derivationpaths.go b/bip32/derivationpaths.go index d8bf08e..b4ee958 100644 --- a/bip32/derivationpaths.go +++ b/bip32/derivationpaths.go @@ -50,11 +50,11 @@ func DeriveNumber(path string) (uint64, error) { return seed, nil } -// DeriveKeyFromPath will generate a new extended key derived from the key k using the +// DeriveChildFromPath will generate a new extended key derived from the key k using the // bip32 path provided, ie "1234/0/123" // Child keys must be ints or hardened keys followed by '. // https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki -func (k *ExtendedKey) DeriveKeyFromPath(derivationPath string) (*ExtendedKey, error) { +func (k *ExtendedKey) DeriveChildFromPath(derivationPath string) (*ExtendedKey, error) { if derivationPath == "" { return k, nil } @@ -80,7 +80,7 @@ func (k *ExtendedKey) DeriveKeyFromPath(derivationPath string) (*ExtendedKey, er // bip32 path provided, ie "1234/0/123". It will then transform to an bec.PublicKey before // serialising the bytes and returning. func (k *ExtendedKey) DerivePublicKeyFromPath(derivationPath string) ([]byte, error) { - key, err := k.DeriveKeyFromPath(derivationPath) + key, err := k.DeriveChildFromPath(derivationPath) if err != nil { return nil, err } diff --git a/bip32/derivationpaths_test.go b/bip32/derivationpaths_test.go index 4636fda..a9c5e43 100644 --- a/bip32/derivationpaths_test.go +++ b/bip32/derivationpaths_test.go @@ -165,7 +165,7 @@ func Test_DeriveChildFromPath(t *testing.T) { } for name, test := range tests { t.Run(name, func(t *testing.T) { - k, err := test.key.DeriveKeyFromPath(test.path) + k, err := test.key.DeriveChildFromPath(test.path) assert.NoError(t, err) assert.Equal(t, test.expPriv, k.String()) pubKey, err := k.Neuter()