diff --git a/slasher/BUILD.bazel b/slasher/BUILD.bazel index 152b58a5d..49530afce 100644 --- a/slasher/BUILD.bazel +++ b/slasher/BUILD.bazel @@ -1,47 +1,37 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ - "server.go", - "service.go", + "main.go", + "usage.go", ], importpath = "github.com/prysmaticlabs/prysm/slasher", visibility = ["//slasher:__subpackages__"], deps = [ - "//beacon-chain/core/helpers:go_default_library", - "//proto/eth/v1alpha1:go_default_library", - "//slasher/db:go_default_library", - "//slasher/rpc:go_default_library", - "@com_github_gogo_protobuf//proto:go_default_library", - "@com_github_gogo_protobuf//types:go_default_library", - "@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library", - "@com_github_grpc_ecosystem_go_grpc_middleware//recovery:go_default_library", - "@com_github_grpc_ecosystem_go_grpc_prometheus//:go_default_library", - "@com_github_pkg_errors//:go_default_library", + "//shared/cmd:go_default_library", + "//shared/debug:go_default_library", + "//shared/logutil:go_default_library", + "//shared/version:go_default_library", + "//slasher/flags:go_default_library", + "//slasher/service:go_default_library", + "@com_github_joonix_log//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", - "@io_opencensus_go//plugin/ocgrpc:go_default_library", - "@org_golang_google_grpc//:go_default_library", - "@org_golang_google_grpc//codes:go_default_library", - "@org_golang_google_grpc//credentials:go_default_library", - "@org_golang_google_grpc//reflection:go_default_library", - "@org_golang_google_grpc//status:go_default_library", + "@com_github_urfave_cli//:go_default_library", + "@com_github_x_cray_logrus_prefixed_formatter//:go_default_library", ], ) go_test( name = "go_default_test", - srcs = [ - "server_test.go", - "service_test.go", - ], + size = "small", + srcs = ["usage_test.go"], embed = [":go_default_library"], - deps = [ - "//proto/eth/v1alpha1:go_default_library", - "//shared/testutil:go_default_library", - "//slasher/db:go_default_library", - "@com_github_gogo_protobuf//proto:go_default_library", - "@com_github_sirupsen_logrus//:go_default_library", - "@com_github_sirupsen_logrus//hooks/test:go_default_library", - ], + deps = ["@com_github_urfave_cli//:go_default_library"], +) + +go_binary( + name = "slasher", + embed = [":go_default_library"], + visibility = ["//visibility:public"], ) diff --git a/slasher/README.md b/slasher/README.md new file mode 100644 index 000000000..062feed12 --- /dev/null +++ b/slasher/README.md @@ -0,0 +1,13 @@ +# Prysmatic Labs Hash Slinging Slasher Server Implementation + +This is the main project folder for a slasher server implementation of Ethereum Serenity in Golang by [Prysmatic Labs](https://prysmaticlabs.com). A slasher listens to queries from a running beacon node in order to detect slashable attestations and block proposals. +It is advised to run the slasher in a closed network and let only your beacon node connect to it while not exposing its endpoints to the public network as DOS attacks on the slasher are easy to accomplish as the lookup for certain can have serious overhead if spammed. + +Before you begin, check out our main [README](https://github.com/prysmaticlabs/prysm/blob/master/README.md) and join our active chat room on Discord or Gitter below: + +[![Discord](https://user-images.githubusercontent.com/7288322/34471967-1df7808a-efbb-11e7-9088-ed0b04151291.png)](https://discord.gg/KSA7rPr) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prysmaticlabs/prysm?badge&utm_medium=badge&utm_campaign=pr-badge) + +Also, read the latest sharding + casper [design spec](https://github.com/ethereum/eth2.0-specs), this design spec serves as a source of truth for the beacon chain implementation we follow at prysmatic labs. +Check out the [FAQs](https://notes.ethereum.org/9MMuzWeFTTSg-3Tz_YeiBA?view). Refer this page on [why](http://email.mg2.substack.com/c/eJwlj9GOhCAMRb9G3jRQQPGBh5mM8xsbhKrsDGIAM9m_X9xN2qZtbpt7rCm4xvSjj5gLOTOmL-809CMbKXFaOKakIl4DZYr2AGyQIGjHOnWH22OiYnoIxmDijaBhhS6fcy7GvjobA9m0mSXOcnZq5GBqLkilXBZhBsus5ZK89VbKkRt-a-BZI6DzZ7iur1lQ953KJ9bemnxgahuQU9XJu6pFPdu8meT8vragzEjpMCwMGLlgLo6h5z1JumQTu4IJd4v15xqMf_8ZLP_Y1bSLdbnrD-LL71i2Kj7DLxaWWF4) +we are combining sharding and casper together. diff --git a/slasher/flags/BUILD.bazel b/slasher/flags/BUILD.bazel new file mode 100644 index 000000000..556d00eff --- /dev/null +++ b/slasher/flags/BUILD.bazel @@ -0,0 +1,9 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["flags.go"], + importpath = "github.com/prysmaticlabs/prysm/slasher/flags", + visibility = ["//visibility:public"], + deps = ["@com_github_urfave_cli//:go_default_library"], +) diff --git a/slasher/flags/flags.go b/slasher/flags/flags.go new file mode 100644 index 000000000..745310a53 --- /dev/null +++ b/slasher/flags/flags.go @@ -0,0 +1,24 @@ +package flags + +import ( + "github.com/urfave/cli" +) + +var ( + // CertFlag defines a flag for the node's TLS certificate. + CertFlag = cli.StringFlag{ + Name: "tls-cert", + Usage: "Certificate for secure gRPC. Pass this and the tls-key flag in order to use gRPC securely.", + } + // RPCPort defines a slasher node RPC port to open. + RPCPort = cli.IntFlag{ + Name: "rpc-port", + Usage: "RPC port exposed by a beacon node", + Value: 5000, + } + // KeyFlag defines a flag for the node's TLS key. + KeyFlag = cli.StringFlag{ + Name: "tls-key", + Usage: "Key for secure gRPC. Pass this and the tls-cert flag in order to use gRPC securely.", + } +) diff --git a/slasher/main.go b/slasher/main.go new file mode 100644 index 000000000..927726907 --- /dev/null +++ b/slasher/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "os" + "runtime" + + joonix "github.com/joonix/log" + "github.com/prysmaticlabs/prysm/shared/cmd" + "github.com/prysmaticlabs/prysm/shared/debug" + "github.com/prysmaticlabs/prysm/shared/logutil" + "github.com/prysmaticlabs/prysm/shared/version" + "github.com/prysmaticlabs/prysm/slasher/flags" + "github.com/prysmaticlabs/prysm/slasher/service" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" + prefixed "github.com/x-cray/logrus-prefixed-formatter" +) + +var log = logrus.WithField("prefix", "main") + +func startSlasher(ctx *cli.Context) error { + verbosity := ctx.GlobalString(cmd.VerbosityFlag.Name) + level, err := logrus.ParseLevel(verbosity) + if err != nil { + return err + } + logrus.SetLevel(level) + port := ctx.GlobalString(flags.RPCPort.Name) + cert := ctx.GlobalString(flags.CertFlag.Name) + key := ctx.GlobalString(flags.KeyFlag.Name) + cfg := service.Config{ + Port: port, + CertFlag: cert, + KeyFlag: key, + } + slasher, err := service.NewRPCService(&cfg, ctx) + if err != nil { + return err + } + slasher.Start() + return nil +} + +var appFlags = []cli.Flag{ + cmd.VerbosityFlag, + cmd.LogFormat, + cmd.DataDirFlag, + cmd.VerbosityFlag, + cmd.DataDirFlag, + cmd.EnableTracingFlag, + cmd.TracingProcessNameFlag, + cmd.TracingEndpointFlag, + cmd.TraceSampleFractionFlag, + cmd.BootstrapNode, + cmd.MonitoringPortFlag, + cmd.LogFileName, + cmd.LogFormat, + debug.PProfFlag, + debug.PProfAddrFlag, + debug.PProfPortFlag, + debug.MemProfileRateFlag, + debug.CPUProfileFlag, + debug.TraceFlag, + flags.CertFlag, + flags.RPCPort, + flags.KeyFlag, +} + +func init() { + +} + +func main() { + app := cli.NewApp() + app.Name = "hash slinging slasher" + app.Usage = `launches an Ethereum Serenity slasher server that interacts with a beacon chain.` + app.Version = version.GetVersion() + app.Action = startSlasher + app.Flags = appFlags + + app.Before = func(ctx *cli.Context) error { + format := ctx.GlobalString(cmd.LogFormat.Name) + switch format { + case "text": + formatter := new(prefixed.TextFormatter) + formatter.TimestampFormat = "2006-01-02 15:04:05" + formatter.FullTimestamp = true + // If persistent log files are written - we disable the log messages coloring because + // the colors are ANSI codes and seen as Gibberish in the log files. + formatter.DisableColors = ctx.GlobalString(cmd.LogFileName.Name) != "" + logrus.SetFormatter(formatter) + break + case "fluentd": + logrus.SetFormatter(joonix.NewFormatter()) + break + case "json": + logrus.SetFormatter(&logrus.JSONFormatter{}) + break + default: + return fmt.Errorf("unknown log format %s", format) + } + + logFileName := ctx.GlobalString(cmd.LogFileName.Name) + if logFileName != "" { + if err := logutil.ConfigurePersistentLogging(logFileName); err != nil { + log.WithError(err).Error("Failed to configuring logging to disk.") + } + } + + runtime.GOMAXPROCS(runtime.NumCPU()) + return debug.Setup(ctx) + } + + app.After = func(ctx *cli.Context) error { + debug.Exit(ctx) + return nil + } + + if err := app.Run(os.Args); err != nil { + log.Error(err.Error()) + os.Exit(1) + } +} diff --git a/slasher/rpc/server.go b/slasher/rpc/server.go index f4ad7c1f4..0d175e1e0 100644 --- a/slasher/rpc/server.go +++ b/slasher/rpc/server.go @@ -3,9 +3,10 @@ package rpc import ( "context" + "github.com/pkg/errors" + "github.com/gogo/protobuf/proto" "github.com/gogo/protobuf/types" - "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" "github.com/prysmaticlabs/prysm/slasher/db" @@ -30,6 +31,7 @@ func (ss *Server) IsSlashableAttestation(ctx context.Context, req *ethpb.Attesta // IsSlashableBlock returns a proposer slashing if the block header submitted is // a slashable proposal. func (ss *Server) IsSlashableBlock(ctx context.Context, psr *ethpb.ProposerSlashingRequest) (*ethpb.ProposerSlashingResponse, error) { + //TODO(#3133): add signature validation epoch := helpers.SlotToEpoch(psr.BlockHeader.Slot) blockHeaders, err := ss.SlasherDb.BlockHeader(epoch, psr.ValidatorIndex) if err != nil { diff --git a/slasher/rpc/server_test.go b/slasher/rpc/server_test.go index 6ef7b5af9..b3bfeed38 100644 --- a/slasher/rpc/server_test.go +++ b/slasher/rpc/server_test.go @@ -11,7 +11,6 @@ import ( func TestServer_IsSlashableBlock(t *testing.T) { dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) ctx := context.Background() slasherServer := &Server{ @@ -57,7 +56,6 @@ func TestServer_IsSlashableBlock(t *testing.T) { func TestServer_IsNotSlashableBlock(t *testing.T) { dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) slasherServer := &Server{ @@ -95,7 +93,6 @@ func TestServer_IsNotSlashableBlock(t *testing.T) { func TestServer_DoubleBlock(t *testing.T) { dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) ctx := context.Background() slasherServer := &Server{ @@ -126,7 +123,6 @@ func TestServer_DoubleBlock(t *testing.T) { func TestServer_SameEpochDifferentSlotSlashable(t *testing.T) { dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) ctx := context.Background() slasherServer := &Server{ diff --git a/slasher/server.go b/slasher/server.go deleted file mode 100644 index 93c205cd1..000000000 --- a/slasher/server.go +++ /dev/null @@ -1,66 +0,0 @@ -package slasher - -import ( - "context" - - "github.com/pkg/errors" - - "github.com/gogo/protobuf/proto" - types "github.com/gogo/protobuf/types" - "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" - ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" - "github.com/prysmaticlabs/prysm/slasher/db" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// Server defines a server implementation of the gRPC Slasher service, -// providing RPC endpoints for retrieving slashing proofs for malicious validators. -type Server struct { - slasherDb *db.Store - ctx context.Context -} - -// IsSlashableAttestation returns an attester slashing if the attestation submitted -// is a slashable vote. -func (ss *Server) IsSlashableAttestation(ctx context.Context, req *ethpb.Attestation) (*ethpb.AttesterSlashing, error) { - return nil, status.Error(codes.Unimplemented, "not implemented") -} - -// IsSlashableBlock returns a proposer slashing if the block header submitted is -// a slashable proposal. -func (ss *Server) IsSlashableBlock(ctx context.Context, psr *ethpb.ProposerSlashingRequest) (*ethpb.ProposerSlashingResponse, error) { - //TODO(#3133): add signature validation - epoch := helpers.SlotToEpoch(psr.BlockHeader.Slot) - bha, err := ss.slasherDb.BlockHeader(epoch, psr.ValidatorIndex) - - if err != nil { - return nil, errors.Wrap(err, "slasher service error while trying to retrieve blocks") - } - pSlashingsResponse := ðpb.ProposerSlashingResponse{} - presentInDb := false - for _, bh := range bha { - if proto.Equal(bh, psr.BlockHeader) { - presentInDb = true - continue - } - pSlashingsResponse.ProposerSlashing = append(pSlashingsResponse.ProposerSlashing, ðpb.ProposerSlashing{ProposerIndex: psr.ValidatorIndex, Header_1: psr.BlockHeader, Header_2: bh}) - } - if len(pSlashingsResponse.ProposerSlashing) == 0 && !presentInDb { - err = ss.slasherDb.SaveBlockHeader(epoch, psr.ValidatorIndex, psr.BlockHeader) - if err != nil { - return nil, err - } - } - return pSlashingsResponse, nil -} - -// SlashableProposals is a subscription to receive all slashable proposer slashing events found by the watchtower. -func (ss *Server) SlashableProposals(req *types.Empty, server ethpb.Slasher_SlashableProposalsServer) error { - return status.Error(codes.Unimplemented, "not implemented") -} - -// SlashableAttestations is a subscription to receive all slashable attester slashing events found by the watchtower. -func (ss *Server) SlashableAttestations(req *types.Empty, server ethpb.Slasher_SlashableAttestationsServer) error { - return status.Error(codes.Unimplemented, "not implemented") -} diff --git a/slasher/server_test.go b/slasher/server_test.go deleted file mode 100644 index 61b8a5334..000000000 --- a/slasher/server_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package slasher - -import ( - "context" - "testing" - - "github.com/gogo/protobuf/proto" - - ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" - "github.com/prysmaticlabs/prysm/slasher/db" -) - -func TestServer_IsSlashableBlock(t *testing.T) { - dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) - ctx := context.Background() - slasherServer := &Server{ - ctx: ctx, - slasherDb: dbs, - } - psr := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 1, - StateRoot: []byte("A"), - }, - ValidatorIndex: 1, - } - psr2 := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 1, - StateRoot: []byte("B"), - }, - ValidatorIndex: 1, - } - if _, err := slasherServer.IsSlashableBlock(ctx, psr); err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - sr, err := slasherServer.IsSlashableBlock(ctx, psr2) - if err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - want := ðpb.ProposerSlashing{ - ProposerIndex: psr.ValidatorIndex, - Header_1: psr2.BlockHeader, - Header_2: psr.BlockHeader, - } - - if len(sr.ProposerSlashing) != 1 { - t.Errorf("Should return 1 slashaing proof: %v", sr) - } - if !proto.Equal(sr.ProposerSlashing[0], want) { - t.Errorf("wanted slashing proof: %v got: %v", want, sr.ProposerSlashing[0]) - } -} - -func TestServer_IsNotSlashableBlock(t *testing.T) { - dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) - ctx := context.Background() - slasherServer := &Server{ - ctx: ctx, - slasherDb: dbs, - } - psr := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 1, - StateRoot: []byte("A"), - }, - ValidatorIndex: 1, - } - psr2 := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 65, - StateRoot: []byte("B"), - }, - ValidatorIndex: 1, - } - if _, err := slasherServer.IsSlashableBlock(ctx, psr); err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - sr, err := slasherServer.IsSlashableBlock(ctx, psr2) - if err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - if len(sr.ProposerSlashing) != 0 { - t.Errorf("Should return 0 slashaing proof: %v", sr) - } -} - -func TestServer_DoubleBlock(t *testing.T) { - dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) - ctx := context.Background() - slasherServer := &Server{ - ctx: ctx, - slasherDb: dbs, - } - psr := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 1, - StateRoot: []byte("A"), - }, - ValidatorIndex: 1, - } - if _, err := slasherServer.IsSlashableBlock(ctx, psr); err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - sr, err := slasherServer.IsSlashableBlock(ctx, psr) - if err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - if len(sr.ProposerSlashing) != 0 { - t.Errorf("Should return 0 slashaing proof: %v", sr) - } -} - -func TestServer_SameEpochDifferentSlotSlashable(t *testing.T) { - dbs := db.SetupSlasherDB(t) - defer db.TeardownSlasherDB(t, dbs) - ctx := context.Background() - slasherServer := &Server{ - ctx: ctx, - slasherDb: dbs, - } - psr := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 1, - StateRoot: []byte("A"), - }, - ValidatorIndex: 1, - } - psr2 := ðpb.ProposerSlashingRequest{ - BlockHeader: ðpb.BeaconBlockHeader{ - Slot: 63, - StateRoot: []byte("B"), - }, - ValidatorIndex: 1, - } - want := ðpb.ProposerSlashing{ - ProposerIndex: psr.ValidatorIndex, - Header_1: psr2.BlockHeader, - Header_2: psr.BlockHeader, - } - - if _, err := slasherServer.IsSlashableBlock(ctx, psr); err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - sr, err := slasherServer.IsSlashableBlock(ctx, psr2) - if err != nil { - t.Errorf("Could not call RPC method: %v", err) - } - - if len(sr.ProposerSlashing) != 1 { - t.Errorf("Should return 1 slashaing proof: %v", sr) - } - if !proto.Equal(sr.ProposerSlashing[0], want) { - t.Errorf("wanted slashing proof: %v got: %v", want, sr.ProposerSlashing[0]) - } -} diff --git a/slasher/service/BUILD.bazel b/slasher/service/BUILD.bazel new file mode 100644 index 000000000..7a731e5f4 --- /dev/null +++ b/slasher/service/BUILD.bazel @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["service.go"], + importpath = "github.com/prysmaticlabs/prysm/slasher/service", + visibility = ["//visibility:public"], + deps = [ + "//proto/eth/v1alpha1:go_default_library", + "//shared/cmd:go_default_library", + "//shared/debug:go_default_library", + "//shared/version:go_default_library", + "//slasher/db:go_default_library", + "//slasher/rpc:go_default_library", + "@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library", + "@com_github_grpc_ecosystem_go_grpc_middleware//recovery:go_default_library", + "@com_github_grpc_ecosystem_go_grpc_prometheus//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_urfave_cli//:go_default_library", + "@io_opencensus_go//plugin/ocgrpc:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//credentials:go_default_library", + "@org_golang_google_grpc//reflection:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["service_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/testutil:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", + "@com_github_urfave_cli//:go_default_library", + ], +) diff --git a/slasher/service.go b/slasher/service/service.go similarity index 56% rename from slasher/service.go rename to slasher/service/service.go index dda3d010a..cd7768189 100644 --- a/slasher/service.go +++ b/slasher/service/service.go @@ -1,25 +1,38 @@ -// Package slasher defines the service used to retrieve slashings proofs. -package slasher +// Package service defines the service used to retrieve slashings proofs and +// feed attestations and block headers into the slasher db. +package service import ( "fmt" "net" + "os" + "os/signal" + "path" + "sync" + "syscall" + + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/prysmaticlabs/prysm/shared/cmd" + "github.com/prysmaticlabs/prysm/shared/debug" + "github.com/prysmaticlabs/prysm/shared/version" + "github.com/prysmaticlabs/prysm/slasher/rpc" + "github.com/urfave/cli" + "go.opencensus.io/plugin/ocgrpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" middleware "github.com/grpc-ecosystem/go-grpc-middleware" recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" ethpb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" "github.com/prysmaticlabs/prysm/slasher/db" - "github.com/prysmaticlabs/prysm/slasher/rpc" "github.com/sirupsen/logrus" - "go.opencensus.io/plugin/ocgrpc" "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection" ) var log logrus.FieldLogger +const slasherDBName = "slasherdata" + func init() { log = logrus.WithField("prefix", "slasherRPC") } @@ -34,6 +47,9 @@ type Service struct { listener net.Listener credentialError error failStatus error + ctx *cli.Context + lock sync.RWMutex + stop chan struct{} // Channel to wait for termination notifications. } // Config options for the slasher server. @@ -46,16 +62,59 @@ type Config struct { // NewRPCService creates a new instance of a struct implementing the SlasherService // interface. -func NewRPCService(cfg *Config) *Service { - return &Service{ +func NewRPCService(cfg *Config, ctx *cli.Context) (*Service, error) { + s := &Service{ slasherDb: cfg.SlasherDb, port: cfg.Port, + withCert: cfg.CertFlag, + withKey: cfg.KeyFlag, + ctx: ctx, + stop: make(chan struct{}), } + if err := s.startDB(s.ctx); err != nil { + return nil, err + } + + return s, nil } // Start the gRPC server. func (s *Service) Start() { - log.Info("Starting service on port: %v", s.port) + s.lock.Lock() + log.WithFields(logrus.Fields{ + "version": version.GetVersion(), + }).Info("Starting hash slinging slasher node") + s.startSlasher() + stop := s.stop + s.lock.Unlock() + + go func() { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) + defer signal.Stop(sigc) + <-sigc + log.Info("Got interrupt, shutting down...") + debug.Exit(s.ctx) // Ensure trace and CPU profile data are flushed. + go s.Close() + for i := 10; i > 0; i-- { + <-sigc + if i > 1 { + log.Info("Already shutting down, interrupt more to panic", "times", i-1) + } + } + panic("Panic closing the hash slinging slasher node") + }() + + // Wait for stop channel to be closed. + select { + case <-stop: + return + default: + } + +} +func (s *Service) startSlasher() { + log.Info("Starting service on port: ", s.port) lis, err := net.Listen("tcp", fmt.Sprintf(":%s", s.port)) if err != nil { log.Errorf("Could not listen to port in Start() :%s: %v", s.port, err) @@ -108,6 +167,9 @@ func (s *Service) Start() { // Stop the service. func (s *Service) Stop() error { log.Info("Stopping service") + if s.slasherDb != nil { + s.slasherDb.Close() + } if s.listener != nil { s.grpcServer.GracefulStop() log.Debug("Initiated graceful stop of gRPC server") @@ -115,6 +177,19 @@ func (s *Service) Stop() error { return nil } +// Close handles graceful shutdown of the system. +func (s *Service) Close() { + s.lock.Lock() + defer s.lock.Unlock() + + log.Info("Stopping hash slinging slasher") + s.Stop() + if err := s.slasherDb.Close(); err != nil { + log.Errorf("Failed to close slasher database: %v", err) + } + close(s.stop) +} + // Status returns nil, credentialError or fail status. func (s *Service) Status() error { if s.credentialError != nil { @@ -125,3 +200,25 @@ func (s *Service) Status() error { } return nil } + +func (s *Service) startDB(ctx *cli.Context) error { + baseDir := ctx.GlobalString(cmd.DataDirFlag.Name) + dbPath := path.Join(baseDir, slasherDBName) + d, err := db.NewDB(dbPath) + if err != nil { + return err + } + if s.ctx.GlobalBool(cmd.ClearDB.Name) { + if err := d.ClearDB(); err != nil { + return err + } + d, err = db.NewDB(dbPath) + if err != nil { + return err + } + } + + log.WithField("path", dbPath).Info("Checking db") + s.slasherDb = d + return nil +} diff --git a/slasher/service_test.go b/slasher/service/service_test.go similarity index 68% rename from slasher/service_test.go rename to slasher/service/service_test.go index 338a92ad1..46492fbe0 100644 --- a/slasher/service_test.go +++ b/slasher/service/service_test.go @@ -1,8 +1,10 @@ -package slasher +package service import ( "errors" + "flag" "fmt" + "github.com/urfave/cli" "io/ioutil" "testing" @@ -18,29 +20,38 @@ func init() { func TestLifecycle_OK(t *testing.T) { hook := logTest.NewGlobal() - rpcService := NewRPCService(&Config{ + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + context := cli.NewContext(app, set, nil) + rpcService, err := NewRPCService(&Config{ Port: "7348", CertFlag: "alice.crt", KeyFlag: "alice.key", - }) - + }, context) + if err != nil { + t.Error("gRPC Service fail to initialize:", err) + } rpcService.Start() testutil.AssertLogsContain(t, hook, "Starting service") testutil.AssertLogsContain(t, hook, "Listening on port") - rpcService.Stop() + rpcService.Close() testutil.AssertLogsContain(t, hook, "Stopping service") } func TestRPC_BadEndpoint(t *testing.T) { hook := logTest.NewGlobal() - - rpcService := NewRPCService(&Config{ + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + context := cli.NewContext(app, set, nil) + rpcService, err := NewRPCService(&Config{ Port: "ralph merkle!!!", - }) - + }, context) + if err != nil { + t.Error("gRPC Service fail to initialize:", err) + } testutil.AssertLogsDoNotContain(t, hook, "Could not listen to port in Start()") testutil.AssertLogsDoNotContain(t, hook, "Could not load TLS keys") testutil.AssertLogsDoNotContain(t, hook, "Could not serve gRPC") @@ -50,7 +61,7 @@ func TestRPC_BadEndpoint(t *testing.T) { testutil.AssertLogsContain(t, hook, "Starting service") testutil.AssertLogsContain(t, hook, "Could not listen to port in Start()") - rpcService.Stop() + rpcService.Close() } func TestStatus_CredentialError(t *testing.T) { @@ -64,16 +75,21 @@ func TestStatus_CredentialError(t *testing.T) { func TestRPC_InsecureEndpoint(t *testing.T) { hook := logTest.NewGlobal() - rpcService := NewRPCService(&Config{ + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + context := cli.NewContext(app, set, nil) + rpcService, err := NewRPCService(&Config{ Port: "7777", - }) - + }, context) + if err != nil { + t.Error("gRPC Service fail to initialize:", err) + } rpcService.Start() testutil.AssertLogsContain(t, hook, "Starting service") testutil.AssertLogsContain(t, hook, fmt.Sprint("Listening on port")) testutil.AssertLogsContain(t, hook, "You are using an insecure gRPC connection") - rpcService.Stop() + rpcService.Close() testutil.AssertLogsContain(t, hook, "Stopping service") } diff --git a/slasher/usage.go b/slasher/usage.go new file mode 100644 index 000000000..9440fb0e1 --- /dev/null +++ b/slasher/usage.go @@ -0,0 +1,98 @@ +// This code was adapted from https://github.com/ethereum/go-ethereum/blob/master/cmd/geth/usage.go +package main + +import ( + "io" + "sort" + + "github.com/prysmaticlabs/prysm/shared/cmd" + "github.com/prysmaticlabs/prysm/shared/debug" + "github.com/prysmaticlabs/prysm/slasher/flags" + "github.com/urfave/cli" +) + +var appHelpTemplate = `NAME: + {{.App.Name}} - {{.App.Usage}} +USAGE: + {{.App.HelpName}} [options]{{if .App.Commands}} command [command options]{{end}} {{if .App.ArgsUsage}}{{.App.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .App.Version}} +AUTHOR: + {{range .App.Authors}}{{ . }}{{end}} + {{end}}{{if .App.Commands}} +GLOBAL OPTIONS: + {{range .App.Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{end}}{{if .FlagGroups}} +{{range .FlagGroups}}{{.Name}} OPTIONS: + {{range .Flags}}{{.}} + {{end}} +{{end}}{{end}}{{if .App.Copyright }} +COPYRIGHT: + {{.App.Copyright}} +VERSION: + {{.App.Version}} + {{end}}{{if len .App.Authors}} + {{end}} +` + +type flagGroup struct { + Name string + Flags []cli.Flag +} + +var appHelpFlagGroups = []flagGroup{ + { + Name: "cmd", + Flags: []cli.Flag{ + cmd.VerbosityFlag, + cmd.DataDirFlag, + cmd.EnableTracingFlag, + cmd.TracingProcessNameFlag, + cmd.TracingEndpointFlag, + cmd.TraceSampleFractionFlag, + cmd.BootstrapNode, + cmd.MonitoringPortFlag, + cmd.LogFormat, + cmd.LogFileName, + }, + }, + { + Name: "debug", + Flags: []cli.Flag{ + debug.PProfFlag, + debug.PProfAddrFlag, + debug.PProfPortFlag, + debug.MemProfileRateFlag, + debug.CPUProfileFlag, + debug.TraceFlag, + }, + }, + { + Name: "slasher", + Flags: []cli.Flag{ + flags.CertFlag, + flags.KeyFlag, + flags.RPCPort, + }, + }, +} + +func init() { + cli.AppHelpTemplate = appHelpTemplate + + type helpData struct { + App interface{} + FlagGroups []flagGroup + } + + originalHelpPrinter := cli.HelpPrinter + cli.HelpPrinter = func(w io.Writer, tmpl string, data interface{}) { + if tmpl == appHelpTemplate { + for _, group := range appHelpFlagGroups { + sort.Sort(cli.FlagsByName(group.Flags)) + } + originalHelpPrinter(w, tmpl, helpData{data, appHelpFlagGroups}) + } else { + originalHelpPrinter(w, tmpl, data) + } + } +} diff --git a/slasher/usage_test.go b/slasher/usage_test.go new file mode 100644 index 000000000..a8652d502 --- /dev/null +++ b/slasher/usage_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + + "github.com/urfave/cli" +) + +func TestAllFlagsExistInHelp(t *testing.T) { + // If this test is failing, it is because you've recently added/removed a + // flag in beacon chain main.go, but did not add/remove it to the usage.go + // flag grouping (appHelpFlagGroups). + + var helpFlags []cli.Flag + for _, group := range appHelpFlagGroups { + helpFlags = append(helpFlags, group.Flags...) + } + + for _, flag := range appFlags { + if !doesFlagExist(flag, helpFlags) { + t.Errorf("Flag %s does not exist in help/usage flags.", flag.GetName()) + } + } + + for _, flag := range helpFlags { + if !doesFlagExist(flag, appFlags) { + t.Errorf("Flag %s does not exist in main.go, "+ + "but exists in help flags", flag.GetName()) + } + } +} + +func doesFlagExist(flag cli.Flag, flags []cli.Flag) bool { + for _, f := range flags { + if f == flag { + return true + } + } + + return false +}