Skip to content

Commit

Permalink
cmd: exit with validator index, allow BN URLs (#3106)
Browse files Browse the repository at this point in the history
Allow specifying more than one beacon node to use with the `--beacon-node-endpoints` flag.

Users can now sign exits directly with a validator index rather than specifying its public key.

If both are specified, validator liveliness (i.e. whether or not the validator exists on the beacon chain) is not checked.

`sign` checks that the specified validator index exists in the cluster lock before proceeding.


category: feature
ticket: #3104 

Closes #3104
  • Loading branch information
gsora authored and KaloyanTanev committed Jun 5, 2024
1 parent 5c3cdc5 commit 32795a0
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 164 deletions.
64 changes: 35 additions & 29 deletions cmd/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"context"
"time"

eth2http "github.com/attestantio/go-eth2-client/http"
eth2api "github.com/attestantio/go-eth2-client/api"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"

Expand All @@ -18,19 +18,22 @@ import (
)

type exitConfig struct {
BeaconNodeURL string
ValidatorPubkey string
PrivateKeyPath string
ValidatorKeysDir string
LockFilePath string
PublishAddress string
PublishTimeout time.Duration
ExitEpoch uint64
FetchedExitPath string
PlaintextOutput bool
BeaconNodeTimeout time.Duration
ExitFromFilePath string
Log log.Config
BeaconNodeEndpoints []string
ValidatorPubkey string
ValidatorIndex uint64
ValidatorIndexPresent bool
ExpertMode bool
PrivateKeyPath string
ValidatorKeysDir string
LockFilePath string
PublishAddress string
PublishTimeout time.Duration
ExitEpoch uint64
FetchedExitPath string
PlaintextOutput bool
BeaconNodeTimeout time.Duration
ExitFromFilePath string
Log log.Config
}

func newExitCmd(cmds ...*cobra.Command) *cobra.Command {
Expand All @@ -49,7 +52,7 @@ type exitFlag int

const (
publishAddress exitFlag = iota
beaconNodeURL
beaconNodeEndpoints
privateKeyPath
lockFilePath
validatorKeysDir
Expand All @@ -59,14 +62,15 @@ const (
beaconNodeTimeout
fetchedExitPath
publishTimeout
validatorIndex
)

func (ef exitFlag) String() string {
switch ef {
case publishAddress:
return "publish-address"
case beaconNodeURL:
return "beacon-node-url"
case beaconNodeEndpoints:
return "beacon-node-endpoints"
case privateKeyPath:
return "private-key-file"
case lockFilePath:
Expand All @@ -85,6 +89,8 @@ func (ef exitFlag) String() string {
return "fetched-exit-path"
case publishTimeout:
return "publish-timeout"
case validatorIndex:
return "validator-index"
default:
return "unknown"
}
Expand All @@ -110,16 +116,16 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
switch flag {
case publishAddress:
cmd.Flags().StringVar(&config.PublishAddress, publishAddress.String(), "https://api.obol.tech", maybeRequired("The URL of the remote API."))
case beaconNodeURL:
cmd.Flags().StringVar(&config.BeaconNodeURL, beaconNodeURL.String(), "", maybeRequired("Beacon node URL."))
case beaconNodeEndpoints:
cmd.Flags().StringSliceVar(&config.BeaconNodeEndpoints, beaconNodeEndpoints.String(), nil, maybeRequired("Comma separated list of one or more beacon node endpoint URLs."))
case privateKeyPath:
cmd.Flags().StringVar(&config.PrivateKeyPath, privateKeyPath.String(), ".charon/charon-enr-private-key", maybeRequired("The path to the charon enr private key file. "))
case lockFilePath:
cmd.Flags().StringVar(&config.LockFilePath, lockFilePath.String(), ".charon/cluster-lock.json", maybeRequired("The path to the cluster lock file defining the distributed validator cluster."))
case validatorKeysDir:
cmd.Flags().StringVar(&config.ValidatorKeysDir, validatorKeysDir.String(), ".charon/validator_keys", maybeRequired("Path to the directory containing the validator private key share files and passwords."))
case validatorPubkey:
cmd.Flags().StringVar(&config.ValidatorPubkey, validatorPubkey.String(), "", maybeRequired("Public key of the validator to exit, must be present in the cluster lock manifest."))
cmd.Flags().StringVar(&config.ValidatorPubkey, validatorPubkey.String(), "", maybeRequired("Public key of the validator to exit, must be present in the cluster lock manifest. If --validator-index is also provided, validator liveliness won't be checked on the beacon chain."))
case exitEpoch:
cmd.Flags().Uint64Var(&config.ExitEpoch, exitEpoch.String(), 162304, maybeRequired("Exit epoch at which the validator will exit, must be the same across all the partial exits."))
case exitFromFile:
Expand All @@ -130,6 +136,8 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
cmd.Flags().StringVar(&config.FetchedExitPath, fetchedExitPath.String(), "./", maybeRequired("Path to store fetched signed exit messages."))
case publishTimeout:
cmd.Flags().DurationVar(&config.PublishTimeout, publishTimeout.String(), 30*time.Second, "Timeout for publishing a signed exit to the publish-address API.")
case validatorIndex:
cmd.Flags().Uint64Var(&config.ValidatorIndex, validatorIndex.String(), 0, "Validator index of the validator to exit, the associated public key must be present in the cluster lock manifest. If --validator-pubkey is also provided, validator liveliness won't be checked on the beacon chain.")
}

if f.required {
Expand All @@ -138,19 +146,17 @@ func bindExitFlags(cmd *cobra.Command, config *exitConfig, flags []exitCLIFlag)
}
}

func eth2Client(ctx context.Context, u string, timeout time.Duration) (eth2wrap.Client, error) {
bnHTTPClient, err := eth2http.New(ctx,
eth2http.WithAddress(u),
eth2http.WithTimeout(timeout),
eth2http.WithLogLevel(1), // zerolog.InfoLevel
)
func eth2Client(ctx context.Context, u []string, timeout time.Duration) (eth2wrap.Client, error) {
cl, err := eth2wrap.NewMultiHTTP(timeout, u...)
if err != nil {
return nil, errors.Wrap(err, "can't connect to beacon node")
return nil, err
}

bnClient := bnHTTPClient.(*eth2http.Service)
if _, err = cl.NodeVersion(ctx, &eth2api.NodeVersionOpts{}); err != nil {
return nil, errors.Wrap(err, "can't connect to beacon node")
}

return eth2wrap.AdaptEth2HTTP(bnClient, timeout), nil
return cl, nil
}

// signExit signs a voluntary exit message for valIdx with the given keyShare.
Expand Down
4 changes: 2 additions & 2 deletions cmd/exit_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func newBcastFullExitCmd(runFunc func(context.Context, exitConfig) error) *cobra
{validatorKeysDir, false},
{exitEpoch, false},
{validatorPubkey, true},
{beaconNodeURL, true},
{beaconNodeEndpoints, true},
{exitFromFile, false},
{beaconNodeTimeout, false},
})
Expand Down Expand Up @@ -81,7 +81,7 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error {

ctx = log.WithCtx(ctx, z.Str("validator", validator.String()))

eth2Cl, err := eth2Client(ctx, config.BeaconNodeURL, config.BeaconNodeTimeout)
eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout)
if err != nil {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}
Expand Down
80 changes: 40 additions & 40 deletions cmd/exit_broadcast_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, beaconMock.Address(), 10*time.Second)
eth2Cl, err := eth2Client(ctx, []string{beaconMock.Address()}, 10*time.Second)
require.NoError(t, err)

eth2Cl.SetForkVersion([4]byte(lock.ForkVersion))
Expand All @@ -115,15 +115,15 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", idx))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
}

require.NoError(t, runSignPartialExit(ctx, config), "operator index: %v", idx)
Expand All @@ -132,15 +132,15 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", 0))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
}

