-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add command to derive child key from master public key #44
Merged
KonradStaniec
merged 4 commits into
main
from
konradstaniec/add-command-to-derive-child-private-key
Nov 26, 2024
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next
Next commit
add command to derive child key from master public key
- Loading branch information
commit a01a2cd0ac595ea5f3c4c05ba7760055450d2f19
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/babylonlabs-io/covenant-emulator/covenant-signer/keyutils" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func init() { | ||
rootCmd.AddCommand(deriveChildKeyCmd) | ||
} | ||
|
||
var deriveChildKeyCmd = &cobra.Command{ | ||
Use: "derive-child-key [master-key] [derivation-path]", | ||
Args: cobra.ExactArgs(2), | ||
Short: "derives a child key from a master key", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
masterKey := args[0] | ||
derivationPath := args[1] | ||
|
||
result, err := keyutils.DeriveChildKey(masterKey, derivationPath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Printf("Derived private key: %s\n", result.PrivateKey) | ||
fmt.Printf("Derived public key: %s\n", result.PublicKey) | ||
|
||
return nil | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,131 @@ | ||||||
package keyutils | ||||||
|
||||||
import ( | ||||||
"encoding/hex" | ||||||
"fmt" | ||||||
"strconv" | ||||||
"strings" | ||||||
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain" | ||||||
) | ||||||
|
||||||
// 84h/1h/0h/0/0 | ||||||
|
||||||
const ( | ||||||
HardenedPostfix = "h" | ||||||
ExpectedDerivationDepth = 5 | ||||||
) | ||||||
|
||||||
func parseHardened(elem string) (uint32, error) { | ||||||
// valid hardened element is at least 2 characters example: 0h | ||||||
if len(elem) < 2 { | ||||||
return 0, fmt.Errorf("invalid hardened element: %s", elem) | ||||||
} | ||||||
|
||||||
number := strings.TrimSuffix(elem, HardenedPostfix) | ||||||
|
||||||
// if the element is unchanged, it means it did not end with correct suffix | ||||||
if number == elem { | ||||||
return 0, fmt.Errorf("invalid hardened element") | ||||||
} | ||||||
|
||||||
parsedNum, err := strconv.ParseUint(number, 10, 32) | ||||||
if err != nil { | ||||||
return 0, fmt.Errorf("invalid hardened element") | ||||||
} | ||||||
|
||||||
numAsUint32 := uint32(parsedNum) | ||||||
|
||||||
// todo check for overflow | ||||||
return hdkeychain.HardenedKeyStart + numAsUint32, nil | ||||||
} | ||||||
|
||||||
func parseNormal(elem string) (uint32, error) { | ||||||
if len(elem) == 0 { | ||||||
return 0, fmt.Errorf("invalid normal element") | ||||||
} | ||||||
|
||||||
parsedNum, err := strconv.ParseUint(elem, 10, 32) | ||||||
if err != nil { | ||||||
return 0, fmt.Errorf("invalid normal element") | ||||||
} | ||||||
|
||||||
return uint32(parsedNum), nil | ||||||
} | ||||||
|
||||||
func ParsePath(path string) ([]uint32, error) { | ||||||
splitted := strings.Split(path, "/") | ||||||
|
||||||
if len(splitted) != ExpectedDerivationDepth { | ||||||
return nil, fmt.Errorf("invalid derivation path length") | ||||||
} | ||||||
|
||||||
h1, err := parseHardened(splitted[0]) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path element at index 0: %w", err) | ||||||
} | ||||||
|
||||||
h2, err := parseHardened(splitted[1]) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path element at index 1: %w", err) | ||||||
} | ||||||
|
||||||
h3, err := parseHardened(splitted[2]) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path element at index 2: %w", err) | ||||||
} | ||||||
|
||||||
n4, err := parseNormal(splitted[3]) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path element at index 3: %w", err) | ||||||
} | ||||||
|
||||||
n5, err := parseNormal(splitted[4]) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path element at index 4: %w", err) | ||||||
} | ||||||
|
||||||
return []uint32{h1, h2, h3, n4, n5}, nil | ||||||
|
||||||
} | ||||||
|
||||||
type DerivationResult struct { | ||||||
PrivateKey string | ||||||
PublicKey string | ||||||
} | ||||||
|
||||||
// DeriveChildKey derives a child key from a master key using a derivation path | ||||||
// masterKey is a base58 encoded master key | ||||||
// childPath is a derivation path in the format "84h/1h/0h/0/0", it must be 5 elements long | ||||||
func DeriveChildKey(masterKey string, childPath string) (*DerivationResult, error) { | ||||||
parsedMasterKey, err := hdkeychain.NewKeyFromString(masterKey) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid master key: %w", err) | ||||||
} | ||||||
|
||||||
derivationPath, err := ParsePath(childPath) | ||||||
|
||||||
if err != nil { | ||||||
return nil, fmt.Errorf("invalid derivation path: %w", err) | ||||||
} | ||||||
|
||||||
var keyResult *hdkeychain.ExtendedKey = parsedMasterKey | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a bit less verbose |
||||||
for _, elem := range derivationPath { | ||||||
keyResult, err = keyResult.Derive(elem) | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("failed to derive child key: %w", err) | ||||||
} | ||||||
} | ||||||
|
||||||
privKey, err := keyResult.ECPrivKey() | ||||||
if err != nil { | ||||||
return nil, fmt.Errorf("failed to get private key: %w", err) | ||||||
} | ||||||
|
||||||
pubKey := privKey.PubKey() | ||||||
|
||||||
return &DerivationResult{ | ||||||
PrivateKey: hex.EncodeToString(privKey.Serialize()), | ||||||
PublicKey: hex.EncodeToString(pubKey.SerializeCompressed()), | ||||||
}, nil | ||||||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package keyutils | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
var parsePathTests = []struct { | ||
name string | ||
path string | ||
expected []uint32 | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "valid hardened and non-hardened path", | ||
path: "84h/1h/0h/0/0", | ||
expected: []uint32{0x80000054, 0x80000001, 0x80000000, 0, 0}, | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
func TestParsePath(t *testing.T) { | ||
for _, tt := range parsePathTests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := ParsePath(tt.path) | ||
|
||
if tt.wantErr { | ||
require.Error(t, err) | ||
return | ||
} else { | ||
require.NoError(t, err) | ||
} | ||
|
||
if len(got) != len(tt.expected) { | ||
t.Errorf("ParsePath() got length = %v, want length = %v", len(got), len(tt.expected)) | ||
return | ||
} | ||
for i := range got { | ||
if got[i] != tt.expected[i] { | ||
t.Errorf("ParsePath() got[%d] = %v, want[%d] = %v", i, got[i], i, tt.expected[i]) | ||
} | ||
} | ||
}) | ||
} | ||
} | ||
|
||
var deriveKeyTests = []struct { | ||
name string | ||
masterPrivKey string | ||
derivationPath string | ||
expectedPublicKey string | ||
}{ | ||
{ | ||
name: "derive from master at index 0", | ||
masterPrivKey: "tprv8ZgxMBicQKsPdNsenC9sBio2D65FssTr2hz7eeeLqTZbvYe8v6BbTjzctbhhSTtFu1QCFMC1Ag58cokSC9q8E68w71nZDqkFnxNdeAXVsG9", | ||
derivationPath: "84h/1h/0h/0/0", | ||
expectedPublicKey: "026f1738cc40f1a67b0726d8a3184277a1137422cfdb0d888a0dfdf69b907f8840", | ||
}, | ||
{ | ||
name: "derive from master at index 0 from new private key", | ||
masterPrivKey: "tprv8ZgxMBicQKsPecqD8mUyo6R18nZE2KyUJGudL3gFsYApS6wYd35sYw6wS2rWHuFakSP6Z9k4NgmP93JvJGbwf4Cc1o7bQWcsUgR4mELA91q", | ||
derivationPath: "84h/1h/0h/0/0", | ||
expectedPublicKey: "02a81acd3457a7f622ab8c5800f0afd21a58a0dc2f35cefb1c623bc0033b012554", | ||
}, | ||
{ | ||
name: "derive from master at index 7", | ||
masterPrivKey: "tprv8ZgxMBicQKsPecqD8mUyo6R18nZE2KyUJGudL3gFsYApS6wYd35sYw6wS2rWHuFakSP6Z9k4NgmP93JvJGbwf4Cc1o7bQWcsUgR4mELA91q", | ||
derivationPath: "84h/1h/0h/0/7", | ||
expectedPublicKey: "030c1362c11495d10247ad916db47504a0d4704fae2951b9bdc460edbd3c4df54b", | ||
}, | ||
} | ||
|
||
func TestDeriveKey(t *testing.T) { | ||
for _, tt := range deriveKeyTests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := DeriveChildKey(tt.masterPrivKey, tt.derivationPath) | ||
require.NoError(t, err) | ||
require.Equal(t, tt.expectedPublicKey, got.PublicKey) | ||
}) | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion, you don't have to accept it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh tbh I find this flow a bit convoluted
Current is a bit verbose, but it is super simple. (though it may be one of those things that everybody sees a bit different)
I will leave it as it is.