-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add command to derive child key from master public key
- Loading branch information
1 parent
4bfd897
commit 6df3439
Showing
3 changed files
with
245 additions
and
0 deletions.
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 | ||
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) | ||
}) | ||
} | ||
} |