From 6df34392fb47b448c0fa7aacde517bf83a17b337 Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Tue, 26 Nov 2024 13:08:24 +0100 Subject: [PATCH] add command to derive child key from master public key --- covenant-signer/cmd/derivechildkeycmd.go | 32 +++++ covenant-signer/keyutils/bip32deriv.go | 131 ++++++++++++++++++++ covenant-signer/keyutils/bip32deriv_test.go | 82 ++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 covenant-signer/cmd/derivechildkeycmd.go create mode 100644 covenant-signer/keyutils/bip32deriv.go create mode 100644 covenant-signer/keyutils/bip32deriv_test.go diff --git a/covenant-signer/cmd/derivechildkeycmd.go b/covenant-signer/cmd/derivechildkeycmd.go new file mode 100644 index 0000000..c8ed9fb --- /dev/null +++ b/covenant-signer/cmd/derivechildkeycmd.go @@ -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 + }, +} diff --git a/covenant-signer/keyutils/bip32deriv.go b/covenant-signer/keyutils/bip32deriv.go new file mode 100644 index 0000000..bbc34ae --- /dev/null +++ b/covenant-signer/keyutils/bip32deriv.go @@ -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 +} diff --git a/covenant-signer/keyutils/bip32deriv_test.go b/covenant-signer/keyutils/bip32deriv_test.go new file mode 100644 index 0000000..6df5cd4 --- /dev/null +++ b/covenant-signer/keyutils/bip32deriv_test.go @@ -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) + }) + } +}