diff --git a/beacon-chain/node/BUILD.bazel b/beacon-chain/node/BUILD.bazel index 5b2125496e9b..b15d6d1c833b 100644 --- a/beacon-chain/node/BUILD.bazel +++ b/beacon-chain/node/BUILD.bazel @@ -34,6 +34,7 @@ go_library( "//shared/event:go_default_library", "//shared/featureconfig:go_default_library", "//shared/params:go_default_library", + "//shared/prereq:go_default_library", "//shared/prometheus:go_default_library", "//shared/sliceutil:go_default_library", "//shared/tracing:go_default_library", diff --git a/beacon-chain/node/node.go b/beacon-chain/node/node.go index 1f37e49afdfd..ed602a3091b9 100644 --- a/beacon-chain/node/node.go +++ b/beacon-chain/node/node.go @@ -41,6 +41,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/event" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/prereq" "github.com/prysmaticlabs/prysm/shared/prometheus" "github.com/prysmaticlabs/prysm/shared/sliceutil" "github.com/prysmaticlabs/prysm/shared/tracing" @@ -91,6 +92,9 @@ func NewBeaconNode(cliCtx *cli.Context) (*BeaconNode, error) { return nil, err } + // Warn if user's platform is not supported + prereq.WarnIfNotSupported(cliCtx.Context) + featureconfig.ConfigureBeaconChain(cliCtx) cmd.ConfigureBeaconChain(cliCtx) flags.ConfigureGlobalFlags(cliCtx) diff --git a/shared/prereq/BUILD.bazel b/shared/prereq/BUILD.bazel new file mode 100644 index 000000000000..3f5113f015a8 --- /dev/null +++ b/shared/prereq/BUILD.bazel @@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["prereq.go"], + importpath = "github.com/prysmaticlabs/prysm/shared/prereq", + visibility = ["//visibility:public"], + deps = [ + "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["prereq_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/testutil/require:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//hooks/test:go_default_library", + ], +) diff --git a/shared/prereq/prereq.go b/shared/prereq/prereq.go new file mode 100644 index 000000000000..52eeaa3b8460 --- /dev/null +++ b/shared/prereq/prereq.go @@ -0,0 +1,108 @@ +package prereq + +import ( + "context" + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +type platform struct { + os string + arch string + majorVersion int + minorVersion int +} + +var ( + // execShellOutput has execShellOutputFunc as the default but can be changed for testing purposes. + execShellOutput func(ctx context.Context, command string, args ...string) (string, error) = execShellOutputFunc + runtimeOS = runtime.GOOS + runtimeArch = runtime.GOARCH +) + +// execShellOutputFunc passes a command and args to exec.CommandContext and returns the result as a string +func execShellOutputFunc(ctx context.Context, command string, args ...string) (string, error) { + result, err := exec.CommandContext(ctx, command, args...).Output() + if err != nil { + return "", errors.Wrap(err, "error in command execution") + } + return string(result), nil +} + +func getSupportedPlatforms() []platform { + return []platform{ + {os: "linux", arch: "amd64"}, + {os: "linux", arch: "arm64"}, + {os: "darwin", arch: "amd64", majorVersion: 10, minorVersion: 14}, + {os: "windows", arch: "amd64"}, + } +} + +// parseVersion takes a string and splits it using sep separator, and outputs a slice of integers +// corresponding to version numbers. If it cannot find num level of versions, it returns an error +func parseVersion(input string, num int, sep string) ([]int, error) { + var version = make([]int, num) + components := strings.Split(input, sep) + for i, component := range components { + components[i] = strings.TrimSpace(component) + } + if len(components) < num { + return nil, errors.New("insufficient information about version") + } + for i := range version { + var err error + version[i], err = strconv.Atoi(components[i]) + if err != nil { + return nil, errors.Wrap(err, "error during conversion") + } + } + return version, nil +} + +// meetsMinPlatformReqs returns true if the runtime matches any on the list of supported platforms +func meetsMinPlatformReqs(ctx context.Context) (bool, error) { + okPlatforms := getSupportedPlatforms() + for _, platform := range okPlatforms { + if runtimeOS == platform.os && runtimeArch == platform.arch { + // If MacOS we make sure it meets the minimum version cutoff + if runtimeOS == "darwin" { + versionStr, err := execShellOutput(ctx, "uname", "-r") + if err != nil { + return false, errors.Wrap(err, "error obtaining MacOS version") + } + version, err := parseVersion(versionStr, 2, ".") + if err != nil { + return false, errors.Wrap(err, "error parsing version") + } + if version[0] != platform.majorVersion { + return version[0] > platform.majorVersion, nil + } + if version[1] < platform.minorVersion { + return false, nil + } + return true, nil + } + // Otherwise we have a match between runtime and our list of accepted platforms + return true, nil + } + } + return false, nil +} + +// WarnIfNotSupported warns if the user's platform is not supported or if it fails to detect user's platform +func WarnIfNotSupported(ctx context.Context) { + supported, err := meetsMinPlatformReqs(ctx) + if err != nil { + log.Warnf("Failed to detect host platform: %v", err) + return + } + if !supported { + log.Warn("This platform is not supported. The following platforms are supported: Linux/AMD64," + + " Linux/ARM64, Mac OS X/AMD64 (10.14+ only), and Windows/AMD64") + } +} diff --git a/shared/prereq/prereq_test.go b/shared/prereq/prereq_test.go new file mode 100644 index 000000000000..e9b0497359a9 --- /dev/null +++ b/shared/prereq/prereq_test.go @@ -0,0 +1,128 @@ +package prereq + +import ( + "context" + "testing" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/shared/testutil/require" + logTest "github.com/sirupsen/logrus/hooks/test" +) + +func TestMeetsMinPlatformReqs(t *testing.T) { + // Linux + runtimeOS = "linux" + runtimeArch = "amd64" + meetsReqs, err := meetsMinPlatformReqs(context.Background()) + require.Equal(t, true, meetsReqs) + require.NoError(t, err) + runtimeArch = "arm64" + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, true, meetsReqs) + require.NoError(t, err) + // mips64 is not supported + runtimeArch = "mips64" + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, false, meetsReqs) + require.NoError(t, err) + + // Mac OS X + // In this function we'll set the execShellOutput package variable to another function that will 'mock' the shell + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "", errors.New("error while running command") + } + runtimeOS = "darwin" + runtimeArch = "amd64" + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, false, meetsReqs) + require.ErrorContains(t, "error obtaining MacOS version", err) + + // Insufficient version + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "10.4", nil + } + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, false, meetsReqs) + require.NoError(t, err) + + // Just-sufficient older version + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "10.14", nil + } + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, true, meetsReqs) + require.NoError(t, err) + + // Sufficient newer version + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "10.15.7", nil + } + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, true, meetsReqs) + require.NoError(t, err) + + // Handling abnormal response + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "tiger.lion", nil + } + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, false, meetsReqs) + require.ErrorContains(t, "error parsing version", err) + + // Windows + runtimeOS = "windows" + runtimeArch = "amd64" + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, true, meetsReqs) + require.NoError(t, err) + runtimeArch = "arm64" + meetsReqs, err = meetsMinPlatformReqs(context.Background()) + require.Equal(t, false, meetsReqs) + require.NoError(t, err) +} + +func TestParseVersion(t *testing.T) { + version, err := parseVersion("1.2.3", 3, ".") + require.DeepEqual(t, version, []int{1, 2, 3}) + require.NoError(t, err) + + version, err = parseVersion("6 .7 . 8 ", 3, ".") + require.DeepEqual(t, version, []int{6, 7, 8}) + require.NoError(t, err) + + version, err = parseVersion("10,3,5,6", 4, ",") + require.DeepEqual(t, version, []int{10, 3, 5, 6}) + require.NoError(t, err) + + version, err = parseVersion("4;6;8;10;11", 3, ";") + require.DeepEqual(t, version, []int{4, 6, 8}) + require.NoError(t, err) + + _, err = parseVersion("10.11", 3, ".") + require.ErrorContains(t, "insufficient information about version", err) +} + +func TestWarnIfNotSupported(t *testing.T) { + runtimeOS = "linux" + runtimeArch = "amd64" + hook := logTest.NewGlobal() + WarnIfNotSupported(context.Background()) + require.LogsDoNotContain(t, hook, "Failed to detect host platform") + require.LogsDoNotContain(t, hook, "platform is not supported") + + execShellOutput = func(ctx context.Context, command string, args ...string) (string, error) { + return "tiger.lion", nil + } + runtimeOS = "darwin" + runtimeArch = "amd64" + hook = logTest.NewGlobal() + WarnIfNotSupported(context.Background()) + require.LogsContain(t, hook, "Failed to detect host platform") + require.LogsContain(t, hook, "error parsing version") + + runtimeOS = "falseOs" + runtimeArch = "falseArch" + hook = logTest.NewGlobal() + WarnIfNotSupported(context.Background()) + require.LogsContain(t, hook, "platform is not supported") +} diff --git a/slasher/node/BUILD.bazel b/slasher/node/BUILD.bazel index 8dd3fceb2f65..a9fb7f7871db 100644 --- a/slasher/node/BUILD.bazel +++ b/slasher/node/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "//shared/event:go_default_library", "//shared/featureconfig:go_default_library", "//shared/params:go_default_library", + "//shared/prereq:go_default_library", "//shared/prometheus:go_default_library", "//shared/tracing:go_default_library", "//shared/version:go_default_library", diff --git a/slasher/node/node.go b/slasher/node/node.go index dcd9cf6d59b4..c1eb2c8702c0 100644 --- a/slasher/node/node.go +++ b/slasher/node/node.go @@ -19,6 +19,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/event" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/prereq" "github.com/prysmaticlabs/prysm/shared/prometheus" "github.com/prysmaticlabs/prysm/shared/tracing" "github.com/prysmaticlabs/prysm/shared/version" @@ -64,6 +65,9 @@ func NewSlasherNode(cliCtx *cli.Context) (*SlasherNode, error) { return nil, err } + // Warn if user's platform is not supported + prereq.WarnIfNotSupported(cliCtx.Context) + if cliCtx.Bool(flags.EnableHistoricalDetectionFlag.Name) { // Set the max RPC size to 4096 as configured by --historical-slasher-node for optimal historical detection. cmdConfig := cmd.Get() diff --git a/validator/node/BUILD.bazel b/validator/node/BUILD.bazel index a165e392411c..d79db9e91230 100644 --- a/validator/node/BUILD.bazel +++ b/validator/node/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "//shared/featureconfig:go_default_library", "//shared/fileutil:go_default_library", "//shared/params:go_default_library", + "//shared/prereq:go_default_library", "//shared/prometheus:go_default_library", "//shared/tracing:go_default_library", "//shared/version:go_default_library", diff --git a/validator/node/node.go b/validator/node/node.go index f0a21cd2276c..c654e4f5a9b5 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -21,6 +21,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/prereq" "github.com/prysmaticlabs/prysm/shared/prometheus" "github.com/prysmaticlabs/prysm/shared/tracing" "github.com/prysmaticlabs/prysm/shared/version" @@ -71,6 +72,9 @@ func NewValidatorClient(cliCtx *cli.Context) (*ValidatorClient, error) { } logrus.SetLevel(level) + // Warn if user's platform is not supported + prereq.WarnIfNotSupported(cliCtx.Context) + registry := shared.NewServiceRegistry() ValidatorClient := &ValidatorClient{ cliCtx: cliCtx,