Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

go/oasis-node/cmd/genesis: Add check for canonical form to check command & Docs #3298

Merged
merged 2 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/3298.doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Document [`oasis-node genesis` CLI commands]

[`oasis-node genesis` CLI commands]: docs/oasis-node/cli.md#genesis
1 change: 1 addition & 0 deletions .changelog/3298.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/oasis-node: Make `oasis-node genesis check` command check if form canonical
58 changes: 58 additions & 0 deletions docs/oasis-node/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
39 changes: 38 additions & 1 deletion go/oasis-node/cmd/genesis/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
package genesis

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math"
"os"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down
86 changes: 50 additions & 36 deletions go/oasis-test-runner/scenario/e2e/genesis_file.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -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)
Expand All @@ -72,62 +71,77 @@ 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",
"--height", "0",
"--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)
}
doc, err := provider.GetGenesisDocument()
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
}