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

feat: dot/state: implement import-state subcommand #1455

Merged
merged 12 commits into from
Mar 12, 2021
70 changes: 36 additions & 34 deletions cmd/gossamer/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ var (
defaultGssmrConfigPath = "./chain/gssmr/config.toml"
defaultKusamaConfigPath = "./chain/ksmcc/config.toml"
defaultPolkadotConfigPath = "./chain/polkadot/config.toml"

gossamerName = "gssmr"
kusamaName = "ksmcc"
polkadotName = "polkadot"
)

// loadConfigFile loads a default config file if --chain is specified, a specific
Expand All @@ -68,39 +72,49 @@ func loadConfigFile(ctx *cli.Context, cfg *ctoml.Config) (err error) {
return err
}

// createDotConfig creates a new dot configuration from the provided flag values
func createDotConfig(ctx *cli.Context) (cfg *dot.Config, err error) {
func setupConfigFromChain(ctx *cli.Context) (*ctoml.Config, *dot.Config, error) {
tomlCfg := &ctoml.Config{}
cfg = DefaultCfg()
cfg := DefaultCfg()

err = loadConfigFile(ctx, tomlCfg)
err := loadConfigFile(ctx, tomlCfg)
if err != nil {
logger.Error("failed to load toml configuration", "error", err)
return nil, err
return nil, nil, err
}

// check --chain flag and load configuration from defaults.go
if id := ctx.GlobalString(ChainFlag.Name); id != "" {
switch id {
case "gssmr":
case gossamerName:
logger.Info("loading toml configuration...", "config path", defaultGssmrConfigPath)
tomlCfg = &ctoml.Config{}
err = loadConfig(tomlCfg, defaultGssmrConfigPath)
case "ksmcc":
case kusamaName:
logger.Info("loading toml configuration...", "config path", defaultKusamaConfigPath)
tomlCfg = &ctoml.Config{}
cfg = dot.KsmccConfig()
err = loadConfig(tomlCfg, defaultKusamaConfigPath)
case "polkadot":
case polkadotName:
logger.Info("loading toml configuration...", "config path", defaultPolkadotConfigPath)
tomlCfg = &ctoml.Config{}
cfg = dot.PolkadotConfig()
err = loadConfig(tomlCfg, defaultPolkadotConfigPath)
default:
return nil, fmt.Errorf("unknown chain id provided: %s", id)
return nil, nil, fmt.Errorf("unknown chain id provided: %s", id)
}
}

if err != nil {
logger.Error("failed to set chain configuration", "error", err)
return nil, nil, err
}

return tomlCfg, cfg, nil
}

// createDotConfig creates a new dot configuration from the provided flag values
func createDotConfig(ctx *cli.Context) (*dot.Config, error) {
tomlCfg, cfg, err := setupConfigFromChain(ctx)
if err != nil {
logger.Error("failed to set chain configuration", "error", err)
return nil, err
Expand Down Expand Up @@ -137,31 +151,7 @@ func createDotConfig(ctx *cli.Context) (cfg *dot.Config, err error) {

// createInitConfig creates the configuration required to initialize a dot node
func createInitConfig(ctx *cli.Context) (*dot.Config, error) {
tomlCfg := &ctoml.Config{}
cfg := DefaultCfg()

err := loadConfigFile(ctx, tomlCfg)
if err != nil {
logger.Error("failed to load toml configuration", "error", err)
return nil, err
}

// check --chain flag and load configuration from defaults.go
if id := ctx.GlobalString(ChainFlag.Name); id != "" {
switch id {
case "gssmr":
tomlCfg = &ctoml.Config{}
err = loadConfig(tomlCfg, defaultGssmrConfigPath)
case "ksmcc":
tomlCfg = &ctoml.Config{}
err = loadConfig(tomlCfg, defaultKusamaConfigPath)
case "polkadot":
tomlCfg = &ctoml.Config{}
err = loadConfig(tomlCfg, defaultPolkadotConfigPath)
default:
return nil, fmt.Errorf("unknown chain id provided: %s", id)
}
}
tomlCfg, cfg, err := setupConfigFromChain(ctx)
if err != nil {
logger.Error("failed to set chain configuration", "error", err)
return nil, err
Expand Down Expand Up @@ -196,6 +186,18 @@ func createInitConfig(ctx *cli.Context) (*dot.Config, error) {
return cfg, nil
}

func createImportStateConfig(ctx *cli.Context) (*dot.Config, error) {
tomlCfg, cfg, err := setupConfigFromChain(ctx)
if err != nil {
logger.Error("failed to set chain configuration", "error", err)
return nil, err
}

// set global configuration values
setDotGlobalConfig(ctx, tomlCfg, &cfg.Global)
return cfg, nil
}

func createBuildSpecConfig(ctx *cli.Context) (*dot.Config, error) {
var tomlCfg *ctoml.Config
cfg := &dot.Config{}
Expand Down
25 changes: 25 additions & 0 deletions cmd/gossamer/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ var (
}
)

// ImportState-only flags
var (
StateFlag = cli.StringFlag{
Name: "state",
Usage: "Path to JSON file consisting of key-value pairs",
}
HeaderFlag = cli.StringFlag{
Name: "header",
Usage: "Path to JSON file of block header corresponding to the given state",
}
FirstSlotFlag = cli.IntFlag{
Name: "first-slot",
Usage: "The first BABE slot of the network",
}
)

// BuildSpec-only flags
var (
RawFlag = cli.BoolFlag{
Expand Down Expand Up @@ -315,6 +331,15 @@ var (
Sr25519Flag,
Secp256k1Flag,
}, GlobalFlags...)

ImportStateFlags = []cli.Flag{
BasePathFlag,
ChainFlag,
ConfigFlag,
StateFlag,
HeaderFlag,
FirstSlotFlag,
}
)

// FixFlagOrder allow us to use various flag order formats (ie, `gossamer init
Expand Down
42 changes: 42 additions & 0 deletions cmd/gossamer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package main

import (
"errors"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -93,6 +94,18 @@ var (
Description: "The convert-wasm command converts a .wasm file to a hex string to be used in a genesis file.\n" +
"\tUsage: gossamer convert-wasm runtime.wasm\n",
}

importStateCommand = cli.Command{
Action: FixFlagOrder(importStateAction),
Name: "import-state",
Usage: "Import state from a JSON file and set it as the chain head state",
ArgsUsage: "",
Flags: ImportStateFlags,
Category: "IMPORT-STATE",
Description: "The import-state command allows a JSON file containing a given state in the form of key-value pairs to be imported.\n" +
"Input can be generated by using the RPC function state_getPairs.\n" +
"\tUsage: gossamer import-state --state state.json --header header.json --first-slot <first slot of network>\n",
}
)

// init initializes the cli application
Expand All @@ -109,6 +122,7 @@ func init() {
accountCommand,
buildSpecCommand,
wasmToHexCommand,
importStateCommand,
}
app.Flags = RootFlags
}
Expand All @@ -121,6 +135,34 @@ func main() {
}
}

func importStateAction(ctx *cli.Context) error {
var (
stateFP, headerFP string
firstSlot int
)

if stateFP = ctx.String(StateFlag.Name); stateFP == "" {
return errors.New("must provide argument to --state")
}

if headerFP = ctx.String(HeaderFlag.Name); headerFP == "" {
return errors.New("must provide argument to --header")
}

if firstSlot = ctx.Int(FirstSlotFlag.Name); firstSlot == 0 {
return errors.New("must provide argument to --first-slot")
}

cfg, err := createImportStateConfig(ctx)
if err != nil {
logger.Error("failed to create node configuration", "error", err)
return err
}
cfg.Global.BasePath = utils.ExpandDir(cfg.Global.BasePath)

return dot.ImportState(cfg.Global.BasePath, stateFP, headerFP, uint64(firstSlot))
}

// wasmToHexAction converts a .wasm file to a hex string and outputs it to stdout
func wasmToHexAction(ctx *cli.Context) error {
arguments := ctx.Args()
Expand Down
150 changes: 150 additions & 0 deletions dot/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2019 ChainSafe Systems (ON) Corp.
// This file is part of gossamer.
//
// The gossamer library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The gossamer library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the gossamer library. If not, see <http://www.gnu.org/licenses/>.

package dot

import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"math/big"
"path/filepath"

"github.com/ChainSafe/gossamer/dot/state"
"github.com/ChainSafe/gossamer/dot/types"
"github.com/ChainSafe/gossamer/lib/common"
"github.com/ChainSafe/gossamer/lib/trie"

log "github.com/ChainSafe/log15"
)

// ImportState imports the state in the given files to the database with the given path.
func ImportState(basepath, stateFP, headerFP string, firstSlot uint64) error {
tr, err := newTrieFromPairs(stateFP)
if err != nil {
return err
}

header, err := newHeaderFromFile(headerFP)
if err != nil {
return err
}

log.Info("ImportState", "header", header)

srv := state.NewService(basepath, log.LvlInfo)
return srv.Import(header, tr, firstSlot)
}

func newTrieFromPairs(filename string) (*trie.Trie, error) {
data, err := ioutil.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, err
}

pairs := make([]interface{}, 0)
err = json.Unmarshal(data, &pairs)
if err != nil {
return nil, err
}

entries := make(map[string]string)
for _, pair := range pairs {
pairArr := pair.([]interface{})
if len(pairArr) != 2 {
return nil, errors.New("state file contains invalid pair")
}
entries[pairArr[0].(string)] = pairArr[1].(string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assert as a safety check that the length of pairArr is always equal to two?

}

tr := trie.NewEmptyTrie()
err = tr.LoadFromMap(entries)
if err != nil {
return nil, err
}

return tr, nil
}

func newHeaderFromFile(filename string) (*types.Header, error) {
data, err := ioutil.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, err
}

jsonHeader := make(map[string]interface{})
err = json.Unmarshal(data, &jsonHeader)
if err != nil {
return nil, err
}

hexNum, ok := jsonHeader["number"].(string)
if !ok {
return nil, errors.New("invalid number field in header JSON")
}

numBytes := common.MustHexToBytes(hexNum)
num := big.NewInt(0).SetBytes(numBytes)

parentHashStr, ok := jsonHeader["parentHash"].(string)
if !ok {
return nil, errors.New("invalid parentHash field in header JSON")
}
parentHash := common.MustHexToHash(parentHashStr)

stateRootStr, ok := jsonHeader["stateRoot"].(string)
if !ok {
return nil, errors.New("invalid stateRoot field in header JSON")
}
stateRoot := common.MustHexToHash(stateRootStr)

extrinsicsRootStr, ok := jsonHeader["extrinsicsRoot"].(string)
if !ok {
return nil, errors.New("invalid extrinsicsRoot field in header JSON")
}
extrinsicsRoot := common.MustHexToHash(extrinsicsRootStr)

digestRaw, ok := jsonHeader["digest"].(map[string]interface{})
if !ok {
return nil, errors.New("invalid digest field in header JSON")
}
logs := digestRaw["logs"].([]interface{})

digest := types.Digest{}

for _, log := range logs {
digestBytes := common.MustHexToBytes(log.(string))
r := &bytes.Buffer{}
_, _ = r.Write(digestBytes)
digestItem, err := types.DecodeDigestItem(r)
if err != nil {
return nil, err
}

digest = append(digest, digestItem)
}

header := &types.Header{
ParentHash: parentHash,
Number: num,
StateRoot: stateRoot,
ExtrinsicsRoot: extrinsicsRoot,
Digest: digest,
}

return header, nil
}
Loading