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

cmd: add exit commands #2934

Merged
merged 16 commits into from
Apr 3, 2024
5 changes: 3 additions & 2 deletions app/obolapi/exit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import (
"github.com/obolnetwork/charon/tbls"
"github.com/obolnetwork/charon/tbls/tblsconv"
"github.com/obolnetwork/charon/testutil/beaconmock"
"github.com/obolnetwork/charon/testutil/obolapimock"
)

const exitEpoch = eth2p0.Epoch(162304)

func TestAPIFlow(t *testing.T) {
kn := 4

handler, addLockFiles := MockServer(false)
handler, addLockFiles := obolapimock.MockServer(false)
srv := httptest.NewServer(handler)

defer srv.Close()
Expand Down Expand Up @@ -119,7 +120,7 @@ func TestAPIFlow(t *testing.T) {
func TestAPIFlowMissingSig(t *testing.T) {
kn := 4

handler, addLockFiles := MockServer(true)
handler, addLockFiles := obolapimock.MockServer(true)
srv := httptest.NewServer(handler)

defer srv.Close()
Expand Down
5 changes: 5 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
newAddValidatorsCmd(runAddValidatorsSolo),
newViewClusterManifestCmd(runViewClusterManifest),
),
newExitCmd(
newListActiveValidatorsCmd(runListActiveValidatorsCmd),
newSubmitPartialExitCmd(runSubmitPartialExit),
newBcastFullExitCmd(runBcastFullExit),
),

Check warning on line 55 in cmd/cmd.go

View check run for this annotation

Codecov / codecov/patch

cmd/cmd.go#L51-L55

Added lines #L51 - L55 were not covered by tests
newUnsafeCmd(newRunCmd(app.Run, true)),
)
}
Expand Down
114 changes: 114 additions & 0 deletions cmd/exit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package cmd

import (
"context"
"time"

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

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/eth2wrap"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/eth2util/signing"
"github.com/obolnetwork/charon/tbls"
)

type exitConfig struct {
BeaconNodeURL string
ValidatorAddr string
DataDir string
ObolAPIEndpoint string
ExitEpoch uint64

PlaintextOutput bool

Log log.Config
}

func newExitCmd(cmds ...*cobra.Command) *cobra.Command {
root := &cobra.Command{
Use: "exit",
Short: "Exit a distributed validator.",
Long: "Exit a distributed validator through the Obol API.",
}

root.AddCommand(cmds...)

return root

Check warning on line 41 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L32-L41

Added lines #L32 - L41 were not covered by tests
}

func bindGenericExitFlags(cmd *cobra.Command, config *exitConfig) {
cmd.Flags().StringVar(&config.ObolAPIEndpoint, "obol-api-endpoint", "https://api.obol.tech", "Endpoint of the Obol API instance.")
cmd.Flags().StringVar(&config.BeaconNodeURL, "beacon-node-url", "", "Beacon node URL.")
cmd.Flags().StringVar(&config.DataDir, "data-dir", ".charon", "The directory where charon will read lock file and partial validator keys.")

mustMarkFlagRequired(cmd, "beacon-node-url")

Check warning on line 49 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L44-L49

Added lines #L44 - L49 were not covered by tests
}

func bindExitRelatedFlags(cmd *cobra.Command, config *exitConfig) {
cmd.Flags().StringVar(&config.ValidatorAddr, "validator-address", "", "Validator to exit, must be present in the cluster lock manifest.")
cmd.Flags().Uint64Var(&config.ExitEpoch, "exit-epoch", 162304, "Exit epoch at which the validator will exit, must be the same across all the partial exits.")

mustMarkFlagRequired(cmd, "validator-address")

Check warning on line 56 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L52-L56

Added lines #L52 - L56 were not covered by tests
}

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

bnClient := bnHTTPClient.(*eth2http.Service)

return eth2wrap.AdaptEth2HTTP(bnClient, 10*time.Second), nil
}

// signExit signs a voluntary exit message for valIdx with the given keyShare.
func signExit(ctx context.Context, eth2Cl eth2wrap.Client, valIdx eth2p0.ValidatorIndex, keyShare tbls.PrivateKey, exitEpoch eth2p0.Epoch) (eth2p0.SignedVoluntaryExit, error) {
exit := &eth2p0.VoluntaryExit{
Epoch: exitEpoch,
ValidatorIndex: valIdx,
}

sigData, err := sigDataForExit(ctx, *exit, eth2Cl, exitEpoch)
if err != nil {
return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "exit hash tree root")
}

Check warning on line 83 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L82-L83

Added lines #L82 - L83 were not covered by tests

sig, err := tbls.Sign(keyShare, sigData[:])
if err != nil {
return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "signing error")
}

Check warning on line 88 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L87-L88

Added lines #L87 - L88 were not covered by tests

return eth2p0.SignedVoluntaryExit{
Message: exit,
Signature: eth2p0.BLSSignature(sig),
}, nil
}

