Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,68 @@ make build
### Prerequisites
- Ethereum RPC endpoint to send transactions.

## ECDSA Key Management

The CLI supports two methods for providing ECDSA private keys for transaction signing:

### Option 1: Encrypted Keystore Files (Recommended)

For enhanced security, use encrypted keystore files compatible with EigenLayer CLI:

```bash
# Generate a new ECDSA key using EigenLayer CLI
eigenlayer operator keys create --key-type ecdsa [keyname]

# Use the keystore file with etherfi-avs-operator-CLI
./avs-cli arpa register-node \
--operator-id 1 \
--dkg-public-key "0x..." \
--registration-signature input.json \
--ecdsa-keystore /path/to/keystore.json \
--ecdsa-password "your_password"
```

### Option 2: Environment Variables (Legacy)

For backward compatibility, you can still use environment variables:

```bash
export PRIVATE_KEY="your_private_key_hex"
export ADMIN_1271_SIGNING_KEY="admin_signing_key_hex"
export WATCHTOWER_PRIVATE_KEY="watchtower_private_key_hex" # For witness-chain only

./avs-cli arpa register-node \
--operator-id 1 \
--dkg-public-key "0x..." \
--registration-signature input.json
```

### Migration Guide

If you're currently using environment variables, we recommend migrating to encrypted keystore files:

1. **Create a keystore file from your existing private key:**
```bash
# Using EigenLayer CLI
eigenlayer operator keys import --key-type ecdsa [keyname] [private_key_hex]
```

2. **Update your commands to use keystore flags:**
- Add `--ecdsa-keystore /path/to/keystore.json`
- Add `--ecdsa-password "your_password"`
- Remove environment variable exports

3. **Supported commands with keystore support:**
- `arpa register-node`
- `arpa generate-registration-signature`
- `update-ecdsa-signer`
- `witness-chain prepare-registration`

### Security Benefits

- **No raw private keys in environment:** Keystore files are encrypted with your password
- **Industry standard:** Compatible with EigenLayer CLI keystore format
- **Better operational security:** Reduces risk of key exposure in process environments

## Step 1: Request ether.fi team to be registered as a Delegated AVS operator

Expand Down
18 changes: 9 additions & 9 deletions bin/avs-cli/arpa/arpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"fmt"
"os"

"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/avs/arpa"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/config"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/etherfi"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/utils"
"github.com/urfave/cli/v3"
)

