From 3ac488e2c4196350dd18af5f7b37048784d17f86 Mon Sep 17 00:00:00 2001 From: mrz1836 Date: Thu, 1 Oct 2020 16:04:22 -0400 Subject: [PATCH] Added more HD key methods, examples & tests --- README.md | 2 + .../address_from_private_key.go | 2 +- examples/create_pubkey/create_pubkey.go | 2 +- .../get_private_key_for_path.go | 28 +++ .../private_key_to_wif/private_key_to_wif.go | 3 +- .../script_from_address.go | 2 +- examples/sign_message/sign_message.go | 2 +- hd_key.go | 34 ++++ hd_key_test.go | 190 ++++++++++++++++++ 9 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 examples/get_private_key_for_path/get_private_key_for_path.go diff --git a/README.md b/README.md index 4701c60..3d8f5d0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ View the generated [documentation](https://pkg.go.dev/github.com/bitcoinschema/g - [WIF to PrivateKey](private_key.go) - [PrivateKey to WIF](private_key.go) - [Generate HD Keys](hd_key.go) +- [Get PrivateKey by Path](hd_key.go) +- [Get HD Key by Path](hd_key.go) ### ToDo - Support `testnet` addresses & keys diff --git a/examples/address_from_private_key/address_from_private_key.go b/examples/address_from_private_key/address_from_private_key.go index c5ff318..d03a99a 100644 --- a/examples/address_from_private_key/address_from_private_key.go +++ b/examples/address_from_private_key/address_from_private_key.go @@ -8,7 +8,7 @@ import ( func main() { - // Start with a private key + // Start with a private key (we will make one for this example) privateKey, err := bitcoin.CreatePrivateKeyString() if err != nil { log.Fatalf("error occurred: %s", err.Error()) diff --git a/examples/create_pubkey/create_pubkey.go b/examples/create_pubkey/create_pubkey.go index 622e8d7..fa191d8 100644 --- a/examples/create_pubkey/create_pubkey.go +++ b/examples/create_pubkey/create_pubkey.go @@ -8,7 +8,7 @@ import ( func main() { - // Start with a private key + // Start with a private key (we will make one for this example) privateKey, err := bitcoin.CreatePrivateKeyString() if err != nil { log.Fatalf("error occurred: %s", err.Error()) diff --git a/examples/get_private_key_for_path/get_private_key_for_path.go b/examples/get_private_key_for_path/get_private_key_for_path.go new file mode 100644 index 0000000..5e207dc --- /dev/null +++ b/examples/get_private_key_for_path/get_private_key_for_path.go @@ -0,0 +1,28 @@ +package main + +import ( + "encoding/hex" + "log" + + "github.com/bitcoinschema/go-bitcoin" + "github.com/bitcoinsv/bsvd/bsvec" +) + +func main() { + + // Start with an HD key (we will make one for this example) + hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + if err != nil { + log.Fatalf("error occurred: %s", err.Error()) + } + + // Get a private key from a specific path (chain/num) + var privateKey *bsvec.PrivateKey + privateKey, err = bitcoin.GetPrivateKeyByPath(hdKey, 10, 2) + if err != nil { + log.Fatalf("error occurred: %s", err.Error()) + } + + // Success! + log.Printf("private key: %s for chain/path: %d/%d", hex.EncodeToString(privateKey.Serialize()), 10, 2) +} diff --git a/examples/private_key_to_wif/private_key_to_wif.go b/examples/private_key_to_wif/private_key_to_wif.go index 704dcf3..e27716c 100644 --- a/examples/private_key_to_wif/private_key_to_wif.go +++ b/examples/private_key_to_wif/private_key_to_wif.go @@ -7,7 +7,8 @@ import ( ) func main() { - // Start with a private key + + // Start with a private key (we will make one for this example) privateKey, err := bitcoin.CreatePrivateKeyString() if err != nil { log.Fatalf("error occurred: %s", err.Error()) diff --git a/examples/script_from_address/script_from_address.go b/examples/script_from_address/script_from_address.go index 568ef5b..c4223d7 100644 --- a/examples/script_from_address/script_from_address.go +++ b/examples/script_from_address/script_from_address.go @@ -7,7 +7,7 @@ import ( ) func main() { - // Start with a private key + // Start with a private key (we will make one for this example) privateKey, err := bitcoin.CreatePrivateKeyString() if err != nil { log.Fatalf("error occurred: %s", err.Error()) diff --git a/examples/sign_message/sign_message.go b/examples/sign_message/sign_message.go index 63bca2f..e358b05 100644 --- a/examples/sign_message/sign_message.go +++ b/examples/sign_message/sign_message.go @@ -7,7 +7,7 @@ import ( ) func main() { - // Create a private key for the example + // Create a private key (we will make one for this example) privateKey, err := bitcoin.CreatePrivateKeyString() if err != nil { log.Fatalf("error occurred: %s", err.Error()) diff --git a/hd_key.go b/hd_key.go index b5d5f54..4cf7d88 100644 --- a/hd_key.go +++ b/hd_key.go @@ -1,6 +1,9 @@ package bitcoin import ( + "errors" + + "github.com/bitcoinsv/bsvd/bsvec" "github.com/bitcoinsv/bsvd/chaincfg" "github.com/bitcoinsv/bsvutil/hdkeychain" ) @@ -55,3 +58,34 @@ func GenerateHDKeyPair(seedLength uint8) (xPrivateKey, xPublicKey string, err er return } + +// GetHDKeyByPath gets the corresponding HD key from a chain/num path +func GetHDKeyByPath(hdKey *hdkeychain.ExtendedKey, chain, num uint32) (*hdkeychain.ExtendedKey, error) { + + // Make sure we have a valid key + if hdKey == nil { + return nil, errors.New("hdKey is nil") + } + + // Derive the child key from the chain path + childKeyChain, err := hdKey.Child(chain) + if err != nil { + return nil, err + } + + // Get the child key from the num path + return childKeyChain.Child(num) +} + +// GetPrivateKeyByPath gets the key for a given derivation path (chain/num) +func GetPrivateKeyByPath(hdKey *hdkeychain.ExtendedKey, chain, num uint32) (*bsvec.PrivateKey, error) { + + // Get the child key from the num & chain + childKeyNum, err := GetHDKeyByPath(hdKey, chain, num) + if err != nil { + return nil, err + } + + // Get the private key + return childKeyNum.ECPrivKey() +} diff --git a/hd_key_test.go b/hd_key_test.go index 1db6fb4..bc80e51 100644 --- a/hd_key_test.go +++ b/hd_key_test.go @@ -1,8 +1,12 @@ package bitcoin import ( + "encoding/hex" "fmt" "testing" + + "github.com/bitcoinsv/bsvd/bsvec" + "github.com/bitcoinsv/bsvutil/hdkeychain" ) // TestGenerateHDKey will test the method GenerateHDKey() @@ -123,3 +127,189 @@ func BenchmarkGenerateHDKeyPairSecure(b *testing.B) { _, _, _ = GenerateHDKeyPair(SecureSeedLength) } } + +// TestGetPrivateKeyByPath will test the method GetPrivateKeyByPath() +func TestGetPrivateKeyByPath(t *testing.T) { + + t.Parallel() + + // Generate a valid key + validKey, err := GenerateHDKey(RecommendedSeedLength) + if err != nil { + t.Fatalf("error occurred: %s", err.Error()) + } + + // Max depth key + var maxKey *hdkeychain.ExtendedKey + maxKey, err = GetHDKeyByPath(validKey, 1<<9, 1<<9) + if err != nil { + t.Fatalf("error occurred: %s", err.Error()) + } + + // Test depth limit + for i := 0; i < 1<<8-1; i++ { + maxKey, err = GetHDKeyByPath(maxKey, uint32(i), uint32(i)) + if err != nil { + t.Log("hit the depth limit on HD key") + break + } + } + + // Create the list of tests + var tests = []struct { + inputHDKey *hdkeychain.ExtendedKey + inputChain uint32 + inputNum uint32 + expectedNil bool + expectedError bool + }{ + {nil, 0, 0, true, true}, + {validKey, 0, 0, false, false}, + {validKey, 10, 10, false, false}, + {validKey, 100, 100, false, false}, + {validKey, 2 ^ 31 + 1, 2 ^ 32 - 1, false, false}, + {validKey, 1 << 8, 1 << 8, false, false}, + {validKey, 1 << 9, 1 << 9, false, false}, + {validKey, 1 << 10, 1 << 10, false, false}, + {validKey, 1 << 11, 1 << 11, false, false}, + {validKey, 1 << 12, 1 << 12, false, false}, + {validKey, 1 << 16, 1 << 16, false, false}, + {validKey, 1<<32 - 1, 1<<32 - 1, false, false}, + } + + // Run tests + for _, test := range tests { + if privateKey, err := GetPrivateKeyByPath(test.inputHDKey, test.inputChain, test.inputNum); err != nil && !test.expectedError { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and error not expected but got: %s", t.Name(), test.inputHDKey, test.inputChain, test.inputNum, err.Error()) + } else if err == nil && test.expectedError { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and error was expected", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if privateKey == nil && !test.expectedNil { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and was nil but not expected", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if privateKey != nil && test.expectedNil { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and was NOT nil but expected to be nil", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if privateKey != nil && len(hex.EncodeToString(privateKey.Serialize())) == 0 { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and should not be empty", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } + } +} + +// ExampleGetPrivateKeyByPath example using GetPrivateKeyByPath() +func ExampleGetPrivateKeyByPath() { + + hdKey, err := GenerateHDKey(SecureSeedLength) + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + + // Get a private key at the path + var privateKey *bsvec.PrivateKey + privateKey, err = GetPrivateKeyByPath(hdKey, 0, 1) + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + fmt.Printf("private key (%d) found at path %d/%d", len(privateKey.Serialize()), 0, 1) + // Output:private key (32) found at path 0/1 +} + +// BenchmarkGetPrivateKeyByPath benchmarks the method GetPrivateKeyByPath() +func BenchmarkGetPrivateKeyByPath(b *testing.B) { + hdKey, _ := GenerateHDKey(SecureSeedLength) + for i := 0; i < b.N; i++ { + _, _ = GetPrivateKeyByPath(hdKey, 0, 1) + } +} + +// TestGetHDKeyByPath will test the method GetHDKeyByPath() +func TestGetHDKeyByPath(t *testing.T) { + + t.Parallel() + + // Generate a valid key + validKey, err := GenerateHDKey(RecommendedSeedLength) + if err != nil { + t.Fatalf("error occurred: %s", err.Error()) + } + + // Max depth key + var maxKey *hdkeychain.ExtendedKey + maxKey, err = GetHDKeyByPath(validKey, 1<<9, 1<<9) + if err != nil { + t.Fatalf("error occurred: %s", err.Error()) + } + + // Test depth limit + for i := 0; i < 1<<8-1; i++ { + maxKey, err = GetHDKeyByPath(maxKey, uint32(i), uint32(i)) + if err != nil { + t.Log("hit the depth limit on HD key") + break + } + } + + // Create the list of tests + var tests = []struct { + inputHDKey *hdkeychain.ExtendedKey + inputChain uint32 + inputNum uint32 + expectedNil bool + expectedError bool + }{ + {nil, 0, 0, true, true}, + {validKey, 0, 0, false, false}, + {validKey, 10, 10, false, false}, + {validKey, 100, 100, false, false}, + {validKey, 2 ^ 31 + 1, 2 ^ 32 - 1, false, false}, + {validKey, 1 << 8, 1 << 8, false, false}, + {validKey, 1 << 9, 1 << 9, false, false}, + {validKey, 1 << 10, 1 << 10, false, false}, + {validKey, 1 << 11, 1 << 11, false, false}, + {validKey, 1 << 12, 1 << 12, false, false}, + {validKey, 1 << 16, 1 << 16, false, false}, + {validKey, 1<<32 - 1, 1<<32 - 1, false, false}, + } + + // Run tests + for _, test := range tests { + if hdKey, err := GetHDKeyByPath(test.inputHDKey, test.inputChain, test.inputNum); err != nil && !test.expectedError { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and error not expected but got: %s", t.Name(), test.inputHDKey, test.inputChain, test.inputNum, err.Error()) + } else if err == nil && test.expectedError { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and error was expected", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if hdKey == nil && !test.expectedNil { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and was nil but not expected", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if hdKey != nil && test.expectedNil { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and was NOT nil but expected to be nil", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } else if hdKey != nil && len(hdKey.String()) == 0 { + t.Errorf("%s Failed: [%v] [%d] [%d] inputted and should not be empty", t.Name(), test.inputHDKey, test.inputChain, test.inputNum) + } + } +} + +// ExampleGetPrivateKeyByPath example using GetHDKeyByPath() +func ExampleGetHDKeyByPath() { + + hdKey, err := GenerateHDKey(SecureSeedLength) + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + + // Get a child key + var childKey *hdkeychain.ExtendedKey + childKey, err = GetHDKeyByPath(hdKey, 0, 1) + if err != nil { + fmt.Printf("error occurred: %s", err.Error()) + return + } + fmt.Printf("hd key (%d) found at path %d/%d", len(childKey.String()), 0, 1) + // Output:hd key (111) found at path 0/1 +} + +// BenchmarkGetPrivateKeyByPath benchmarks the method GetHDKeyByPath() +func BenchmarkGetHDKeyByPath(b *testing.B) { + hdKey, _ := GenerateHDKey(SecureSeedLength) + for i := 0; i < b.N; i++ { + _, _ = GetHDKeyByPath(hdKey, 0, 1) + } +}