diff --git a/cmd/gossamer/config.go b/cmd/gossamer/config.go index 0131a5a9d9..1695c73e62 100644 --- a/cmd/gossamer/config.go +++ b/cmd/gossamer/config.go @@ -17,7 +17,6 @@ package main import ( - "encoding/binary" "fmt" "strconv" "strings" @@ -33,7 +32,7 @@ import ( "github.com/ChainSafe/gossamer/lib/runtime/life" "github.com/ChainSafe/gossamer/lib/runtime/wasmer" "github.com/ChainSafe/gossamer/lib/runtime/wasmtime" - "github.com/cosmos/go-bip39" + "github.com/ChainSafe/gossamer/lib/utils" log "github.com/ChainSafe/log15" "github.com/urfave/cli" @@ -132,7 +131,10 @@ func createDotConfig(ctx *cli.Context) (*dot.Config, error) { logger.Info("loaded package log configuration", "cfg", cfg.Log) // set global configuration values - setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + if err := setDotGlobalConfig(ctx, tomlCfg, &cfg.Global); err != nil { + logger.Error("failed to set global node configuration", "error", err) + return nil, err + } // set remaining cli configuration values setDotInitConfig(ctx, tomlCfg.Init, &cfg.Init) @@ -160,7 +162,11 @@ func createInitConfig(ctx *cli.Context) (*dot.Config, error) { } // set global configuration values - setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + err = setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + if err != nil { + logger.Error("failed to set global node configuration", "error", err) + return nil, err + } // set log config err = setLogConfig(ctx, tomlCfg, &cfg.Global, &cfg.Log) @@ -196,7 +202,11 @@ func createImportStateConfig(ctx *cli.Context) (*dot.Config, error) { } // set global configuration values - setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + if err := setDotGlobalConfig(ctx, tomlCfg, &cfg.Global); err != nil { + logger.Error("failed to set global node configuration", "error", err) + return nil, err + } + return cfg, nil } @@ -210,7 +220,11 @@ func createBuildSpecConfig(ctx *cli.Context) (*dot.Config, error) { } // set global configuration values - setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + if err := setDotGlobalConfig(ctx, tomlCfg, &cfg.Global); err != nil { + logger.Error("failed to set global node configuration", "error", err) + return nil, err + } + return cfg, nil } @@ -229,7 +243,11 @@ func createExportConfig(ctx *cli.Context) (*dot.Config, error) { updateDotConfigFromGenesisJSONRaw(*tomlCfg, cfg) // set global configuration values - setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + err = setDotGlobalConfig(ctx, tomlCfg, &cfg.Global) + if err != nil { + logger.Error("failed to set global node configuration", "error", err) + return nil, err + } // set log config err = setLogConfig(ctx, &ctoml.Config{}, &cfg.Global, &cfg.Log) @@ -385,13 +403,27 @@ func setDotInitConfig(ctx *cli.Context, tomlCfg ctoml.InitConfig, cfg *dot.InitC ) } -// setDotGlobalConfig sets dot.GlobalConfig using flag values from the cli context -func setDotGlobalConfig(ctx *cli.Context, tomlCfg *ctoml.Config, cfg *dot.GlobalConfig) { - if tomlCfg != nil { - if tomlCfg.Global.Name != "" { - cfg.Name = tomlCfg.Global.Name - } +func setDotGlobalConfig(ctx *cli.Context, tomlConfig *ctoml.Config, cfg *dot.GlobalConfig) error { + setDotGlobalConfigFromToml(tomlConfig, cfg) + setDotGlobalConfigFromFlags(ctx, cfg) + + if err := setDotGlobalConfigName(ctx, tomlConfig, cfg); err != nil { + return fmt.Errorf("could not set global node name: %w", err) + } + logger.Debug( + "global configuration", + "name", cfg.Name, + "id", cfg.ID, + "basepath", cfg.BasePath, + ) + + return nil +} + +// setDotGlobalConfigFromToml will apply the toml configs to dot global config +func setDotGlobalConfigFromToml(tomlCfg *ctoml.Config, cfg *dot.GlobalConfig) { + if tomlCfg != nil { if tomlCfg.Global.ID != "" { cfg.ID = tomlCfg.Global.ID } @@ -406,20 +438,10 @@ func setDotGlobalConfig(ctx *cli.Context, tomlCfg *ctoml.Config, cfg *dot.Global cfg.MetricsPort = tomlCfg.Global.MetricsPort } +} - // TODO: generate random name if one is not assigned (see issue #1496) - // check --name flag and update node configuration - if name := ctx.GlobalString(NameFlag.Name); name != "" { - cfg.Name = name - } else { - // generate random name - entropy, _ := bip39.NewEntropy(128) - randomNamesString, _ := bip39.NewMnemonic(entropy) - randomNames := strings.Split(randomNamesString, " ") - number := binary.BigEndian.Uint16(entropy) - cfg.Name = randomNames[0] + "-" + randomNames[1] + "-" + fmt.Sprint(number) - } - +// setDotGlobalConfigFromFlags sets dot.GlobalConfig using flag values from the cli context +func setDotGlobalConfigFromFlags(ctx *cli.Context, cfg *dot.GlobalConfig) { // check --basepath flag and update node configuration if basepath := ctx.GlobalString(BasePathFlag.Name); basepath != "" { cfg.BasePath = basepath @@ -429,6 +451,7 @@ func setDotGlobalConfig(ctx *cli.Context, tomlCfg *ctoml.Config, cfg *dot.Global if cfg.BasePath == "" { cfg.BasePath = dot.GssmrConfig().Global.BasePath } + // check --log flag if lvlToInt, err := strconv.Atoi(ctx.String(LogFlag.Name)); err == nil { cfg.LogLvl = log.Lvl(lvlToInt) @@ -444,13 +467,39 @@ func setDotGlobalConfig(ctx *cli.Context, tomlCfg *ctoml.Config, cfg *dot.Global } cfg.NoTelemetry = ctx.Bool("no-telemetry") +} - logger.Debug( - "global configuration", - "name", cfg.Name, - "id", cfg.ID, - "basepath", cfg.BasePath, - ) +func setDotGlobalConfigName(ctx *cli.Context, tomlCfg *ctoml.Config, cfg *dot.GlobalConfig) error { + globalBasePath := utils.ExpandDir(cfg.BasePath) + initialised := dot.NodeInitialized(globalBasePath, false) + + // consider the --name flag as higher priority + if ctx.GlobalString(NameFlag.Name) != "" { + cfg.Name = ctx.GlobalString(NameFlag.Name) + return nil + } + + // consider the name on config as a second priority + if tomlCfg.Global.Name != "" { + cfg.Name = tomlCfg.Global.Name + return nil + } + + // if node was previously initialised and is not the init command + if initialised && ctx.Command.Name != initCommandName { + var err error + if cfg.Name, err = dot.LoadGlobalNodeName(globalBasePath); err != nil { + return err + } + + if cfg.Name != "" { + logger.Debug("load global node name from database", "name", cfg.Name) + return nil + } + } + + cfg.Name = dot.RandomNodeName() + return nil } // setDotAccountConfig sets dot.AccountConfig using flag values from the cli context diff --git a/cmd/gossamer/config_test.go b/cmd/gossamer/config_test.go index 6c87cb8d06..c369a322f7 100644 --- a/cmd/gossamer/config_test.go +++ b/cmd/gossamer/config_test.go @@ -23,6 +23,7 @@ import ( "github.com/ChainSafe/gossamer/chain/gssmr" "github.com/ChainSafe/gossamer/dot" "github.com/ChainSafe/gossamer/dot/state" + "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/genesis" "github.com/ChainSafe/gossamer/lib/utils" @@ -833,3 +834,142 @@ func TestUpdateConfigFromGenesisData(t *testing.T) { require.Equal(t, expected, cfg) } + +func TestGlobalNodeName_WhenNodeAlreadyHasStoredName(t *testing.T) { + // Initialise a node with a random name + globalName := dot.RandomNodeName() + + cfg := dot.NewTestConfig(t) + cfg.Global.Name = globalName + require.NotNil(t, cfg) + + genPath := dot.NewTestGenesisAndRuntime(t) + require.NotNil(t, genPath) + + defer utils.RemoveTestDir(t) + + cfg.Core.Roles = types.FullNodeRole + cfg.Core.BabeAuthority = false + cfg.Core.GrandpaAuthority = false + cfg.Core.BabeThresholdNumerator = 0 + cfg.Core.BabeThresholdDenominator = 0 + cfg.Init.Genesis = genPath + + err := dot.InitNode(cfg) + require.NoError(t, err) + + // call another command and test the name + testApp := cli.NewApp() + testApp.Writer = ioutil.Discard + + testcases := []struct { + description string + flags []string + values []interface{} + expected string + }{ + { + "Test gossamer --roles --basepath", + []string{"basepath", "roles"}, + []interface{}{cfg.Global.BasePath, "4"}, + globalName, + }, + { + "Test gossamer --roles", + []string{"basepath", "roles"}, + []interface{}{cfg.Global.BasePath, "0"}, + globalName, + }, + } + + for _, c := range testcases { + c := c // bypass scopelint false positive + t.Run(c.description, func(t *testing.T) { + ctx, err := newTestContext(c.description, c.flags, c.values) + require.Nil(t, err) + createdCfg, err := createDotConfig(ctx) + require.Nil(t, err) + require.Equal(t, c.expected, createdCfg.Global.Name) + }) + } +} + +func TestGlobalNodeNamePriorityOrder(t *testing.T) { + cfg, testCfgFile := newTestConfigWithFile(t) + require.NotNil(t, cfg) + require.NotNil(t, testCfgFile) + + defer utils.RemoveTestDir(t) + + // call another command and test the name + testApp := cli.NewApp() + testApp.Writer = ioutil.Discard + + // when name flag is defined + whenNameFlagIsDefined := struct { + description string + flags []string + values []interface{} + expected string + }{ + "Test gossamer --basepath --name --config", + []string{"basepath", "name", "config"}, + []interface{}{cfg.Global.BasePath, "mydefinedname", testCfgFile.Name()}, + "mydefinedname", + } + + c := whenNameFlagIsDefined + t.Run(c.description, func(t *testing.T) { + ctx, err := newTestContext(c.description, c.flags, c.values) + require.Nil(t, err) + createdCfg, err := createDotConfig(ctx) + require.Nil(t, err) + require.Equal(t, c.expected, createdCfg.Global.Name) + }) + + // when name flag is not defined + // then should load name from toml if it exists + whenNameIsDefinedOnTomlConfig := struct { + description string + flags []string + values []interface{} + expected string + }{ + "Test gossamer --basepath --config", + []string{"basepath", "config"}, + []interface{}{cfg.Global.BasePath, testCfgFile.Name()}, + cfg.Global.Name, + } + + c = whenNameIsDefinedOnTomlConfig + t.Run(c.description, func(t *testing.T) { + ctx, err := newTestContext(c.description, c.flags, c.values) + require.Nil(t, err) + createdCfg, err := createDotConfig(ctx) + require.Nil(t, err) + require.Equal(t, c.expected, createdCfg.Global.Name) + }) + + // when there is no name flag and no name in config + // should check the load is initialised or generate a new random name + cfg.Global.Name = "" + + whenThereIsNoName := struct { + description string + flags []string + values []interface{} + }{ + "Test gossamer --basepath", + []string{"basepath"}, + []interface{}{cfg.Global.BasePath}, + } + + t.Run(c.description, func(t *testing.T) { + ctx, err := newTestContext(whenThereIsNoName.description, whenThereIsNoName.flags, whenThereIsNoName.values) + require.Nil(t, err) + createdCfg, err := createDotConfig(ctx) + require.Nil(t, err) + require.NotEmpty(t, createdCfg.Global.Name) + require.NotEqual(t, cfg.Global.Name, createdCfg.Global.Name) + }) +} diff --git a/cmd/gossamer/main.go b/cmd/gossamer/main.go index 5c0d3d0ea3..b6d18f5a68 100644 --- a/cmd/gossamer/main.go +++ b/cmd/gossamer/main.go @@ -28,6 +28,15 @@ import ( "github.com/urfave/cli" ) +const ( + accountCommandName = "account" + exportCommandName = "export" + initCommandName = "init" + buildSpecCommandName = "build-spec" + importRuntimeCommandName = "import-runtime" + importStateCommandName = "import-state" +) + // app is the cli application var app = cli.NewApp() var logger = log.New("pkg", "cmd") @@ -36,7 +45,7 @@ var ( // exportCommand defines the "export" subcommand (ie, `gossamer export`) exportCommand = cli.Command{ Action: FixFlagOrder(exportAction), - Name: "export", + Name: exportCommandName, Usage: "Export configuration values to TOML configuration file", ArgsUsage: "", Flags: ExportFlags, @@ -47,7 +56,7 @@ var ( // initCommand defines the "init" subcommand (ie, `gossamer init`) initCommand = cli.Command{ Action: FixFlagOrder(initAction), - Name: "init", + Name: initCommandName, Usage: "Initialise node databases and load genesis data to state", ArgsUsage: "", Flags: InitFlags, @@ -58,7 +67,7 @@ var ( // accountCommand defines the "account" subcommand (ie, `gossamer account`) accountCommand = cli.Command{ Action: FixFlagOrder(accountAction), - Name: "account", + Name: accountCommandName, Usage: "Create and manage node keystore accounts", Flags: AccountFlags, Category: "ACCOUNT", @@ -72,7 +81,7 @@ var ( // buildSpecCommand creates a raw genesis file from a human readable genesis file. buildSpecCommand = cli.Command{ Action: FixFlagOrder(buildSpecAction), - Name: "build-spec", + Name: buildSpecCommandName, Usage: "Generates genesis JSON data, and can convert to raw genesis data", ArgsUsage: "", Flags: BuildSpecFlags, @@ -86,7 +95,7 @@ var ( // importRuntime generates a genesis file given a .wasm runtime binary. importRuntimeCommand = cli.Command{ Action: FixFlagOrder(importRuntimeAction), - Name: "import-runtime", + Name: importRuntimeCommandName, Usage: "Generates a genesis file given a .wasm runtime binary", ArgsUsage: "", Flags: RootFlags, @@ -97,7 +106,7 @@ var ( importStateCommand = cli.Command{ Action: FixFlagOrder(importStateAction), - Name: "import-state", + Name: importStateCommandName, Usage: "Import state from a JSON file and set it as the chain head state", ArgsUsage: "", Flags: ImportStateFlags, diff --git a/cmd/gossamer/main_test.go b/cmd/gossamer/main_test.go index a1ab39225f..c09ef05742 100644 --- a/cmd/gossamer/main_test.go +++ b/cmd/gossamer/main_test.go @@ -32,6 +32,7 @@ import ( "text/template" "time" + "github.com/ChainSafe/gossamer/dot" "github.com/ChainSafe/gossamer/lib/utils" "github.com/docker/docker/pkg/reexec" "github.com/stretchr/testify/require" @@ -281,7 +282,47 @@ func TestGossamerCommand(t *testing.T) { require.NotContains(t, string(stderr), m) } } +} + +func TestInitCommand_RenameNodeWhenCalled(t *testing.T) { + genesisPath := utils.GetGssmrGenesisRawPath() + + tempDir, err := ioutil.TempDir("", "gossamer-maintest-") + require.Nil(t, err) + + nodeName := dot.RandomNodeName() + init := runTestGossamer(t, + "init", + "--basepath", tempDir, + "--genesis", genesisPath, + "--name", nodeName, + "--config", defaultGssmrConfigPath, + "--force", + ) + + stdout, stderr := init.GetOutput() + require.Nil(t, err) + + t.Log("init gossamer output, ", "stdout", string(stdout), "stderr", string(stderr)) + + // should contains the name defined in name flag + require.Contains(t, string(stdout), nodeName) + + init = runTestGossamer(t, + "init", + "--basepath", tempDir, + "--genesis", genesisPath, + "--config", defaultGssmrConfigPath, + "--force", + ) + + stdout, stderr = init.GetOutput() + require.Nil(t, err) + + t.Log("init gossamer output, ", "stdout", string(stdout), "stderr", string(stderr)) + // should not contains the name from the last init + require.NotContains(t, string(stdout), nodeName) } func TestBuildSpecCommandWithOutput(t *testing.T) { diff --git a/cmd/gossamer/utils.go b/cmd/gossamer/utils.go index dad18746f5..b5feb371ab 100644 --- a/cmd/gossamer/utils.go +++ b/cmd/gossamer/utils.go @@ -35,6 +35,8 @@ import ( "golang.org/x/crypto/ssh/terminal" //nolint ) +const confirmCharacter = "Y" + // setupLogger sets up the gossamer logger func setupLogger(ctx *cli.Context) (log.Lvl, error) { handler := log.StreamHandler(os.Stdout, log.TerminalFormat()) @@ -76,7 +78,7 @@ func confirmMessage(msg string) bool { for { text, _ := reader.ReadString('\n') text = strings.ReplaceAll(text, "\n", "") - return strings.Compare("Y", text) == 0 + return strings.Compare(confirmCharacter, strings.ToUpper(text)) == 0 } } @@ -124,7 +126,6 @@ func newTestConfigWithFile(t *testing.T) (*dot.Config, *os.File) { require.NoError(t, err) tomlCfg := dotConfigToToml(cfg) - cfgFile := exportConfig(tomlCfg, file.Name()) return cfg, cfgFile } diff --git a/dot/node.go b/dot/node.go index b2e02e826e..25ac6561b8 100644 --- a/dot/node.go +++ b/dot/node.go @@ -104,6 +104,11 @@ func InitNode(cfg *Config) error { return fmt.Errorf("failed to initialise state service: %s", err) } + err = storeGlobalNodeName(cfg.Global.Name, cfg.Global.BasePath) + if err != nil { + return fmt.Errorf("failed to store global node name: %s", err) + } + logger.Info( "node initialised", "name", cfg.Global.Name, @@ -122,6 +127,7 @@ func InitNode(cfg *Config) error { func NodeInitialized(basepath string, expected bool) bool { // check if key registry exists registry := path.Join(basepath, "KEYREGISTRY") + _, err := os.Stat(registry) if os.IsNotExist(err) { if expected { @@ -167,6 +173,36 @@ func NodeInitialized(basepath string, expected bool) bool { return true } +// LoadGlobalNodeName returns the stored global node name from database +func LoadGlobalNodeName(basepath string) (nodename string, err error) { + // initialise database using data directory + db, err := state.SetupDatabase(basepath) + if err != nil { + return "", err + } + + defer func() { + err = db.Close() + if err != nil { + logger.Error("failed to close database", "error", err) + return + } + }() + + basestate := state.NewBaseState(db) + nodename, err = basestate.LoadNodeGlobalName() + if err != nil { + logger.Warn( + "failed to load global node name", + "basepath", basepath, + "error", err, + ) + return "", err + } + + return nodename, err +} + // NewNode creates a new dot node from a dot node configuration func NewNode(cfg *Config, ks *keystore.GlobalKeystore, stopFunc func()) (*Node, error) { // set garbage collection percent to 10% @@ -198,6 +234,7 @@ func NewNode(cfg *Config, ks *keystore.GlobalKeystore, stopFunc func()) (*Node, // create state service and append state service to node services stateSrvc, err := createStateService(cfg) + if err != nil { return nil, fmt.Errorf("failed to create state service: %s", err) } @@ -354,6 +391,35 @@ func setupMetricsServer(address string) { }() } +// stores the global node name to reuse +func storeGlobalNodeName(name, basepath string) (err error) { + db, err := state.SetupDatabase(basepath) + if err != nil { + return err + } + + defer func() { + err = db.Close() + if err != nil { + logger.Error("failed to close database", "error", err) + return + } + }() + + basestate := state.NewBaseState(db) + err = basestate.StoreNodeGlobalName(name) + if err != nil { + logger.Warn( + "failed to store global node name", + "basepath", basepath, + "error", err, + ) + return err + } + + return nil +} + // Start starts all dot node services func (n *Node) Start() error { logger.Info("🕸️ starting node services...") diff --git a/dot/node_test.go b/dot/node_test.go index 58eb2ea088..ce3d78ac5a 100644 --- a/dot/node_test.go +++ b/dot/node_test.go @@ -382,3 +382,30 @@ func TestNode_StopFunc(t *testing.T) { node.Stop() require.Equal(t, testvar, "after") } + +func TestNode_PersistGlobalName_WhenInitialize(t *testing.T) { + globalName := RandomNodeName() + + cfg := NewTestConfig(t) + cfg.Global.Name = globalName + require.NotNil(t, cfg) + + genPath := NewTestGenesisAndRuntime(t) + require.NotNil(t, genPath) + + defer utils.RemoveTestDir(t) + + cfg.Core.Roles = types.FullNodeRole + cfg.Core.BabeAuthority = false + cfg.Core.GrandpaAuthority = false + cfg.Core.BabeThresholdNumerator = 0 + cfg.Core.BabeThresholdDenominator = 0 + cfg.Init.Genesis = genPath + + err := InitNode(cfg) + require.NoError(t, err) + + storedName, err := LoadGlobalNodeName(cfg.Global.BasePath) + require.Nil(t, err) + require.Equal(t, globalName, storedName) +} diff --git a/dot/state/base.go b/dot/state/base.go index 32f338d5d3..0d0ff47e81 100644 --- a/dot/state/base.go +++ b/dot/state/base.go @@ -27,6 +27,13 @@ import ( "github.com/ChainSafe/chaindb" ) +// SetupDatabase will return an instance of database based on basepath +func SetupDatabase(basepath string) (chaindb.Database, error) { + return chaindb.NewBadgerDB(&chaindb.Config{ + DataDir: basepath, + }) +} + // BaseState is a wrapper for the chaindb.Database, without any prefixes type BaseState struct { db chaindb.Database @@ -39,6 +46,21 @@ func NewBaseState(db chaindb.Database) *BaseState { } } +// StoreNodeGlobalName stores the current node name to avoid create new ones after each initialization +func (s *BaseState) StoreNodeGlobalName(nodeName string) error { + return s.db.Put(common.NodeNameKey, []byte(nodeName)) +} + +// LoadNodeGlobalName loads the latest stored node global name +func (s *BaseState) LoadNodeGlobalName() (string, error) { + nodeName, err := s.db.Get(common.NodeNameKey) + if err != nil { + return "", err + } + + return string(nodeName), nil +} + // StoreBestBlockHash stores the hash at the BestBlockHashKey func (s *BaseState) StoreBestBlockHash(hash common.Hash) error { return s.db.Put(common.BestBlockHashKey, hash[:]) diff --git a/dot/utils.go b/dot/utils.go index 03c9640468..4f67021322 100644 --- a/dot/utils.go +++ b/dot/utils.go @@ -17,11 +17,14 @@ package dot import ( + "encoding/binary" "encoding/hex" "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" + "strings" "testing" ctoml "github.com/ChainSafe/gossamer/dot/config/toml" @@ -30,6 +33,7 @@ import ( "github.com/ChainSafe/gossamer/lib/runtime/wasmer" "github.com/ChainSafe/gossamer/lib/utils" log "github.com/ChainSafe/log15" + "github.com/cosmos/go-bip39" "github.com/naoina/toml" "github.com/stretchr/testify/require" ) @@ -235,3 +239,13 @@ func CreateJSONRawFile(bs *BuildSpec, fp string) *os.File { } return WriteConfig(data, fp) } + +// RandomNodeName generate a new random name +// if there is no name configured to the node +func RandomNodeName() string { + entropy, _ := bip39.NewEntropy(128) + randomNamesString, _ := bip39.NewMnemonic(entropy) + randomNames := strings.Split(randomNamesString, " ") + number := binary.BigEndian.Uint16(entropy) + return randomNames[0] + "-" + randomNames[1] + "-" + fmt.Sprint(number) +} diff --git a/lib/common/db_keys.go b/lib/common/db_keys.go index 249665dcc9..2de8019943 100644 --- a/lib/common/db_keys.go +++ b/lib/common/db_keys.go @@ -31,4 +31,6 @@ var ( LatestFinalizedRoundKey = []byte("latest_finalised_round") // WorkingStorageHashKey is the storage key that the runtime uses to store the latest working state root. WorkingStorageHashKey = []byte("working_storage_hash") + //NodeNameKey is the storage key to store de current node name and avoid create a new name every initialization + NodeNameKey = []byte("node_name") )