diff --git a/.changelog/3298.doc.md b/.changelog/3298.doc.md new file mode 100644 index 00000000000..171daaccab8 --- /dev/null +++ b/.changelog/3298.doc.md @@ -0,0 +1,3 @@ +Document [`oasis-node genesis` CLI commands] + +[`oasis-node genesis` CLI commands]: docs/oasis-node/cli.md#genesis diff --git a/.changelog/3298.feature.md b/.changelog/3298.feature.md new file mode 100644 index 00000000000..1b6eb8202e9 --- /dev/null +++ b/.changelog/3298.feature.md @@ -0,0 +1 @@ +go/oasis-node: Make `oasis-node genesis check` command check if form canonical diff --git a/docs/oasis-node/cli.md b/docs/oasis-node/cli.md index 4aa85aa5432..8f0154c3345 100644 --- a/docs/oasis-node/cli.md +++ b/docs/oasis-node/cli.md @@ -155,6 +155,64 @@ node): } ``` +## `genesis` + +### `check` + +To check if a given [genesis file] is valid, run: + +```sh +oasis-node genesis check --genesis.file /path/to/genesis.json +``` + +{% hint style="info" %} +This also checks if the genesis file is in the [canonical form]. +{% endhint %} + +### `dump` + +To dump the state of the network at a specific block height, e.g. 717600, to a +[genesis file], run: + +```sh +oasis-node genesis dump \ + --address unix:/path/to/node/internal.sock \ + --genesis.file /path/to/genesis_dump.json \ + --height 717600 +``` + +{% hint style="warning" %} +You must only run the following command after the given block height has been +reached on the network. +{% endhint %} + +### `init` + +To initialize a new [genesis file] with the given chain id and [staking token +symbol], run: + +```sh +oasis-node genesis init --genesis.file /path/to/genesis.json \ + --chain.id "name-of-my-network" \ + --staking.token_symbol TEST +``` + +{% hint style="info" %} +You can set a lot of parameters for the various [consensus layer services]. + +To see the full list, run: + +```sh +oasis-node genesis init --help +``` + +{% endhint %} + +[genesis file]: ../consensus/genesis.md#genesis-file +[canonical form]: ../consensus/genesis.md#canonical-form +[consensus layer services]: ../consensus/index.md +[staking token symbol]: ../consensus/staking.md#tokens-and-base-units + ## `stake` ### `account` diff --git a/go/oasis-node/cmd/genesis/genesis.go b/go/oasis-node/cmd/genesis/genesis.go index 70ef1b6b157..2ed1d9430d9 100644 --- a/go/oasis-node/cmd/genesis/genesis.go +++ b/go/oasis-node/cmd/genesis/genesis.go @@ -2,13 +2,16 @@ package genesis import ( + "bytes" "context" "encoding/json" "errors" + "fmt" "io/ioutil" "math" "os" "strconv" + "strings" "time" "github.com/spf13/cobra" @@ -99,6 +102,10 @@ const ( // Our 'entity' flag overlaps with the common flag 'entity'. // We bind it to a separate Viper key to disambiguate at runtime. viperEntity = "provision_entity" + + // Check command. + // Number of lines to print if document not in canonical form. + checkNotCanonicalLines = 10 ) var ( @@ -611,7 +618,37 @@ func doCheckGenesis(cmd *cobra.Command, args []string) { os.Exit(1) } - // TODO: Pretty-print contents of genesis document. + // Load raw genesis file. + rawFile, err := ioutil.ReadFile(filename) + if err != nil { + logger.Error("failed to read genesis file:", "err", err) + os.Exit(1) + } + // Create a marshalled genesis document in the canonical form with 2 space indents. + rawCanonical, err := json.MarshalIndent(doc, "", " ") + if err != nil { + logger.Error("failed to marshal genesis document", "err", err) + os.Exit(1) + } + // Genesis file should equal the canonical form. + if !bytes.Equal(rawFile, rawCanonical) { + fileLines := strings.Split(string(rawFile), "\n") + if len(fileLines) > checkNotCanonicalLines { + fileLines = fileLines[:checkNotCanonicalLines] + } + canonicalLines := strings.Split(string(rawCanonical), "\n") + if len(canonicalLines) > checkNotCanonicalLines { + canonicalLines = canonicalLines[:checkNotCanonicalLines] + } + logger.Error("genesis document is not marshalled in the canonical form") + fmt.Fprintf(os.Stderr, + "Error: genesis document is not marshalled in the canonical form:\n"+ + "\nActual marshalled genesis document (trimmed):\n%s\n\n... trimmed ...\n"+ + "\nExpected marshalled genesis document (trimmed):\n%s\n\n... trimmed ...\n", + strings.Join(fileLines, "\n"), strings.Join(canonicalLines, "\n"), + ) + os.Exit(1) + } } // Register registers the genesis sub-command and all of it's children. diff --git a/go/oasis-test-runner/scenario/e2e/genesis_file.go b/go/oasis-test-runner/scenario/e2e/genesis_file.go index c374fd0feab..15c0d9b4ce6 100644 --- a/go/oasis-test-runner/scenario/e2e/genesis_file.go +++ b/go/oasis-test-runner/scenario/e2e/genesis_file.go @@ -1,7 +1,6 @@ package e2e import ( - "bytes" "context" "encoding/json" "fmt" @@ -56,10 +55,10 @@ func (s *genesisFileImpl) Run(childEnv *env.Env) error { cfg := s.Net.Config() cfg.GenesisFile = s.Net.GenesisPath() - if err := checkGenesisFile(s.Net.GenesisPath()); err != nil { - return fmt.Errorf("e2e/genesis-file: %w", err) + if err := s.runGenesisCheckCmd(childEnv, s.Net.GenesisPath()); err != nil { + return fmt.Errorf("e2e/genesis-file: running genesis check failed: %w", err) } - s.Logger.Info("manually provisioned genesis file equals canonical form") + s.Logger.Info("manually provisioned genesis file passed genesis check command") if err := s.Net.Start(); err != nil { return fmt.Errorf("e2e/genesis-file: failed to start network: %w", err) @@ -72,7 +71,6 @@ func (s *genesisFileImpl) Run(childEnv *env.Env) error { // Dump network state to a genesis file. s.Logger.Info("dumping network state to genesis file") - dumpPath := filepath.Join(childEnv.Dir(), "genesis_dump.json") args := []string{ "genesis", "dump", @@ -80,21 +78,54 @@ func (s *genesisFileImpl) Run(childEnv *env.Env) error { "--genesis.file", dumpPath, "--address", "unix:" + s.Net.Validators()[0].SocketPath(), } - if err := cli.RunSubCommand(childEnv, s.Logger, "genesis-file", s.Net.Config().NodeBinary, args); err != nil { - return fmt.Errorf("e2e/genesis-file: failed to dump state: %w", err) + out, err := cli.RunSubCommandWithOutput(childEnv, s.Logger, "genesis-file", s.Net.Config().NodeBinary, args) + if err != nil { + return fmt.Errorf("e2e/genesis-file: failed to dump state: error: %w output: %s", err, out.String()) + } + + if err = s.runGenesisCheckCmd(childEnv, dumpPath); err != nil { + return fmt.Errorf("e2e/genesis-file: running genesis check failed: %w", err) } - if err := checkGenesisFile(dumpPath); err != nil { - return fmt.Errorf("e2e/genesis-file: %w", err) + s.Logger.Info("genesis file from dumped network state passed genesis check command") + + uncanonicalPath := filepath.Join(childEnv.Dir(), "genesis_uncanonical.json") + if err = s.createUncanonicalGenesisFile(childEnv, uncanonicalPath); err != nil { + return fmt.Errorf("e2e/genesis-file: creating uncanonical genesis file failed: %w", err) } - s.Logger.Info("genesis file from dumped network state equals canonical form") + err = s.runGenesisCheckCmd(childEnv, uncanonicalPath) + expectedError := "genesis document is not marshalled in the canonical form" + switch { + case err == nil: + return fmt.Errorf("e2e/genesis-file: running genesis check for an uncanonical genesis file should fail") + case !strings.Contains(err.Error(), expectedError): + return fmt.Errorf( + "e2e/genesis-file: running genesis check for an uncanonical genesis file "+ + "should fail with an error containing: '%s' (actual error: %s)", + expectedError, err, + ) + default: + s.Logger.Info("uncanonical genesis file didn't pass genesis check command") + } + + return nil +} +func (s *genesisFileImpl) runGenesisCheckCmd(childEnv *env.Env, genesisFilePath string) error { + args := []string{ + "genesis", "check", + "--genesis.file", genesisFilePath, + "--debug.dont_blame_oasis", + "--debug.allow_test_keys", + } + out, err := cli.RunSubCommandWithOutput(childEnv, s.Logger, "genesis-file", s.Net.Config().NodeBinary, args) + if err != nil { + return fmt.Errorf("genesis check failed: error: %w output: %s", err, out.String()) + } return nil } -// checkGenesisFile checks if the given genesis file equals the canonical form. -func checkGenesisFile(filePath string) error { - // Load genesis document from the genesis file. - provider, err := genesisFile.NewFileProvider(filePath) +func (s *genesisFileImpl) createUncanonicalGenesisFile(childEnv *env.Env, uncanonicalGenesisFilePath string) error { + provider, err := genesisFile.NewFileProvider(s.Net.GenesisPath()) if err != nil { return fmt.Errorf("failed to open genesis file: %w", err) } @@ -102,32 +133,15 @@ func checkGenesisFile(filePath string) error { if err != nil { return fmt.Errorf("failed to get genesis document: %w", err) } - // Perform sanity checks on the loaded genesis document. - err = doc.SanityCheck() - if err != nil { - return fmt.Errorf("genesis document sanity check failed: %w", err) - } - // Load raw genesis file. - rawFile, err := ioutil.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read genesis file: %w", err) - } - // Create a marshalled genesis document in the canonical form with 2 space indents. - rawCanonical, err := json.MarshalIndent(doc, "", " ") + // Create a marshalled genesis document in an uncanonical form (e.g with 4 space indents). + rawUncanonical, err := json.MarshalIndent(doc, "", " ") if err != nil { return fmt.Errorf("failed to marshal genesis document: %w", err) } - // Genesis file should equal the canonical form. - if !bytes.Equal(rawFile, rawCanonical) { - fileLines := strings.Split(string(rawFile), "\n") - canonicalLines := strings.Split(string(rawCanonical), "\n") - return fmt.Errorf( - "genesis document is not marshalled to the canonical form:\n"+ - "\nActual marshalled genesis document (trimmed):\n%s\n"+ - "\nExpected marshalled genesis document (trimmed):\n%s\n", - strings.Join(fileLines[:10], "\n"), strings.Join(canonicalLines[:10], "\n"), - ) + if err := ioutil.WriteFile(uncanonicalGenesisFilePath, rawUncanonical, 0o600); err != nil { + return fmt.Errorf("failed to write mashalled genesis document to file: %w", err) } + return nil }