Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add batch key create utility #35

Merged
merged 16 commits into from
Oct 17, 2023
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

GO_LINES_IGNORED_DIRS=contracts
GO_PACKAGES=./chainio/... ./crypto/... ./logging/... \
./types/... ./utils/... ./signer/...
./types/... ./utils/... ./signer/... ./cmd/...
GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g")
help:
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
29 changes: 29 additions & 0 deletions cmd/egnkey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## egnkey
This tool is used to manage keys for AVS development purpose

Features:
- [Generate ecdsa or bls key in batches](#generate-ecdsa-or-bls-key-in-batches)

### How to install
#### Install from source
```bash
go install github.com/Layr-Labs/eigensdk-go/cmd/egnkey@latest
```

#### Build from source
Navigate to [egnkey](../egnkey/) directory and run
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved
```bash
go install
```

### Generate ecdsa or bls key in batches

To create in a random folder
```bash
egnkey generate --key-type ecdsa --num-keys <num_key>
```

To create in specific folder
```bash
egnkey generate --key-type ecdsa --num-keys <num_key> --output-dir <path_to_folder>
```
208 changes: 208 additions & 0 deletions cmd/egnkey/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package main

import (
"encoding/hex"
"fmt"
"math/rand"
"os"
"path/filepath"
"time"

"github.com/Layr-Labs/eigensdk-go/crypto/bls"

"github.com/Layr-Labs/eigensdk-go/crypto/ecdsa"
"github.com/ethereum/go-ethereum/crypto"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
)

const (
DefaultKeyFolder = "keys"

PasswordFile = "password.txt"
PrivateKeyHexFile = "private_key_hex.txt"
)

var (
KeyTypeFlag = &cli.StringFlag{
Name: "key-type",
Usage: "key type to create (ecdsa or bls)",
Required: true,
}
NumKeysFlag = &cli.IntFlag{
Name: "num-keys",
Usage: "number of keys to create",
Required: true,
DefaultText: "1",
}
OutputDirFlag = &cli.StringFlag{
Name: "output-dir",
Usage: "folder to store keys",
Required: false,
}
)

var commandGenerate = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Description: `Generate keys for testing purpose.
This command creates ecdsa or bls key pair for testing purposes. It generates
all the relevant files for reading and keys and decrypted it and also gets
you the private keys in plaintext.

It creates the following artifacts based on arguments
- passwords.txt - contains all passwords to decrypt keys
- private_key_hex.txt - will create plaintext private keys
- keys/* - create all the encrypted json files in this folder
`,
Action: generate,
Flags: []cli.Flag{
KeyTypeFlag,
NumKeysFlag,
OutputDirFlag,
},
}

func generate(c *cli.Context) error {
keyType := c.String(KeyTypeFlag.Name)
if keyType != "ecdsa" && keyType != "bls" {
return cli.Exit("Invalid key type", 1)
}
numKeys := c.Int(NumKeysFlag.Name)
if numKeys < 1 {
return cli.Exit("Invalid number of keys", 1)
}

folder := c.String(OutputDirFlag.Name)
if folder == "" {
id, err := uuid.NewUUID()
if err != nil {
return cli.Exit("Failed to generate UUID", 1)
}
folder = id.String()
}

_, err := os.Stat(folder)
if !os.IsNotExist(err) {
return cli.Exit("Folder already exists, choose a different folder or delete the existing folder", 1)
}

err = os.MkdirAll(folder+"/"+DefaultKeyFolder, 0755)
if err != nil {
return err
}

passwordFile, err := os.Create(filepath.Clean(folder + "/" + PasswordFile))
if err != nil {
return err
}
privateKeyFile, err := os.Create(filepath.Clean(folder + "/" + PrivateKeyHexFile))
if err != nil {
return err
}

if keyType == "ecdsa" {
err := generateECDSAKeys(numKeys, folder, passwordFile, privateKeyFile)
if err != nil {
return err
}
} else if keyType == "bls" {
err := generateBlsKeys(numKeys, folder, passwordFile, privateKeyFile)
if err != nil {
return err
}
} else {
return cli.Exit("Invalid key type", 1)
}

return nil
}

func generateBlsKeys(numKeys int, path string, passwordFile, privateKeyFile *os.File) error {
for i := 0; i < numKeys; i++ {
key, err := bls.GenRandomBlsKeys()
if err != nil {
return err
}

password := generateRandomPassword()
if err != nil {
return err
}

privateKeyHex := key.PrivKey.String()
fileName := fmt.Sprintf("%d.bls.key.json", i+1)
err = key.SaveToFile(filepath.Clean(path+"/"+DefaultKeyFolder+"/"+fileName), password)
if err != nil {
return err
}

_, err = passwordFile.WriteString(password + "\n")
if err != nil {
return err
}

_, err = privateKeyFile.WriteString(privateKeyHex + "\n")
if err != nil {
return err
}
}
return nil
}

