Skip to content

Commit

Permalink
add command to derive child key from master public key
Browse files Browse the repository at this point in the history
  • Loading branch information
KonradStaniec committed Nov 26, 2024
1 parent 4bfd897 commit 6df3439
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 0 deletions.
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

}

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
}
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)
})
}
}

0 comments on commit 6df3439

Please sign in to comment.