Skip to content

Commit 0425d5c

Browse files
add keys import (Layr-Labs#5)
* add keys import * add keys import * add to keys
1 parent 4e75fed commit 0425d5c

File tree

5 files changed

+299
-1
lines changed

5 files changed

+299
-1
lines changed

pkg/operator/keys.go

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func KeysCmd(p utils.Prompter) *cli.Command {
1313
Subcommands: []*cli.Command{
1414
keys.CreateCmd(p),
1515
keys.ListCmd(),
16+
keys.ImportCmd(p),
1617
},
1718
}
1819

pkg/operator/keys/create_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestCreateCmd(t *testing.T) {
4949
err: ErrKeyContainsWhitespaces,
5050
},
5151
{
52-
name: "invalid keytype",
52+
name: "invalid key type",
5353
args: []string{"--key-type", "invalid", "do_not_use_this_name"},
5454
err: ErrInvalidKeyType,
5555
},
File renamed without changes.

pkg/operator/keys/import.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package keys
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
"regexp"
7+
"strings"
8+
9+
"github.com/Layr-Labs/eigenlayer-cli/pkg/utils"
10+
"github.com/Layr-Labs/eigensdk-go/crypto/bls"
11+
"github.com/ethereum/go-ethereum/crypto"
12+
"github.com/urfave/cli/v2"
13+
)
14+
15+
func ImportCmd(p utils.Prompter) *cli.Command {
16+
importCmd := &cli.Command{
17+
Name: "import",
18+
Usage: "Used to import existing keys in local keystore",
19+
UsageText: "import --key-type <key-type> [flags] <keyname> <private-key>",
20+
Description: `
21+
Used to import ecdsa and bls key in local keystore
22+
23+
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
24+
25+
use --key-type ecdsa/bls to import ecdsa/bls key.
26+
- ecdsa - <private-key> should be plaintext hex encoded private key
27+
- bls - <private-key> should be plaintext bls private key
28+
29+
It will prompt for password to encrypt the key, which is optional but highly recommended.
30+
If you want to import a key with weak/no password, use --insecure flag. Do NOT use those keys in production
31+
32+
This command will import keys in $HOME/.eigenlayer/operator_keys/ location
33+
`,
34+
Flags: []cli.Flag{
35+
&KeyTypeFlag,
36+
&InsecureFlag,
37+
},
38+
39+
Action: func(ctx *cli.Context) error {
40+
args := ctx.Args()
41+
if args.Len() != 2 {
42+
return fmt.Errorf("%w: accepts 2 arg, received %d", ErrInvalidNumberOfArgs, args.Len())
43+
}
44+
45+
keyName := args.Get(0)
46+
if err := validateKeyName(keyName); err != nil {
47+
return err
48+
}
49+
50+
privateKey := args.Get(1)
51+
if err := validatePrivateKey(privateKey); err != nil {
52+
return err
53+
}
54+
55+
keyType := ctx.String(KeyTypeFlag.Name)
56+
insecure := ctx.Bool(InsecureFlag.Name)
57+
58+
switch keyType {
59+
case KeyTypeECDSA:
60+
privateKey = strings.TrimPrefix(privateKey, "0x")
61+
privateKeyPair, err := crypto.HexToECDSA(privateKey)
62+
if err != nil {
63+
return err
64+
}
65+
return saveEcdsaKey(keyName, p, privateKeyPair, insecure)
66+
case KeyTypeBLS:
67+
privateKeyBigInt := new(big.Int)
68+
_, ok := privateKeyBigInt.SetString(privateKey, 10)
69+
blsKeyPair := new(bls.KeyPair)
70+
var err error
71+
if ok {
72+
fmt.Println("Importing from large integer")
73+
blsKeyPair, err = bls.NewKeyPairFromString(privateKey)
74+
if err != nil {
75+
return err
76+
}
77+
} else {
78+
// Try to parse as hex
79+
fmt.Println("Importing from hex")
80+
z := new(big.Int)
81+
privateKey = strings.TrimPrefix(privateKey, "0x")
82+
_, ok := z.SetString(privateKey, 16)
83+
if !ok {
84+
return ErrInvalidHexPrivateKey
85+
}
86+
blsKeyPair, err = bls.NewKeyPairFromString(z.String())
87+
if err != nil {
88+
return err
89+
}
90+
}
91+
return saveBlsKey(keyName, p, blsKeyPair, insecure)
92+
default:
93+
return ErrInvalidKeyType
94+
}
95+
},
96+
}
97+
return importCmd
98+
}
99+
100+
func validatePrivateKey(pk string) error {
101+
if len(pk) == 0 {
102+
return ErrEmptyPrivateKey
103+
}
104+
105+
if match, _ := regexp.MatchString("\\s", pk); match {
106+
return ErrPrivateKeyContainsWhitespaces
107+
}
108+
109+
return nil
110+
}