func generateECDSAKeys(numKeys int, path string, passwordFile, privateKeyFile *os.File) error {
for i := 0; i < numKeys; i++ {
key, err := crypto.GenerateKey()
if err != nil {
return err
}

password := generateRandomPassword()
if err != nil {
return err
}

privateKeyHex := hex.EncodeToString(key.D.Bytes())
fileName := fmt.Sprintf("%d.ecdsa.key.json", i+1)
err = ecdsa.WriteKey(filepath.Clean(path+"/"+DefaultKeyFolder+"/"+fileName), key, password)
if err != nil {
return err
}

_, err = passwordFile.WriteString(password + "\n")
if err != nil {
return err
}

_, err = privateKeyFile.WriteString("0x" + privateKeyHex + "\n")
if err != nil {
return err
}
}
return nil
}

func generateRandomPassword() string {
// Seed the random number generator
random := rand.New(rand.NewSource(time.Now().UnixNano()))

// Define character sets for the password
uppercaseLetters := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercaseLetters := "abcdefghijklmnopqrstuvwxyz"
digits := "0123456789"
//specialSymbols := "!@#$%^&*()-_+=[]{}|;:,.<>?/\\"

// Combine character sets into one
//allCharacters := uppercaseLetters + lowercaseLetters + digits + specialSymbols
allCharacters := uppercaseLetters + lowercaseLetters + digits

// Length of the password you want
passwordLength := 20

// Generate the password
password := make([]byte, passwordLength)
for i := range password {
password[i] = allCharacters[random.Intn(len(allCharacters))]
}
return string(password)
}
25 changes: 25 additions & 0 deletions cmd/egnkey/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"
"os"

"github.com/urfave/cli/v2"
)

func main() {
app := cli.NewApp()
app.Name = "egnkey"
app.Description = "Eigenlayer batch keys manager"
app.Commands = []*cli.Command{
commandGenerate,
}

app.Usage = "Used to manage batch keys for testing"

if err := app.Run(os.Args); err != nil {
fmt.Println("Error: ", err)
os.Exit(1)
}

}
97 changes: 97 additions & 0 deletions crypto/utils/batch_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package utils

import (
"bufio"
"fmt"
"os"
"path/filepath"
)

const (
// TODO: move to a common constants file which
// can be used by utils and cmd
DefaultKeyFolder = "keys"
PasswordFile = "password.txt"
PrivateKeyHexFile = "private_key_hex.txt"
)

// ReadBatchKeys reads the batch keys from the given folder
// and returns the list of BatchKey
// folder: folder where the keys are stored, relative to the current directory
func ReadBatchKeys(folder string, isECDSA bool) ([]BatchKey, error) {
var batchKey []BatchKey
absFolder, err := filepath.Abs(folder)
if err != nil {
return nil, err
}

// read the private key file
privateKeyFile, err := os.Open(filepath.Clean(absFolder + "/" + PrivateKeyHexFile))
if err != nil {
fmt.Println("Error opening the file:", err)
return nil, err
}
defer func(privateKeyFile *os.File) {
err := privateKeyFile.Close()
if err != nil {
_ = fmt.Errorf("error closing the file: %s", err)
return
}
}(privateKeyFile)

// read the password file
passwordFile, err := os.Open(filepath.Clean(absFolder + "/" + PasswordFile))
if err != nil {
fmt.Println("Error opening the file:", err)
return nil, err
}
defer func(passwordFile *os.File) {
err := passwordFile.Close()
if err != nil {
_ = fmt.Errorf("error closing the file: %s", err)
return
}
}(passwordFile)

privateKeyScanner := bufio.NewScanner(privateKeyFile)
passwordScanner := bufio.NewScanner(passwordFile)

// To create file names
fileCtr := 1
keyType := "ecdsa"
if !isECDSA {
keyType = "bls"
}
for privateKeyScanner.Scan() && passwordScanner.Scan() {
privateKey := privateKeyScanner.Text()
password := passwordScanner.Text()
fileName := fmt.Sprintf("%d.%s.key.json", fileCtr, keyType)
filePath := fmt.Sprintf("%s/%s/%s", absFolder, DefaultKeyFolder, fileName)
// Since a last line with empty string is also read
// I need to break here
// TODO(madhur): remove empty string when it gets created
if privateKey == "" {
break
}

bK := BatchKey{
FilePath: filePath,
Password: password,
PrivateKey: privateKey,
}
batchKey = append(batchKey, bK)
fileCtr++
}

if err := privateKeyScanner.Err(); err != nil {
fmt.Println("Error reading privateKey file: ", err)
return nil, err
}

if err := passwordScanner.Err(); err != nil {
fmt.Println("Error reading password file: ", err)
return nil, err
}

return batchKey, nil
}
Loading
Loading