Expand Down Expand Up @@ -61,7 +61,7 @@ var RegisterCmd = &cli.Command{
Name: "register-node",
Usage: "(Node Operator) register a node with the ARPA service",
Action: handleRegistration,
Flags: []cli.Flag{
Flags: append([]cli.Flag{
&cli.IntFlag{
Name: "operator-id",
Usage: "Operator ID",
Expand All @@ -77,7 +77,7 @@ var RegisterCmd = &cli.Command{
Usage: "path to registration signature file created by prepare-registration command",
Required: true,
},
},
}, utils.GetECDSAKeystoreFlags()...),
}

func handleRegistration(ctx context.Context, cli *cli.Command) error {
Expand Down Expand Up @@ -119,9 +119,9 @@ func handleRegistration(ctx context.Context, cli *cli.Command) error {
}

// load `Node Account` private key
signingKey, err := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
signingKey, err := utils.LoadECDSAKey(cli, "PRIVATE_KEY")
if err != nil {
return fmt.Errorf("invalid private key: %w", err)
return fmt.Errorf("loading Node Account private key: %w", err)
}

return arpaAPI.Register(operator, dkgPublicKey, inputSignature, signingKey)
Expand All @@ -131,13 +131,13 @@ var GenerationRegisterSignatureCmd = &cli.Command{
Name: "generate-registration-signature",
Usage: "(Admin) generate a registration signature for the given operator",
Action: handleGenerateRegistrationSignature,
Flags: []cli.Flag{
Flags: append([]cli.Flag{
&cli.IntFlag{
Name: "operator-id",
Usage: "Operator ID",
Required: true,
},
},
}, utils.GetECDSAKeystoreFlags()...),
}

func handleGenerateRegistrationSignature(ctx context.Context, cli *cli.Command) error {
Expand All @@ -152,9 +152,9 @@ func handleGenerateRegistrationSignature(ctx context.Context, cli *cli.Command)
}

// load eip-1271 admin signing key
signingKey, err := crypto.HexToECDSA(os.Getenv("ADMIN_1271_SIGNING_KEY"))
signingKey, err := utils.LoadECDSAKey(cli, "ADMIN_1271_SIGNING_KEY")
if err != nil {
return fmt.Errorf("invalid private key: %w", err)
return fmt.Errorf("loading admin signing key: %w", err)
}

return arpaAPI.GenerateAVSRegistrationSignature(operator, signingKey)
Expand Down
13 changes: 6 additions & 7 deletions bin/avs-cli/update_ecdsa_signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import (
"encoding/hex"
"fmt"
"math/big"
"os"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/config"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/etherfi"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/utils"
"github.com/urfave/cli/v3"
)

var updateEcdsaSignerCmd = &cli.Command{
Name: "update-ecdsa-signer",
Usage: "(Ether.fi Admin) the signer associated with this operator",
Action: handleUpdateEcdsaSigner,
Flags: []cli.Flag{
Flags: append([]cli.Flag{
&cli.IntFlag{
Name: "operator-id",
Usage: "Operator ID",
Expand All @@ -40,7 +39,7 @@ var updateEcdsaSignerCmd = &cli.Command{
Name: "rpc-url",
Usage: "rpc url",
},
},
}, utils.GetECDSAKeystoreFlags()...),
}

func handleUpdateEcdsaSigner(ctx context.Context, cli *cli.Command) error {
Expand All @@ -56,10 +55,10 @@ func handleUpdateEcdsaSigner(ctx context.Context, cli *cli.Command) error {
return fmt.Errorf("dialing rpc: %w", err)
}

return updateEcdsaSigner(rpcClient, operatorID, ecdsaSigner, broadcast)
return updateEcdsaSigner(rpcClient, operatorID, ecdsaSigner, broadcast, cli)
}

func updateEcdsaSigner(rpcClient *ethclient.Client, operatorID int64, ecdsaSigner common.Address, broadcast bool) error {
func updateEcdsaSigner(rpcClient *ethclient.Client, operatorID int64, ecdsaSigner common.Address, broadcast bool, cli *cli.Command) error {

// load configuration
chainID, err := rpcClient.ChainID(context.Background())
Expand All @@ -78,7 +77,7 @@ func updateEcdsaSigner(rpcClient *ethclient.Client, operatorID int64, ecdsaSigne
}

// load signing key
privateKey, err := crypto.HexToECDSA(os.Getenv("PRIVATE_KEY"))
privateKey, err := utils.LoadECDSAKey(cli, "PRIVATE_KEY")
if err != nil {
return fmt.Errorf("loading signing key: %w", err)
}
Expand Down
11 changes: 5 additions & 6 deletions bin/avs-cli/witness-chain/prepareRegistration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"context"
"fmt"
"math/big"
"os"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/utils"
"github.com/urfave/cli/v3"
)

Expand All @@ -22,13 +21,13 @@ var WitnessPrepareRegistrationCmd = &cli.Command{
Name: "prepare-registration",
Usage: "(Node Operator) gather all inputs required to register for avs",
Action: handleWitnessPrepareRegistration,
Flags: []cli.Flag{
Flags: append([]cli.Flag{
&cli.IntFlag{
Name: "operator-id",
Usage: "Operator ID",
Required: true,
},
},
}, utils.GetECDSAKeystoreFlags()...),
}

func handleWitnessPrepareRegistration(ctx context.Context, cli *cli.Command) error {
Expand All @@ -37,9 +36,9 @@ func handleWitnessPrepareRegistration(ctx context.Context, cli *cli.Command) err
operatorID := cli.Int("operator-id")

// load watchtower private key
watchtowerPrivateKey, err := crypto.HexToECDSA(os.Getenv("WATCHTOWER_PRIVATE_KEY"))
watchtowerPrivateKey, err := utils.LoadECDSAKey(cli, "WATCHTOWER_PRIVATE_KEY")
if err != nil {
return fmt.Errorf("invalid WATCHTOWER_PRIVATE_KEY env var: %w", err)
return fmt.Errorf("loading watchtower private key: %w", err)
}

// look up operator contract associated with this id
Expand Down
33 changes: 32 additions & 1 deletion sample.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
export PRIVATE_KEY=""
# RPC endpoint for blockchain interaction
export RPC_URL=""

# ================================================================================
# ECDSA Private Key Configuration
# ================================================================================
#
# SECURITY NOTICE: We strongly recommend using encrypted keystore files instead
# of environment variables for better security.
#
# NEW APPROACH (Recommended): Use --ecdsa-keystore and --ecdsa-password flags
# Example:
# ./avs-cli arpa register-node \
# --operator-id 1 \
# --dkg-public-key "0x..." \
# --registration-signature input.json \
# --ecdsa-keystore /path/to/keystore.json \
# --ecdsa-password "your_password"
#
# LEGACY APPROACH (Deprecated): Environment variables (less secure)
# Only use if you cannot migrate to keystore files yet.
# ================================================================================

# Node operator private key (used by ARPA register-node command)
# DEPRECATED: Use --ecdsa-keystore flag instead
export PRIVATE_KEY=""

# Admin EIP-1271 signing key (used by admin registration commands)
# DEPRECATED: Use --ecdsa-keystore flag instead
export ADMIN_1271_SIGNING_KEY=""

# Witness chain watchtower private key (used by witness-chain prepare-registration)
# DEPRECATED: Use --ecdsa-keystore flag instead
export WATCHTOWER_PRIVATE_KEY=""
44 changes: 44 additions & 0 deletions src/keystore/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"

"github.com/consensys/gnark-crypto/ecc/bn254"
"github.com/consensys/gnark-crypto/ecc/bn254/fp"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -78,3 +80,45 @@ func TestKeystore_BLSSign(t *testing.T) {
// input should be [32]byte format but how get I get this?
//signature.Verify(blsKeyPair.GetPubKeyG2(), )
}

func TestKeystore_LoadECDSA(t *testing.T) {
// fixtures
ecdsaKeyFile := "./fixtures/fixture.ecdsa.key.json"
expectedAddress := "0x287B703F25CE707D7974D26DBE5B78121f70794f"
passwd := "fixture@test1234"

ks := NewKeystoreV3()
ecdsaKey, err := ks.LoadECDSA(ecdsaKeyFile, passwd)

assert.Nil(t, err)
assert.NotNil(t, ecdsaKey)

// Verify the loaded key generates the expected address
address := crypto.PubkeyToAddress(ecdsaKey.PublicKey)
assert.Equal(t, strings.ToLower(expectedAddress), strings.ToLower(address.Hex()))

fmt.Printf("Loaded ECDSA key for address: %s\n", address.Hex())
}

func TestKeystore_LoadECDSA_InvalidPassword(t *testing.T) {
ecdsaKeyFile := "./fixtures/fixture.ecdsa.key.json"
wrongPasswd := "wrongpassword"

ks := NewKeystoreV3()
ecdsaKey, err := ks.LoadECDSA(ecdsaKeyFile, wrongPasswd)

assert.NotNil(t, err)
assert.Nil(t, ecdsaKey)
assert.Contains(t, err.Error(), "could not decrypt key")
}

func TestKeystore_LoadECDSA_NonexistentFile(t *testing.T) {
nonexistentFile := "./fixtures/nonexistent.json"
passwd := "fixture@test1234"

ks := NewKeystoreV3()
ecdsaKey, err := ks.LoadECDSA(nonexistentFile, passwd)

assert.NotNil(t, err)
assert.Nil(t, ecdsaKey)
}
64 changes: 64 additions & 0 deletions src/utils/ecdsa_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package utils

import (
"crypto/ecdsa"
"fmt"
"os"

"github.com/ethereum/go-ethereum/crypto"
"github.com/etherfi-protocol/etherfi-avs-operator-tool/src/keystore"
"github.com/urfave/cli/v3"
)

// CommandStringGetter interface for getting string values from CLI commands
type CommandStringGetter interface {
String(name string) string
}

// LoadECDSAKey loads an ECDSA private key from keystore file if provided,
// otherwise falls back to environment variable for backward compatibility
func LoadECDSAKey(cmd CommandStringGetter, envVarName string) (*ecdsa.PrivateKey, error) {
keystoreFile := cmd.String("ecdsa-keystore")
keystorePassword := cmd.String("ecdsa-password")

// If keystore flags are provided, load from keystore
if keystoreFile != "" {
if keystorePassword == "" {
return nil, fmt.Errorf("--ecdsa-password is required when --ecdsa-keystore is provided")
}

ks := keystore.NewKeystoreV3()
privateKey, err := ks.LoadECDSA(keystoreFile, keystorePassword)
if err != nil {
return nil, fmt.Errorf("loading ECDSA key from keystore: %w", err)
}
return privateKey, nil
}

// Fall back to environment variable for backward compatibility
privateKeyHex := os.Getenv(envVarName)
if privateKeyHex == "" {
return nil, fmt.Errorf("must provide either --ecdsa-keystore flag or set %s environment variable", envVarName)
}

privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, fmt.Errorf("invalid private key in %s: %w", envVarName, err)
}

return privateKey, nil
}

// GetECDSAKeystoreFlags returns the standard ECDSA keystore flags
func GetECDSAKeystoreFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "ecdsa-keystore",
Usage: "Path to ECDSA keystore file (alternative to environment variable)",
},
&cli.StringFlag{
Name: "ecdsa-password",
Usage: "Password for ECDSA keystore file",
},
}
}
Loading