pkg/operator/keys/import_test.go

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package keys
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"testing"
12+
13+
"github.com/urfave/cli/v2"
14+
15+
"github.com/Layr-Labs/eigensdk-go/crypto/bls"
16+
17+
prompterMock "github.com/Layr-Labs/eigenlayer-cli/pkg/utils/mocks"
18+
"github.com/stretchr/testify/assert"
19+
"go.uber.org/mock/gomock"
20+
)
21+
22+
func TestImportCmd(t *testing.T) {
23+
homePath, err := os.UserHomeDir()
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
28+
tests := []struct {
29+
name string
30+
args []string
31+
err error
32+
keyPath string
33+
expectedPrivKey string
34+
promptMock func(p *prompterMock.MockPrompter)
35+
}{
36+
{
37+
name: "key-name flag not set",
38+
args: []string{},
39+
err: errors.New("Required flag \"key-type\" not set"),
40+
},
41+
{
42+
name: "one argument",
43+
args: []string{"--key-type", "ecdsa", "arg1"},
44+
err: fmt.Errorf("%w: accepts 2 arg, received 1", ErrInvalidNumberOfArgs),
45+
},
46+
47+
{
48+
name: "more than two argument",
49+
args: []string{"--key-type", "ecdsa", "arg1", "arg2", "arg3"},
50+
err: fmt.Errorf("%w: accepts 2 arg, received 3", ErrInvalidNumberOfArgs),
51+
},
52+
{
53+
name: "empty key name argument",
54+
args: []string{"--key-type", "ecdsa", "", ""},
55+
err: ErrEmptyKeyName,
56+
},
57+
{
58+
name: "keyname with whitespaces",
59+
args: []string{"--key-type", "ecdsa", "hello world", ""},
60+
err: ErrKeyContainsWhitespaces,
61+
},
62+
{
63+
name: "empty private key argument",
64+
args: []string{"--key-type", "ecdsa", "hello", ""},
65+
err: ErrEmptyPrivateKey,
66+
},
67+
{
68+
name: "keyname with whitespaces",
69+
args: []string{"--key-type", "ecdsa", "hello", "hello world"},
70+
err: ErrPrivateKeyContainsWhitespaces,
71+
},
72+
{
73+
name: "invalid key type",
74+
args: []string{"--key-type", "invalid", "hello", "privkey"},
75+
err: ErrInvalidKeyType,
76+
},
77+
{
78+
name: "invalid password based on validation function - ecdsa",
79+
args: []string{"--key-type", "ecdsa", "test", "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"},
80+
err: ErrInvalidPassword,
81+
promptMock: func(p *prompterMock.MockPrompter) {
82+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", ErrInvalidPassword)
83+
},
84+
},
85+
{
86+
name: "invalid password based on validation function - bls",
87+
args: []string{"--key-type", "bls", "test", "123"},
88+
err: ErrInvalidPassword,
89+
promptMock: func(p *prompterMock.MockPrompter) {
90+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", ErrInvalidPassword)
91+
},
92+
},
93+
{
94+
name: "valid ecdsa key import",
95+
args: []string{"--key-type", "ecdsa", "test", "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"},
96+
err: nil,
97+
promptMock: func(p *prompterMock.MockPrompter) {
98+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
99+
},
100+
expectedPrivKey: "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6",
101+
keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.ecdsa.key.json"),
102+
},
103+
{
104+
name: "valid ecdsa key import with 0x prefix",
105+
args: []string{"--key-type", "ecdsa", "test", "0x6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6"},
106+
err: nil,
107+
promptMock: func(p *prompterMock.MockPrompter) {
108+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
109+
},
110+
expectedPrivKey: "6842fb8f5fa574d0482818b8a825a15c4d68f542693197f2c2497e3562f335f6",
111+
keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.ecdsa.key.json"),
112+
},
113+
{
114+
name: "valid bls key import",
115+
args: []string{"--key-type", "bls", "test", "20030410000080487431431153104351076122223465926814327806350179952713280726583"},
116+
err: nil,
117+
promptMock: func(p *prompterMock.MockPrompter) {
118+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
119+
},
120+
expectedPrivKey: "20030410000080487431431153104351076122223465926814327806350179952713280726583",
121+
keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"),
122+
},
123+
{
124+
name: "valid bls key import for hex key",
125+
args: []string{"--key-type", "bls", "test", "0xfe198b992d97545b3b0174f026f781039f167c13f6d0ce9f511d0d2e973b7f02"},
126+
err: nil,
127+
promptMock: func(p *prompterMock.MockPrompter) {
128+
p.EXPECT().InputHiddenString(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
129+
},
130+
expectedPrivKey: "5491383829988096583828972342810831790467090979842721151380259607665538989821",
131+
keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"),
132+
},
133+
{
134+
name: "invalid bls key import for hex key",
135+
args: []string{"--key-type", "bls", "test", "0xfes"},
136+
err: ErrInvalidHexPrivateKey,
137+
keyPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "/test.bls.key.json"),
138+
},
139+
}
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
t.Cleanup(func() {
143+
_ = os.Remove(tt.keyPath)
144+
})
145+
controller := gomock.NewController(t)
146+
p := prompterMock.NewMockPrompter(controller)
147+
if tt.promptMock != nil {
148+
tt.promptMock(p)
149+
}
150+
151+
importCmd := ImportCmd(p)
152+
app := cli.NewApp()
153+
154+
// We do this because the in the parsing of arguments it ignores the first argument
155+
// for commands, so we add a blank string as the first argument
156+
// I suspect it does this because it is expecting the first argument to be the name of the command
157+
// But when we are testing the command, we don't want to have to specify the name of the command
158+
// since we are creating the command ourselves
159+
// https://github.com/urfave/cli/blob/c023d9bc5a3122830c9355a0a8c17137e0c8556f/command.go#L323
160+
args := append([]string{""}, tt.args...)
161+
cCtx := cli.NewContext(app, nil, &cli.Context{Context: context.Background()})
162+
err := importCmd.Run(cCtx, args...)
163+
164+
if tt.err == nil {
165+
assert.NoError(t, err)
166+
_, err := os.Stat(tt.keyPath)
167+
168+
// Check if the error indicates that the file does not exist
169+
if os.IsNotExist(err) {
170+
assert.Failf(t, "file does not exist", "file %s does not exist", tt.keyPath)
171+
}
172+
173+
if tt.args[1] == KeyTypeECDSA {
174+
key, err := GetECDSAPrivateKey(tt.keyPath, "")
175+
assert.NoError(t, err)
176+
assert.Equal(t, strings.Trim(tt.args[3], "0x"), hex.EncodeToString(key.D.Bytes()))
177+
} else if tt.args[1] == KeyTypeBLS {
178+
key, err := bls.ReadPrivateKeyFromFile(tt.keyPath, "")
179+
assert.NoError(t, err)
180+
assert.Equal(t, tt.expectedPrivKey, key.PrivKey.String())
181+
}
182+
} else {
183+
assert.EqualError(t, err, tt.err.Error())
184+
}
185+
})
186+
}
187+
}

0 commit comments

Comments
 (0)