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
19 changes: 19 additions & 0 deletions cmd/elkey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## elkey
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved

### How to build
Navigate to [elkey](../elkey/) directory and run
```bash
go build
```

### How to run

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

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

import (
"encoding/hex"
"fmt"
"math/rand"
"os"
"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",
samlaf marked this conversation as resolved.
Show resolved Hide resolved
Usage: "number of keys to create",
Required: true,
}
OutputDirFlag = &cli.StringFlag{
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved
Name: "output-dir",
Usage: "folder to store keys",
Required: false,
}
)

var commandGenerate = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate keys",
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(folder + "/" + PasswordFile)
if err != nil {
return err
}
privateKeyFile, err := os.Create(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(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(path+"/"+DefaultKeyFolder+"/"+fileName, key, 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 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

// 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/elkey/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 = "Eigenlayer batch key manager"
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved
app.Commands = []*cli.Command{
commandGenerate,
}

if err := app.Run(os.Args); err != nil {
_, err := fmt.Fprintln(os.Stderr, err)
if err != nil {
return
}
os.Exit(1)
samlaf marked this conversation as resolved.
Show resolved Hide resolved
}

}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.21
require (
github.com/consensys/gnark-crypto v0.12.0
github.com/ethereum/go-ethereum v1.12.2
github.com/google/uuid v1.3.0
github.com/prometheus/client_golang v1.17.0
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.24.1
go.uber.org/mock v0.2.0
go.uber.org/zap v1.26.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -19,14 +21,14 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/uint256 v1.2.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
Expand All @@ -35,9 +37,11 @@ require (
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/tklauser/go-sysconf v0.3.5 // indirect
github.com/tklauser/numcpus v0.2.2 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect
Expand Down
97 changes: 97 additions & 0 deletions 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 constatns file which
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved
// 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(absFolder + "/" + PrivateKeyHexFile)
shrimalmadhur marked this conversation as resolved.
Show resolved Hide resolved
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(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