// sigDataForExit returns the hash tree root for the given exit message, at the given exit epoch.
func sigDataForExit(ctx context.Context, exit eth2p0.VoluntaryExit, eth2Cl eth2wrap.Client, exitEpoch eth2p0.Epoch) ([32]byte, error) {
sigRoot, err := exit.HashTreeRoot()
if err != nil {
return [32]byte{}, errors.Wrap(err, "exit hash tree root")
}

Check warning on line 101 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L100-L101

Added lines #L100 - L101 were not covered by tests

domain, err := signing.GetDomain(ctx, eth2Cl, signing.DomainExit, exitEpoch)
if err != nil {
return [32]byte{}, errors.Wrap(err, "get domain")
OisinKyne marked this conversation as resolved.
Show resolved Hide resolved
}

Check warning on line 106 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L105-L106

Added lines #L105 - L106 were not covered by tests

sigData, err := (&eth2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot()
if err != nil {
return [32]byte{}, errors.Wrap(err, "signing data hash tree root")
gsora marked this conversation as resolved.
Show resolved Hide resolved
}

Check warning on line 111 in cmd/exit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit.go#L110-L111

Added lines #L110 - L111 were not covered by tests

return sigData, nil
}
130 changes: 130 additions & 0 deletions cmd/exit_fullexit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package cmd

import (
"context"
"path/filepath"

libp2plog "github.com/ipfs/go-log/v2"
"github.com/spf13/cobra"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/k1util"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/obolapi"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/eth2util/keystore"
"github.com/obolnetwork/charon/tbls"
"github.com/obolnetwork/charon/tbls/tblsconv"
)

func newBcastFullExitCmd(runFunc func(context.Context, exitConfig) error) *cobra.Command {
var config exitConfig

cmd := &cobra.Command{
Use: "broadcast",
Short: "Broadcast exit",
Long: `Broadcasts a full exit message, aggregated with the available partial signatures retrieved from Obol API.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := log.InitLogger(config.Log); err != nil {
return err
}
libp2plog.SetPrimaryCore(log.LoggerCore()) // Set libp2p logger to use charon logger

printFlags(cmd.Context(), cmd.Flags())

return runFunc(cmd.Context(), config)

Check warning on line 39 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L23-L39

Added lines #L23 - L39 were not covered by tests
},
}

bindGenericExitFlags(cmd, &config)
bindExitRelatedFlags(cmd, &config)
bindLogFlags(cmd.Flags(), &config.Log)

return cmd

Check warning on line 47 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L43-L47

Added lines #L43 - L47 were not covered by tests
}

func runBcastFullExit(ctx context.Context, config exitConfig) error {
lockFilePath := filepath.Join(config.DataDir, "cluster-lock.json")
manifestFilePath := filepath.Join(config.DataDir, "cluster-manifest.pb")
identityKeyPath := filepath.Join(config.DataDir, "charon-enr-private-key")

identityKey, err := k1util.Load(identityKeyPath)
if err != nil {
return errors.Wrap(err, "could not load identity key")
}

cl, err := loadClusterManifest(manifestFilePath, lockFilePath)
if err != nil {
return errors.Wrap(err, "could not load cluster data")
}

validator := core.PubKey(config.ValidatorAddr)
if _, err := validator.Bytes(); err != nil {
return errors.Wrap(err, "cannot convert validator pubkey to bytes")
}

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

eth2Cl, err := eth2Client(ctx, config.BeaconNodeURL)
if err != nil {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}

oAPI, err := obolapi.New(config.ObolAPIEndpoint)
if err != nil {
return errors.Wrap(err, "could not create obol api client")
}

log.Info(ctx, "Retrieving full exit message")

shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey())
if err != nil {
return errors.Wrap(err, "could not load share index from cluster lock")
}

Check warning on line 87 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L86-L87

Added lines #L86 - L87 were not covered by tests

fullExit, err := oAPI.GetFullExit(ctx, config.ValidatorAddr, cl.GetInitialMutationHash(), shareIdx, identityKey)
if err != nil {
return errors.Wrap(err, "could not load full exit data from Obol API")
}

Check warning on line 92 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L91-L92

Added lines #L91 - L92 were not covered by tests

// parse validator public key
rawPkBytes, err := validator.Bytes()
if err != nil {
return errors.Wrap(err, "could not serialize validator key bytes")
}

Check warning on line 98 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L97-L98

Added lines #L97 - L98 were not covered by tests

pubkey, err := tblsconv.PubkeyFromBytes(rawPkBytes)
if err != nil {
return errors.Wrap(err, "could not convert validator key bytes to BLS public key")
}

Check warning on line 103 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L102-L103

Added lines #L102 - L103 were not covered by tests

// parse signature
signature, err := tblsconv.SignatureFromBytes(fullExit.SignedExitMessage.Signature[:])
if err != nil {
return errors.Wrap(err, "could not parse BLS signature from bytes")
}

Check warning on line 109 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L108-L109

Added lines #L108 - L109 were not covered by tests

exitRoot, err := sigDataForExit(
ctx,
*fullExit.SignedExitMessage.Message,
eth2Cl,
fullExit.SignedExitMessage.Message.Epoch,
)
if err != nil {
return errors.Wrap(err, "cannot calculate hash tree root for exit message for verification")
}

Check warning on line 119 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L118-L119

Added lines #L118 - L119 were not covered by tests

if err := tbls.Verify(pubkey, exitRoot[:], signature); err != nil {
return errors.Wrap(err, "exit message signature not verified")
}

Check warning on line 123 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L122-L123

Added lines #L122 - L123 were not covered by tests

if err := eth2Cl.SubmitVoluntaryExit(ctx, &fullExit.SignedExitMessage); err != nil {
return errors.Wrap(err, "could submit voluntary exit")
gsora marked this conversation as resolved.
Show resolved Hide resolved
}

Check warning on line 127 in cmd/exit_fullexit.go

View check run for this annotation

Codecov / codecov/patch

cmd/exit_fullexit.go#L126-L127

Added lines #L126 - L127 were not covered by tests

return nil
}
Loading
Loading