Skip to content

Commit

Permalink
feat: dot/state: implement import-state subcommand (#1455)
Browse files Browse the repository at this point in the history
  • Loading branch information
noot committed Mar 12, 2021
1 parent 4d432d9 commit c51be1c
Show file tree
Hide file tree
Showing 13 changed files with 585 additions and 39 deletions.
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)
}

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

0 comments on commit c51be1c

Please sign in to comment.