Skip to content

Commit

Permalink
Added more HD key methods, examples & tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mrz1836 committed Oct 1, 2020
1 parent b118de3 commit 3ac488e
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion examples/create_pubkey/create_pubkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
28 changes: 28 additions & 0 deletions examples/get_private_key_for_path/get_private_key_for_path.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 2 additions & 1 deletion examples/private_key_to_wif/private_key_to_wif.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion examples/script_from_address/script_from_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion examples/sign_message/sign_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
34 changes: 34 additions & 0 deletions hd_key.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package bitcoin

import (
"errors"

"github.com/bitcoinsv/bsvd/bsvec"
"github.com/bitcoinsv/bsvd/chaincfg"
"github.com/bitcoinsv/bsvutil/hdkeychain"
)
Expand Down Expand Up @@ -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()
}
190 changes: 190 additions & 0 deletions hd_key_test.go
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 3ac488e

Please sign in to comment.