Skip to content

Commit

Permalink
add keys import (#5)
Browse files Browse the repository at this point in the history
* add keys import

* add keys import

* add to keys
  • Loading branch information
shrimalmadhur committed Dec 1, 2023
1 parent 4e75fed commit 0425d5c
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 1 deletion.
1 change: 1 addition & 0 deletions pkg/operator/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func KeysCmd(p utils.Prompter) *cli.Command {
Subcommands: []*cli.Command{
keys.CreateCmd(p),
keys.ListCmd(),
keys.ImportCmd(p),
},
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/operator/keys/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
File renamed without changes.
110 changes: 110 additions & 0 deletions pkg/operator/keys/import.go
Original file line number Diff line number Diff line change
@@ -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 <key-type> [flags] <keyname> <private-key>",
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 <keyname>.ecdsa.key.json or <keyname>.bls.key.json
use --key-type ecdsa/bls to import ecdsa/bls key.
- ecdsa - <private-key> should be plaintext hex encoded private key
- bls - <private-key> 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
}
187 changes: 187 additions & 0 deletions pkg/operator/keys/import_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
})
}
}

0 comments on commit 0425d5c

Please sign in to comment.