Skip to content

Commit

Permalink
cmd: refactor exits (#3248)
Browse files Browse the repository at this point in the history
Refactor exits logic. There is no change in the logic anywhere. The main differences are:
- fetching the validator index and validator pub key are in separate functions, which makes it more readable what is the flow
- `ExpertMode` is renamed to `SkipBeaconNodeCheck` as this is essentially what it does

If others agree, I'm in favour of completely removing the `SkipBeaconNodeCheck` config, as it is unnecessary.

category: refactor
ticket: none
  • Loading branch information
KaloyanTanev authored Sep 4, 2024
1 parent 92ce181 commit 3d6c2f0
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 74 deletions.
2 changes: 1 addition & 1 deletion cmd/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type exitConfig struct {
ValidatorPubkey string
ValidatorIndex uint64
ValidatorIndexPresent bool
ExpertMode bool
SkipBeaconNodeCheck bool
PrivateKeyPath string
ValidatorKeysDir string
LockFilePath string
Expand Down
146 changes: 79 additions & 67 deletions cmd/exit_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ package cmd
import (
"context"
"fmt"
"strings"

eth2api "github.com/attestantio/go-eth2-client/api"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
libp2plog "github.com/ipfs/go-log/v2"
"github.com/spf13/cobra"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/eth2wrap"
"github.com/obolnetwork/charon/app/k1util"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/obolapi"
Expand Down Expand Up @@ -60,13 +60,13 @@ func newSubmitPartialExitCmd(runFunc func(context.Context, exitConfig) error) *c
valIdxPresent := cmd.Flags().Lookup(validatorIndex.String()).Changed
valPubkPresent := cmd.Flags().Lookup(validatorPubkey.String()).Changed

if strings.TrimSpace(config.ValidatorPubkey) == "" && !valIdxPresent {
if !valPubkPresent && !valIdxPresent {
//nolint:revive // we use our own version of the errors package.
return errors.New(fmt.Sprintf("either %s or %s must be specified at least.", validatorIndex.String(), validatorPubkey.String()))
}

config.ValidatorIndexPresent = valIdxPresent
config.ExpertMode = valIdxPresent && valPubkPresent
config.SkipBeaconNodeCheck = valIdxPresent && valPubkPresent

return nil
})
Expand Down Expand Up @@ -100,110 +100,122 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error {
return errors.Wrap(err, "could not match local validator key shares with their counterparty in cluster lock")
}

validator := core.PubKey(config.ValidatorPubkey)
shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey())
if err != nil {
return errors.Wrap(err, "could not determine operator index from cluster lock for supplied identity key")
}

valEth2, err := validator.ToETH2()
oAPI, err := obolapi.New(config.PublishAddress, obolapi.WithTimeout(config.PublishTimeout))
if err != nil {
if (strings.TrimSpace(config.ValidatorPubkey) != "" && !config.ValidatorIndexPresent) || config.ExpertMode {
return errors.Wrap(err, "cannot convert validator pubkey to bytes")
}
return errors.Wrap(err, "could not create obol api client")
}

switch {
case config.ExpertMode:
ctx = log.WithCtx(ctx, z.U64("validator_index", config.ValidatorIndex), z.Str("validator", validator.String()))
case config.ValidatorIndexPresent && !config.ExpertMode:
eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
if err != nil {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}

if config.ValidatorIndexPresent {
ctx = log.WithCtx(ctx, z.U64("validator_index", config.ValidatorIndex))
default:
ctx = log.WithCtx(ctx, z.Str("validator", validator.String()))
}
if config.ValidatorPubkey != "" {
ctx = log.WithCtx(ctx, z.Str("validator_pubkey", config.ValidatorPubkey))
}

shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey())
if config.SkipBeaconNodeCheck {
log.Info(ctx, "Both public key and index are specified, beacon node won't be checked for validator existence/liveness")
}

valEth2, err := fetchValidatorBLSPubKey(ctx, config, eth2Cl)
if err != nil {
return errors.Wrap(err, "could not determine operator index from cluster lock for supplied identity key")
return errors.Wrap(err, "cannot fetch validator public key")
}

validator := core.PubKeyFrom48Bytes(valEth2)

ourShare, ok := shares[validator]
if !ok {
if (strings.TrimSpace(config.ValidatorPubkey) != "" && !config.ValidatorIndexPresent) || config.ExpertMode {
return errors.New("validator not present in cluster lock", z.Str("validator", validator.String()))
}
return errors.New("validator not present in cluster lock", z.Str("validator", validator.String()))
}

oAPI, err := obolapi.New(config.PublishAddress, obolapi.WithTimeout(config.PublishTimeout))
valIndex, err := fetchValidatorIndex(ctx, config, eth2Cl)
if err != nil {
return errors.Wrap(err, "could not create obol api client")
return errors.Wrap(err, "cannot fetch validator index")
}

log.Info(ctx, "Signing exit message for validator")

var valIndex eth2p0.ValidatorIndex
var valIndexFound bool

valAPICallOpts := &eth2api.ValidatorsOpts{
State: "head",
exitMsg, err := signExit(ctx, eth2Cl, valIndex, ourShare.Share, eth2p0.Epoch(config.ExitEpoch))
if err != nil {
return errors.Wrap(err, "cannot sign partial exit message")
}

if config.ValidatorIndexPresent {
valAPICallOpts.Indices = []eth2p0.ValidatorIndex{
eth2p0.ValidatorIndex(config.ValidatorIndex),
}
valIndex = eth2p0.ValidatorIndex(config.ValidatorIndex)
} else {
valAPICallOpts.PubKeys = []eth2p0.BLSPubKey{
valEth2,
}
exitBlob := obolapi.ExitBlob{
PublicKey: valEth2.String(),
SignedExitMessage: exitMsg,
}

eth2Cl, err := eth2Client(ctx, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion()))
if err != nil {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
if err := oAPI.PostPartialExit(ctx, cl.GetInitialMutationHash(), shareIdx, identityKey, exitBlob); err != nil {
return errors.Wrap(err, "could not POST partial exit message to Obol API")
}

if !config.ExpertMode {
rawValData, err := eth2Cl.Validators(ctx, valAPICallOpts)
return nil
}

func fetchValidatorBLSPubKey(ctx context.Context, config exitConfig, eth2Cl eth2wrap.Client) (eth2p0.BLSPubKey, error) {
if config.ValidatorPubkey != "" {
valEth2, err := core.PubKey(config.ValidatorPubkey).ToETH2()
if err != nil {
return errors.Wrap(err, "cannot fetch validator")
return eth2p0.BLSPubKey{}, errors.Wrap(err, "cannot convert validator pubkey to bytes")
}

valData := rawValData.Data
return valEth2, nil
}

for _, val := range valData {
if val.Validator.PublicKey == valEth2 || val.Index == eth2p0.ValidatorIndex(config.ValidatorIndex) {
valIndex = val.Index
valIndexFound = true
valAPICallOpts := &eth2api.ValidatorsOpts{
State: "head",
Indices: []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(config.ValidatorIndex)},
}

// re-initialize state variable after looking up all the necessary details, since user only provided a validator index
if config.ValidatorIndexPresent {
valEth2 = val.Validator.PublicKey
ourShare, ok = shares[core.PubKeyFrom48Bytes(valEth2)]
if !ok && !config.ValidatorIndexPresent {
return errors.New("validator not present in cluster lock", z.U64("validator_index", config.ValidatorIndex), z.Str("validator", validator.String()))
}
}
rawValData, err := eth2Cl.Validators(ctx, valAPICallOpts)
if err != nil {
return eth2p0.BLSPubKey{}, errors.Wrap(err, "cannot fetch validators")
}

break
}
for _, val := range rawValData.Data {
if val.Index == eth2p0.ValidatorIndex(config.ValidatorIndex) {
return val.Validator.PublicKey, nil
}
}

if !valIndexFound {
return errors.New("validator index not found in beacon node response")
}
return eth2p0.BLSPubKey{}, errors.New("validator index not found in beacon node response")
}

func fetchValidatorIndex(ctx context.Context, config exitConfig, eth2Cl eth2wrap.Client) (eth2p0.ValidatorIndex, error) {
if config.ValidatorIndexPresent {
return eth2p0.ValidatorIndex(config.ValidatorIndex), nil
}

exitMsg, err := signExit(ctx, eth2Cl, valIndex, ourShare.Share, eth2p0.Epoch(config.ExitEpoch))
valEth2, err := core.PubKey(config.ValidatorPubkey).ToETH2()
if err != nil {
return errors.Wrap(err, "cannot sign partial exit message")
return 0, errors.Wrap(err, "cannot convert validator pubkey to bytes")
}

exitBlob := obolapi.ExitBlob{
PublicKey: valEth2.String(),
SignedExitMessage: exitMsg,
valAPICallOpts := &eth2api.ValidatorsOpts{
State: "head",
PubKeys: []eth2p0.BLSPubKey{valEth2},
}

if err := oAPI.PostPartialExit(ctx, cl.GetInitialMutationHash(), shareIdx, identityKey, exitBlob); err != nil {
return errors.Wrap(err, "could not POST partial exit message to Obol API")
rawValData, err := eth2Cl.Validators(ctx, valAPICallOpts)
if err != nil {
return 0, errors.Wrap(err, "cannot fetch validators")
}

return nil
for _, val := range rawValData.Data {
if val.Validator.PublicKey == valEth2 {
return val.Index, nil
}
}

return 0, errors.New("validator public key not found in beacon node response")
}
12 changes: 6 additions & 6 deletions cmd/exit_sign_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func Test_runSubmitPartialExit(t *testing.T) {
)
})

t.Run("main flow with expert mode with bad pubkey", func(t *testing.T) {
t.Run("main flow with skipBeaconNodeCheck mode with bad pubkey", func(t *testing.T) {
runSubmitPartialExitFlowTest(
t,
true,
Expand All @@ -103,7 +103,7 @@ func Test_runSubmitPartialExit(t *testing.T) {
)
})

t.Run("main flow with expert mode with pubkey not found in cluster lock", func(t *testing.T) {
t.Run("main flow with skipBeaconNodeCheck mode with pubkey not found in cluster lock", func(t *testing.T) {
runSubmitPartialExitFlowTest(
t,
true,
Expand All @@ -120,14 +120,14 @@ func Test_runSubmitPartialExit(t *testing.T) {
t.Run("main flow with validator index", func(t *testing.T) {
runSubmitPartialExitFlowTest(t, true, false, "", 0, "")
})
t.Run("main flow with expert mode", func(t *testing.T) {
t.Run("main flow with skipBeaconNodeCheck mode", func(t *testing.T) {
runSubmitPartialExitFlowTest(t, true, true, "", 0, "")
})

t.Run("config", Test_runSubmitPartialExit_Config)
}

func runSubmitPartialExitFlowTest(t *testing.T, useValIdx bool, expertMode bool, valPubkey string, valIndex uint64, errString string) {
func runSubmitPartialExitFlowTest(t *testing.T, useValIdx bool, skipBeaconNodeCheck bool, valPubkey string, valIndex uint64, errString string) {
t.Helper()
t.Parallel()
ctx := context.Background()
Expand Down Expand Up @@ -215,11 +215,11 @@ func runSubmitPartialExitFlowTest(t *testing.T, useValIdx bool, expertMode bool,
pubkey = valPubkey
}

if expertMode {
if skipBeaconNodeCheck {
config.ValidatorIndex = index
config.ValidatorIndexPresent = true
config.ValidatorPubkey = pubkey
config.ExpertMode = true
config.SkipBeaconNodeCheck = true
} else {
if useValIdx {
config.ValidatorIndex = index
Expand Down

0 comments on commit 3d6c2f0

Please sign in to comment.