diff --git a/tools/forkchecker/BUILD.bazel b/tools/forkchecker/BUILD.bazel new file mode 100644 index 0000000000..90259ca15c --- /dev/null +++ b/tools/forkchecker/BUILD.bazel @@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["forkchecker.go"], + importpath = "github.com/prysmaticlabs/prysm/tools/forkchecker", + visibility = ["//visibility:private"], + deps = [ + "//proto/eth/v1alpha1:go_default_library", + "//shared/params:go_default_library", + "@com_github_gogo_protobuf//types:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@org_golang_google_grpc//:go_default_library", + ], +) + +go_binary( + name = "forkchecker", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/tools/forkchecker/forkchecker.go b/tools/forkchecker/forkchecker.go new file mode 100644 index 0000000000..1d99a6fdcd --- /dev/null +++ b/tools/forkchecker/forkchecker.go @@ -0,0 +1,152 @@ +/** + * Fork choice checker + * + * A gRPC client that polls beacon node at every slot to log or compare nodes current head. + * + * Example: 2 beacon nodes with 2 gRPC end points, 127.0.0.1:4000 and 127.0.0.1:4001 + * For logging heads: forkchecker --endpoint 127.0.0.1:4000 --endpoint 127.0.0.1:4001 + * For comparing heads: forkchecker --endpoint 127.0.0.1:4000 --endpoint 127.0.0.1:4001 --compare + */ +package main + +import ( + "context" + "encoding/hex" + "flag" + "reflect" + "time" + + ptypes "github.com/gogo/protobuf/types" + pb "github.com/prysmaticlabs/prysm/proto/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/shared/params" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" +) + +var log = logrus.WithField("prefix", "forkchoice_checker") + +type endpoint []string + +func (e *endpoint) String() string { + return "gRPC endpoints" +} + +func (e *endpoint) Set(value string) error { + *e = append(*e, value) + return nil +} + +func main() { + params.UseDemoBeaconConfig() + + var endpts endpoint + clients := make(map[string]pb.BeaconChainClient) + + flag.Var(&endpts, "endpoint", "Specify gRPC end points for beacon node") + compare := flag.Bool("compare", false, "Enable head comparisons between all end points") + flag.Parse() + + for _, endpt := range endpts { + conn, err := grpc.Dial(endpt, grpc.WithInsecure()) + if err != nil { + log.Fatalf("fail to dial: %v", err) + } + clients[endpt] = pb.NewBeaconChainClient(conn) + } + + ticker := time.NewTicker(time.Duration(params.BeaconConfig().SecondsPerSlot) * time.Second) + go func() { + for { + select { + case <-ticker.C: + if *compare { + compareHeads(clients) + } else { + displayHeads(clients) + } + } + } + }() + select {} +} + +// log heads for all RPC end points +func displayHeads(clients map[string]pb.BeaconChainClient) { + for endpt, client := range clients { + head, err := client.GetChainHead(context.Background(), &ptypes.Empty{}) + if err != nil { + log.Fatal(err) + } + logHead(endpt, head) + } +} + +// compare heads between all RPC end points, log the missmatch if there's one. +func compareHeads(clients map[string]pb.BeaconChainClient) { + endpt1 := randomEndpt(clients) + head1, err := clients[endpt1].GetChainHead(context.Background(), &ptypes.Empty{}) + if err != nil { + log.Fatal(err) + } + + log.Infof("Compare all heads for head slot :%d", head1.BlockSlot) + if head1.BlockSlot%params.BeaconConfig().SlotsPerEpoch == 0 { + p, err := clients[endpt1].GetValidatorParticipation(context.Background(), &pb.GetValidatorParticipationRequest{ + Epoch: (head1.BlockSlot / params.BeaconConfig().SlotsPerEpoch) - 1}) + if err != nil { + log.Fatal(err) + } + logParticipation(endpt1, p) + } + + for endpt2, client := range clients { + head2, err := client.GetChainHead(context.Background(), &ptypes.Empty{}) + if err != nil { + log.Fatal(err) + } + if !reflect.DeepEqual(head1, head2) { + log.Error("Uh oh! Head miss-matched!") + logHead(endpt1, head1) + logHead(endpt2, head2) + + if head1.BlockSlot%params.BeaconConfig().SlotsPerEpoch == 0 { + p, err := clients[endpt2].GetValidatorParticipation(context.Background(), &pb.GetValidatorParticipationRequest{ + Epoch: (head1.BlockSlot / params.BeaconConfig().SlotsPerEpoch) - 1}) + if err != nil { + log.Fatal(err) + } + logParticipation(endpt2, p) + } + } + } +} + +func logHead(endpt string, head *pb.ChainHead) { + log.WithFields( + logrus.Fields{ + "HeadSlot": head.BlockSlot, + "HeadRoot": hex.EncodeToString(head.BlockRoot), + "JustifiedSlot": head.JustifiedSlot, + "JustifiedRoot": hex.EncodeToString(head.JustifiedBlockRoot), + "FinalizedSlot": head.FinalizedSlot, + "Finalizedroot": hex.EncodeToString(head.FinalizedBlockRoot), + }).Info("Head from beacon node ", endpt) +} + +func logParticipation(endpt string, p *pb.ValidatorParticipation) { + log.WithFields( + logrus.Fields{ + "Finalized": p.Finalized, + "Epoch": p.Epoch, + "VotedEther": p.VotedEther, + "TotalEther": p.EligibleEther, + "ParticipationRate": p.GlobalParticipationRate, + }).Info("Participation rate from beacon node ", endpt) +} + +func randomEndpt(clients map[string]pb.BeaconChainClient) string { + for endpt := range clients { + return endpt + } + return "" +}