Skip to content
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
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

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
KonradStaniec committed Nov 26, 2024
commit a01a2cd0ac595ea5f3c4c05ba7760055450d2f19
32 changes: 32 additions & 0 deletions covenant-signer/cmd/derivechildkeycmd.go
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
},
}
131 changes: 131 additions & 0 deletions covenant-signer/keyutils/bip32deriv.go
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

}
Comment on lines +54 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
}
func ParsePath(path string) ([]uint32, error) {
const expectedDepth = 5
parsers := []func(string) (uint32, error){
parseHardened,
parseHardened,
parseHardened,
parseNormal,
parseNormal,
}
splitted := strings.Split(path, "/")
if len(splitted) != expectedDepth {
return nil, fmt.Errorf("invalid derivation path length")
}
result := make([]uint32, expectedDepth)
for i, part := range splitted {
parsed, err := parsers[i](part)
if err != nil {
return nil, fmt.Errorf("invalid derivation path element at index %d: %w", i, err)
}
result[i] = parsed
}
return result, nil
}

Copy link
Member

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

Copy link
Collaborator Author

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.


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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var keyResult *hdkeychain.ExtendedKey = parsedMasterKey
keyResult := parsedMasterKey

Copy link
Member

Choose a reason for hiding this comment

The 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
}
82 changes: 82 additions & 0 deletions covenant-signer/keyutils/bip32deriv_test.go
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)
})
}
}