From 0425d5c2cd0b3cee160fdd854502747b24a85509 Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Thu, 30 Nov 2023 17:00:18 -0800 Subject: [PATCH] add keys import (#5) * add keys import * add keys import * add to keys --- pkg/operator/keys.go | 1 + pkg/operator/keys/create_test.go | 2 +- pkg/operator/keys/{errors.go => error.go} | 0 pkg/operator/keys/import.go | 110 +++++++++++++ pkg/operator/keys/import_test.go | 187 ++++++++++++++++++++++ 5 files changed, 299 insertions(+), 1 deletion(-) rename pkg/operator/keys/{errors.go => error.go} (100%) create mode 100644 pkg/operator/keys/import.go create mode 100644 pkg/operator/keys/import_test.go diff --git a/pkg/operator/keys.go b/pkg/operator/keys.go index 9b22d53..021f8e8 100644 --- a/pkg/operator/keys.go +++ b/pkg/operator/keys.go @@ -13,6 +13,7 @@ func KeysCmd(p utils.Prompter) *cli.Command { Subcommands: []*cli.Command{ keys.CreateCmd(p), keys.ListCmd(), + keys.ImportCmd(p), }, } diff --git a/pkg/operator/keys/create_test.go b/pkg/operator/keys/create_test.go index f8d6697..e82cca2 100644 --- a/pkg/operator/keys/create_test.go +++ b/pkg/operator/keys/create_test.go @@ -49,7 +49,7 @@ func TestCreateCmd(t *testing.T) { err: ErrKeyContainsWhitespaces, }, { - name: "invalid keytype", + name: "invalid key type", args: []string{"--key-type", "invalid", "do_not_use_this_name"}, err: ErrInvalidKeyType, }, diff --git a/pkg/operator/keys/errors.go b/pkg/operator/keys/error.go similarity index 100% rename from pkg/operator/keys/errors.go rename to pkg/operator/keys/error.go diff --git a/pkg/operator/keys/import.go b/pkg/operator/keys/import.go new file mode 100644 index 0000000..532d5a8 --- /dev/null +++ b/pkg/operator/keys/import.go @@ -0,0 +1,110 @@ +package keys + +import ( + "fmt" + "math/big" + "regexp" + "strings" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/crypto/bls" + "github.com/ethereum/go-ethereum/crypto" + "github.com/urfave/cli/v2" +) + +func ImportCmd(p utils.Prompter) *cli.Command { + importCmd := &cli.Command{ + Name: "import", + Usage: "Used to import existing keys in local keystore", + UsageText: "import --key-type [flags] ", + Description: ` +Used to import ecdsa and bls key in local keystore + +keyname (required) - This will be the name of the imported key file. It will be saved as .ecdsa.key.json or .bls.key.json + +use --key-type ecdsa/bls to import ecdsa/bls key. +- ecdsa - should be plaintext hex encoded private key +- bls - should be plaintext bls private key + +It will prompt for password to encrypt the key, which is optional but highly recommended. +If you want to import a key with weak/no password, use --insecure flag. Do NOT use those keys in production + +This command will import keys in $HOME/.eigenlayer/operator_keys/ location + `, + Flags: []cli.Flag{ + &KeyTypeFlag, + &InsecureFlag, + }, + + Action: func(ctx *cli.Context) error { + args := ctx.Args() + if args.Len() != 2 { + return fmt.Errorf("%w: accepts 2 arg, received %d", ErrInvalidNumberOfArgs, args.Len()) + } + + keyName := args.Get(0) + if err := validateKeyName(keyName); err != nil { + return err + } + + privateKey := args.Get(1) + if err := validatePrivateKey(privateKey); err != nil { + return err + } + + keyType := ctx.String(KeyTypeFlag.Name) + insecure := ctx.Bool(InsecureFlag.Name) + + switch keyType { + case KeyTypeECDSA: + privateKey = strings.TrimPrefix(privateKey, "0x") + privateKeyPair, err := crypto.HexToECDSA(privateKey) + if err != nil { + return err + } + return saveEcdsaKey(keyName, p, privateKeyPair, insecure) + case KeyTypeBLS: + privateKeyBigInt := new(big.Int) + _, ok := privateKeyBigInt.SetString(privateKey, 10) + blsKeyPair := new(bls.KeyPair) + var err error + if ok { + fmt.Println("Importing from large integer") + blsKeyPair, err = bls.NewKeyPairFromString(privateKey) + if err != nil { + return err + } + } else { + // Try to parse as hex + fmt.Println("Importing from hex") + z := new(big.Int) + privateKey = strings.TrimPrefix(privateKey, "0x") + _, ok := z.SetString(privateKey, 16) + if !ok { + return ErrInvalidHexPrivateKey + } + blsKeyPair, err = bls.NewKeyPairFromString(z.String()) + if err != nil { + return err + } + } + return saveBlsKey(keyName, p, blsKeyPair, insecure) + default: + return ErrInvalidKeyType + } + }, + } + return importCmd +} + +func validatePrivateKey(pk string) error { + if len(pk) == 0 { + return ErrEmptyPrivateKey + } + + if match, _ := regexp.MatchString("\\s", pk); match { + return ErrPrivateKeyContainsWhitespaces + } + + return nil +} diff --git a/pkg/operator/keys/import_test.go b/pkg/operator/keys/import_test.go new file mode 100644 index 0000000..726e62b --- /dev/null +++ b/pkg/operator/keys/import_test.go @@ -0,0 +1,187 @@ +package keys + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/urfave/cli/v2" + + "github.com/Layr-Labs/eigensdk-go/crypto/bls" + + prompterMock "github.com/Layr-Labs/eigenlayer-cli/pkg/utils/mocks" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestImportCmd(t *testing.T) { + homePath, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + args []string + err error + keyPath string + expectedPrivKey string + promptMock func(p *prompterMock.MockPrompter) + }{ + { + name: "key-name flag not set", + args: []string{}, + err: errors.New("Required flag \"key-type\" not set"), + }, + { + name: "one argument", + args: []string{"--key-type", "ecdsa", "arg1"}, + err: fmt.Errorf("%w: accepts 2 arg, received 1", ErrInvalidNumberOfArgs), + }, + + { + name: "more than two argument", + args: []string{"--key-type", "ecdsa", "arg1", "arg2", "arg3"}, + err: fmt.Errorf("%w: accepts 2 arg, received 3", ErrInvalidNumberOfArgs), + }, + { + name: "empty key name argument", + args: []string{"--key-type", "ecdsa", "", ""}, + err: ErrEmptyKeyName, + }, + { + name: "keyname with whitespaces", + args: []string{"--key-type", "ecdsa", "hello world", ""}, + err: ErrKeyContainsWhitespaces, + }, + { + name: "empty private key argument", + args: []string{"--key-type", "ecdsa", "hello", ""}, + err: ErrEmptyPrivateKey, + }, + { + name: "keyname with whitespaces", + args: []string{"--key-type", "ecdsa", "hello", "hello world"}, + err: ErrPrivateKeyContainsWhitespaces, + }, + { + name: "invalid key type", + args: []string{"--key-type", "invalid", "hello", "privkey"}, + err: ErrInvalidKeyType, + }, + { + name: "invalid password based on validation function - ecdsa", + args: []string{"--key-type", "ecdsa", "test", "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"}, + err: ErrInvalidPassword, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", ErrInvalidPassword) + }, + }, + { + name: "invalid password based on validation function - bls", + args: []string{"--key-type", "bls", "test", "123"}, + err: ErrInvalidPassword, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", ErrInvalidPassword) + }, + }, + { + name: "valid ecdsa key import", + args: []string{"--key-type", "ecdsa", "test", "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"}, + err: nil, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil) + }, + expectedPrivKey: "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6", + keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.ecdsa.key.json"), + }, + { + name: "valid ecdsa key import with 0x prefix", + args: []string{"--key-type", "ecdsa", "test", "0x6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"}, + err: nil, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil) + }, + expectedPrivKey: "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6", + keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.ecdsa.key.json"), + }, + { + name: "valid bls key import", + args: []string{"--key-type", "bls", "test", "20030410000080487431431153104351076122223465926814327806350179952713280726583"}, + err: nil, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil) + }, + expectedPrivKey: "20030410000080487431431153104351076122223465926814327806350179952713280726583", + keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"), + }, + { + name: "valid bls key import for hex key", + args: []string{"--key-type", "bls", "test", "0xfe198b992d97545b3b0174f026f781039f167c13f6d0ce9f511d0d2e973b7f02"}, + err: nil, + promptMock: func(p *prompterMock.MockPrompter) { + p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil) + }, + expectedPrivKey: "5491383829988096583828972342810831790467090979842721151380259607665538989821", + keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"), + }, + { + name: "invalid bls key import for hex key", + args: []string{"--key-type", "bls", "test", "0xfes"}, + err: ErrInvalidHexPrivateKey, + keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + _ = os.Remove(tt.keyPath) + }) + controller := gomock.NewController(t) + p := prompterMock.NewMockPrompter(controller) + if tt.promptMock != nil { + tt.promptMock(p) + } + + importCmd := ImportCmd(p) + app := cli.NewApp() + + // We do this because the in the parsing of arguments it ignores the first argument + // for commands, so we add a blank string as the first argument + // I suspect it does this because it is expecting the first argument to be the name of the command + // But when we are testing the command, we don't want to have to specify the name of the command + // since we are creating the command ourselves + // https://github.com/urfave/cli/blob/c023d9bc5a3122830c9355a0a8c17137e0c8556f/command.go#L323 + args := append([]string{""}, tt.args...) + cCtx := cli.NewContext(app, nil, &cli.Context{Context: context.Background()}) + err := importCmd.Run(cCtx, args...) + + if tt.err == nil { + assert.NoError(t, err) + _, err := os.Stat(tt.keyPath) + + // Check if the error indicates that the file does not exist + if os.IsNotExist(err) { + assert.Failf(t, "file does not exist", "file %s does not exist", tt.keyPath) + } + + if tt.args[1] == KeyTypeECDSA { + key, err := GetECDSAPrivateKey(tt.keyPath, "") + assert.NoError(t, err) + assert.Equal(t, strings.Trim(tt.args[3], "0x"), hex.EncodeToString(key.D.Bytes())) + } else if tt.args[1] == KeyTypeBLS { + key, err := bls.ReadPrivateKeyFromFile(tt.keyPath, "") + assert.NoError(t, err) + assert.Equal(t, tt.expectedPrivKey, key.PrivKey.String()) + } + } else { + assert.EqualError(t, err, tt.err.Error()) + } + }) + } +}