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

Export Slashing Protection History Via CLI #8040

Merged
merged 13 commits into from Dec 4, 2020
1 change: 1 addition & 0 deletions validator/BUILD.bazel
Expand Up @@ -25,6 +25,7 @@ go_library(
"//validator/accounts:go_default_library",
"//validator/flags:go_default_library",
"//validator/node:go_default_library",
"//validator/slashing-protection:go_default_library",
"@com_github_joonix_log//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
Expand Down
7 changes: 7 additions & 0 deletions validator/flags/flags.go
Expand Up @@ -282,6 +282,13 @@ var (
Usage: "Enables the web portal for the validator client (work in progress)",
Value: false,
}
// SlashingProtectionExportDirFlag allows specifying the outpt directory
// for a validator's slashing protection history.
SlashingProtectionExportDirFlag = &cli.StringFlag{
Name: "slashing-protection-export-dir",
Usage: "Allows users to specify the output directory to export their slashing protection EIP-3076 standard JSON File",
Value: "",
}
)

// DefaultValidatorDir returns OS-specific default validator directory.
Expand Down
5 changes: 3 additions & 2 deletions validator/main.go
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/prysmaticlabs/prysm/validator/accounts"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/node"
slashingprotection "github.com/prysmaticlabs/prysm/validator/slashing-protection"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
Expand Down Expand Up @@ -101,13 +102,13 @@ func init() {
func main() {
app := cli.App{}
app.Name = "validator"
app.Usage = `launches an Ethereum 2.0 validator client that interacts with a beacon chain,
starts proposer and attester services, p2p connections, and more`
app.Usage = `launches an Ethereum 2.0 validator client that interacts with a beacon chain, starts proposer and attester services, p2p connections, and more`
app.Version = version.GetVersion()
app.Action = startNode
app.Commands = []*cli.Command{
accounts.WalletCommands,
accounts.AccountCommands,
slashingprotection.Commands,
}

app.Flags = appFlags
Expand Down
23 changes: 22 additions & 1 deletion validator/slashing-protection/BUILD.bazel
Expand Up @@ -4,6 +4,8 @@ load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"cli_export.go",
"cmd.go",
"external.go",
"protector.go",
"slasher_client.go",
Expand All @@ -12,13 +14,22 @@ go_library(
visibility = ["//validator:__subpackages__"],
deps = [
"//proto/slashing:go_default_library",
"//shared/cmd:go_default_library",
"//shared/featureconfig:go_default_library",
"//shared/fileutil:go_default_library",
"//shared/grpcutils:go_default_library",
"//shared/tos:go_default_library",
"//validator/db/kv:go_default_library",
"//validator/flags:go_default_library",
"//validator/slashing-protection/local/standard-protection-format:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_middleware//retry:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_middleware//tracing/opentracing:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_prometheus//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@io_opencensus_go//plugin/ocgrpc:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//connectivity:go_default_library",
Expand All @@ -29,12 +40,22 @@ go_library(

go_test(
name = "go_default_test",
srcs = ["external_test.go"],
srcs = [
"cli_export_test.go",
"external_test.go",
],
embed = [":go_default_library"],
deps = [
"//shared/bytesutil:go_default_library",
"//shared/cmd:go_default_library",
"//shared/fileutil:go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"//validator/db/testing:go_default_library",
"//validator/flags:go_default_library",
"//validator/slashing-protection/local/standard-protection-format:go_default_library",
"//validator/testing:go_default_library",
"@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
)
56 changes: 56 additions & 0 deletions validator/slashing-protection/cli_export.go
@@ -0,0 +1,56 @@
package slashingprotection

import (
"encoding/json"
"path/filepath"

"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/cmd"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/validator/db/kv"
"github.com/prysmaticlabs/prysm/validator/flags"
export "github.com/prysmaticlabs/prysm/validator/slashing-protection/local/standard-protection-format"
"github.com/urfave/cli/v2"
)

const (
jsonExportFileName = "slashing_protection.json"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do other clients call it this as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no standard for it

)

// ExportSlashingProtectionJSONCli extracts a validator's slashing protection
// history from their database and formats it into an EIP-3076 standard JSON
// file via a CLI entrypoint to make it easy to migrate machines or eth2 clients.
//
// Steps:
// 1. Parse a path to the validator's datadir from the CLI context.
// 2. Open the validator database file.
// 3. Call the function which actually exports the data from
// from the validator's db into an EIP standard slashing protection format
// 4. Format and save the JSON file to a user's specified output directory.
func ExportSlashingProtectionJSONCli(cliCtx *cli.Context) error {
datadir := cliCtx.String(cmd.DataDirFlag.Name)
validatorDB, err := kv.NewKVStore(datadir, nil)
if err != nil {
return errors.Wrapf(err, "could not open database at %s", datadir)
}
eipJSON, err := export.ExportStandardProtectionJSON(cliCtx.Context, validatorDB)
if err != nil {
return errors.Wrap(err, "could not export slashing protection history")
}
outputDir := cliCtx.String(flags.SlashingProtectionExportDirFlag.Name)
exists, err := fileutil.HasDir(outputDir)
if err != nil {
return errors.Wrapf(err, "could not check if output directory %s already exists", outputDir)
}
if !exists {
if err := fileutil.MkdirAll(outputDir); err != nil {
return errors.Wrapf(err, "could not create output directory %s", outputDir)
}
}
outputFilePath := filepath.Join(outputDir, jsonExportFileName)
encoded, err := json.MarshalIndent(eipJSON, "", "\t")
if err != nil {
return errors.Wrap(err, "could not JSON marshal slashing protection history")
}
return fileutil.WriteFile(outputFilePath, encoded)
}
94 changes: 94 additions & 0 deletions validator/slashing-protection/cli_export_test.go
@@ -0,0 +1,94 @@
package slashingprotection

import (
"bytes"
"context"
"encoding/json"
"flag"
"os"
"path/filepath"
"testing"

"github.com/prysmaticlabs/prysm/shared/cmd"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
dbTest "github.com/prysmaticlabs/prysm/validator/db/testing"
"github.com/prysmaticlabs/prysm/validator/flags"
protectionFormat "github.com/prysmaticlabs/prysm/validator/slashing-protection/local/standard-protection-format"
mocks "github.com/prysmaticlabs/prysm/validator/testing"
"github.com/urfave/cli/v2"
)

func setupCliCtx(
tb testing.TB,
dbPath string,
outputDir string,
) *cli.Context {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String(cmd.DataDirFlag.Name, dbPath, "")
set.String(flags.SlashingProtectionExportDirFlag.Name, outputDir, "")
assert.NoError(tb, set.Set(cmd.DataDirFlag.Name, dbPath))
assert.NoError(tb, set.Set(flags.SlashingProtectionExportDirFlag.Name, outputDir))
return cli.NewContext(&app, set, nil)
}

func TestExportSlashingProtectionCli(t *testing.T) {
ctx := context.Background()
numValidators := 10

// Create some mock slashing protection history. and JSON file
pubKeys, err := mocks.CreateRandomPubKeys(numValidators)
require.NoError(t, err)
attestingHistory, proposalHistory, err := mocks.MockAttestingAndProposalHistories(numValidators)
require.NoError(t, err)
mockJSON, err := mocks.MockSlashingProtectionJSON(pubKeys, attestingHistory, proposalHistory)
require.NoError(t, err)

// We JSON encode the protection file and import it into our database.
encoded, err := json.Marshal(mockJSON)
require.NoError(t, err)
buf := bytes.NewBuffer(encoded)

validatorDB := dbTest.SetupDB(t, pubKeys)
err = protectionFormat.ImportStandardProtectionJSON(ctx, validatorDB, buf)
require.NoError(t, err)
require.NoError(t, validatorDB.Close())

// We export our slashing protection history by creating a CLI context
// with the required values, such as the database datadir and output directory.
dbPath := validatorDB.DatabasePath()
outputPath := filepath.Join(os.TempDir(), "slashing-exports")
cliCtx := setupCliCtx(t, dbPath, outputPath)
err = ExportSlashingProtectionJSONCli(cliCtx)
require.NoError(t, err)

// Attempt to read the exported file from the output directory.
enc, err := fileutil.ReadFileAsBytes(filepath.Join(outputPath, jsonExportFileName))
require.NoError(t, err)

receivedJSON := &protectionFormat.EIPSlashingProtectionFormat{}
err = json.Unmarshal(enc, receivedJSON)
require.NoError(t, err)

// We verify the parsed JSON file matches. Given there is no guarantee of order,
// we will have to carefully compare and sort values as needed.
//
// First, we compare basic data such as the Metadata value in the JSON file.
require.DeepEqual(t, mockJSON.Metadata, receivedJSON.Metadata)
wantedHistoryByPublicKey := make(map[string]*protectionFormat.ProtectionData)
for _, item := range mockJSON.Data {
wantedHistoryByPublicKey[item.Pubkey] = item
}

// Next, we compare all the data for each validator public key.
for _, item := range receivedJSON.Data {
wanted, ok := wantedHistoryByPublicKey[item.Pubkey]
require.Equal(t, true, ok)
require.Equal(t, len(wanted.SignedBlocks), len(item.SignedBlocks))
require.Equal(t, len(wanted.SignedAttestations), len(item.SignedAttestations))
require.DeepEqual(t, wanted.SignedBlocks, item.SignedBlocks)
require.DeepEqual(t, wanted.SignedAttestations, item.SignedAttestations)
}
}
36 changes: 36 additions & 0 deletions validator/slashing-protection/cmd.go
@@ -0,0 +1,36 @@
package slashingprotection

import (
"github.com/prysmaticlabs/prysm/shared/cmd"
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/shared/tos"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/urfave/cli/v2"
)

// Commands for slashing protection.
var Commands = &cli.Command{
Name: "slashing-protection",
Category: "slashing-protection",
Usage: "defines commands for interacting your validator's slashing protection history",
Subcommands: []*cli.Command{
{
Name: "export",
Description: `exports your validator slashing protection history into an EIP-3076 compliant JSON`,
Flags: cmd.WrapFlags([]cli.Flag{
cmd.DataDirFlag,
flags.SlashingProtectionExportDirFlag,
}),
Before: func(cliCtx *cli.Context) error {
if err := cmd.LoadFlagsFromConfig(cliCtx, cliCtx.Command.Flags); err != nil {
return err
}
return tos.VerifyTosAcceptedOrPrompt(cliCtx)
},
Action: func(cliCtx *cli.Context) error {
featureconfig.ConfigureValidator(cliCtx)
return ExportSlashingProtectionJSONCli(cliCtx)
},
},
},
}
Expand Up @@ -34,14 +34,13 @@ go_test(
embed = [":go_default_library"],
deps = [
"//shared/bls:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/hashutil:go_default_library",
"//shared/params:go_default_library",
"//shared/rand:go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"//validator/db/kv:go_default_library",
"//validator/db/testing:go_default_library",
"//validator/testing:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
],
)