if fromFile {
Expand All @@ -162,14 +162,14 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
func Test_runBcastFullExitCmd_Config(t *testing.T) {
t.Parallel()
type test struct {
name string
noIdentity bool
noLock bool
badOAPIURL bool
badBeaconNodeURL bool
badValidatorAddr bool
badExistingExitPath bool
errData string
name string
noIdentity bool
noLock bool
badOAPIURL bool
badBeaconNodeEndpoints bool
badValidatorAddr bool
badExistingExitPath bool
errData string
}

tests := []test{
Expand All @@ -189,9 +189,9 @@ func Test_runBcastFullExitCmd_Config(t *testing.T) {
errData: "could not create obol api client",
},
{
name: "Bad beacon node URL",
badBeaconNodeURL: true,
errData: "cannot create eth2 client for specified beacon node",
name: "Bad beacon node URLs",
badBeaconNodeEndpoints: true,
errData: "cannot create eth2 client for specified beacon node",
},
{
name: "Bad validator address",
Expand Down Expand Up @@ -261,7 +261,7 @@ func Test_runBcastFullExitCmd_Config(t *testing.T) {

bnURL := badStr

if !test.badBeaconNodeURL {
if !test.badBeaconNodeEndpoints {
beaconMock, err := beaconmock.New()
require.NoError(t, err)
defer func() {
Expand All @@ -283,15 +283,15 @@ func Test_runBcastFullExitCmd_Config(t *testing.T) {
baseDir := filepath.Join(root, "op0") // one operator is enough

config := exitConfig{
BeaconNodeURL: bnURL,
ValidatorPubkey: valAddr,
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: oapiURL,
ExitEpoch: 0,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
BeaconNodeEndpoints: []string{bnURL},
ValidatorPubkey: valAddr,
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: oapiURL,
ExitEpoch: 0,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
}

if test.badExistingExitPath {
Expand Down
20 changes: 10 additions & 10 deletions cmd/exit_fetch_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func Test_runFetchExitFullFlow(t *testing.T) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, beaconMock.Address(), 10*time.Second)
eth2Cl, err := eth2Client(ctx, []string{beaconMock.Address()}, 10*time.Second)
require.NoError(t, err)

eth2Cl.SetForkVersion([4]byte(lock.ForkVersion))
Expand All @@ -99,15 +99,15 @@ func Test_runFetchExitFullFlow(t *testing.T) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", idx))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
ValidatorPubkey: lock.Validators[0].PublicKeyHex(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PublishAddress: srv.URL,
ExitEpoch: 194048,
BeaconNodeTimeout: 30 * time.Second,
PublishTimeout: 10 * time.Second,
}

require.NoError(t, runSignPartialExit(ctx, config), "operator index: %v", idx)
Expand Down
4 changes: 2 additions & 2 deletions cmd/exit_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func newListActiveValidatorsCmd(runFunc func(context.Context, exitConfig) error)

bindExitFlags(cmd, &config, []exitCLIFlag{
{lockFilePath, false},
{beaconNodeURL, true},
{beaconNodeEndpoints, true},
{beaconNodeTimeout, false},
})

Expand Down Expand Up @@ -75,7 +75,7 @@ func listActiveVals(ctx context.Context, config exitConfig) ([]string, error) {
return nil, errors.Wrap(err, "could not load cluster-lock.json")
}

eth2Cl, err := eth2Client(ctx, config.BeaconNodeURL, config.BeaconNodeTimeout)
eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout)
if err != nil {
return nil, errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}
Expand Down
36 changes: 18 additions & 18 deletions cmd/exit_list_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ func Test_runListActiveVals(t *testing.T) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", 0))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
}

require.NoError(t, runListActiveValidatorsCmd(ctx, config))
Expand Down Expand Up @@ -143,12 +143,12 @@ func Test_listActiveVals(t *testing.T) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", 0))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
}

vals, err := listActiveVals(ctx, config)
Expand Down Expand Up @@ -184,12 +184,12 @@ func Test_listActiveVals(t *testing.T) {
baseDir := filepath.Join(root, fmt.Sprintf("op%d", 0))

config := exitConfig{
BeaconNodeURL: beaconMock.Address(),
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
BeaconNodeEndpoints: []string{beaconMock.Address()},
PrivateKeyPath: filepath.Join(baseDir, "charon-enr-private-key"),
ValidatorKeysDir: filepath.Join(baseDir, "validator_keys"),
LockFilePath: filepath.Join(baseDir, "cluster-lock.json"),
PlaintextOutput: true,
BeaconNodeTimeout: 30 * time.Second,
}

vals, err := listActiveVals(ctx, config)
Expand Down
Loading

0 comments on commit 32795a0

Please sign in to comment.