From 3d0a0ce5dab693ab8dee640893a6ad11bca8b943 Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Tue, 19 May 2020 14:44:08 +0000 Subject: [PATCH] go/oasis-node/cmd/debug: Add the `dumpdb` command This command will attempt to extract the ABCI state from a combination of a shutdown node's on-disk database and the genesis document currently being used by the network, and will write the output as a JSON formatted genesis document. Some caveats: * It is not guaranteed that the dumped output will be usable as an actual genesis document without manual intervention. * Only the state that would be exported via a normal dump from a running node will be present in the dump. --- .changelog/2359.feature.md | 14 + go/oasis-node/cmd/debug/debug.go | 2 + go/oasis-node/cmd/debug/dumpdb/dumpdb.go | 383 +++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 .changelog/2359.feature.md create mode 100644 go/oasis-node/cmd/debug/dumpdb/dumpdb.go diff --git a/.changelog/2359.feature.md b/.changelog/2359.feature.md new file mode 100644 index 00000000000..b986d3235a1 --- /dev/null +++ b/.changelog/2359.feature.md @@ -0,0 +1,14 @@ +go/oasis-node/cmd/debug: Add the `dumpdb` command + +This command will attempt to extract the ABCI state from a combination +of a shutdown node's on-disk database and the genesis document currently +being used by the network, and will write the output as a JSON formatted +genesis document. + +Some caveats: + +- It is not guaranteed that the dumped output will be usable as an + actual genesis document without manual intervention. + +- Only the state that would be exported via a normal dump from a running + node will be present in the dump. diff --git a/go/oasis-node/cmd/debug/debug.go b/go/oasis-node/cmd/debug/debug.go index 4adfd41a994..a94dec80183 100644 --- a/go/oasis-node/cmd/debug/debug.go +++ b/go/oasis-node/cmd/debug/debug.go @@ -7,6 +7,7 @@ import ( "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/byzantine" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/consim" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/control" + "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/dumpdb" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/fixgenesis" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/storage" "github.com/oasislabs/oasis-core/go/oasis-node/cmd/debug/txsource" @@ -25,6 +26,7 @@ func Register(parentCmd *cobra.Command) { fixgenesis.Register(debugCmd) control.Register(debugCmd) consim.Register(debugCmd) + dumpdb.Register(debugCmd) parentCmd.AddCommand(debugCmd) } diff --git a/go/oasis-node/cmd/debug/dumpdb/dumpdb.go b/go/oasis-node/cmd/debug/dumpdb/dumpdb.go new file mode 100644 index 00000000000..7b39d451698 --- /dev/null +++ b/go/oasis-node/cmd/debug/dumpdb/dumpdb.go @@ -0,0 +1,383 @@ +// Package dumpdb implements the dumpdb sub-command. +package dumpdb + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + beacon "github.com/oasislabs/oasis-core/go/beacon/api" + "github.com/oasislabs/oasis-core/go/common/logging" + consensus "github.com/oasislabs/oasis-core/go/consensus/genesis" + tendermint "github.com/oasislabs/oasis-core/go/consensus/tendermint" + "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci" + abciState "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci/state" + tendermintAPI "github.com/oasislabs/oasis-core/go/consensus/tendermint/api" + beaconApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/beacon" + keymanagerApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/keymanager" + registryApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/registry" + roothashApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/roothash" + schedulerApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/scheduler" + stakingApp "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking" + epochtime "github.com/oasislabs/oasis-core/go/epochtime/api" + genesis "github.com/oasislabs/oasis-core/go/genesis/api" + genesisFile "github.com/oasislabs/oasis-core/go/genesis/file" + keymanager "github.com/oasislabs/oasis-core/go/keymanager/api" + cmdCommon "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common" + "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/flags" + registry "github.com/oasislabs/oasis-core/go/registry/api" + roothash "github.com/oasislabs/oasis-core/go/roothash/api" + scheduler "github.com/oasislabs/oasis-core/go/scheduler/api" + staking "github.com/oasislabs/oasis-core/go/staking/api" + storage "github.com/oasislabs/oasis-core/go/storage/api" + storageDB "github.com/oasislabs/oasis-core/go/storage/database" + + "encoding/json" +) + +const ( + cfgDumpOutput = "dump.output" + cfgDumpReadOnlyDB = "dump.read_only_db" + cfgDumpVersion = "dump.version" +) + +var ( + dumpDBCmd = &cobra.Command{ + Use: "dumpdb", + Short: "dump the on-disk consensus DB to a JSON document", + Run: doDumpDB, + } + + dumpDBFlags = flag.NewFlagSet("", flag.ContinueOnError) + + logger = logging.GetLogger("cmd/debug/dumpdb") +) + +func doDumpDB(cmd *cobra.Command, args []string) { + var ok bool + defer func() { + if !ok { + os.Exit(1) + } + }() + + if err := cmdCommon.Init(); err != nil { + cmdCommon.EarlyLogAndExit(err) + } + + dataDir := cmdCommon.DataDir() + if dataDir == "" { + logger.Error("data directory must be set") + return + } + + // Load the old genesis document, required for filling in parameters + // that are not persisted to ABCI state. + fp, err := genesisFile.NewFileProvider(flags.GenesisFile()) + if err != nil { + logger.Error("failed to load existing genesis document", + "err", err, + ) + return + } + oldDoc, err := fp.GetGenesisDocument() + if err != nil { + logger.Error("failed to get existing genesis document", + "err", err, + ) + return + } + + // Initialize the ABCI state storage for access. + // + // Note: While it would be great to always use read-only DB access, + // badger will refuse to open a DB that isn't closed properly in + // read-only mode because it needs to truncate the value log. + // + // Hope you have backups if you ever run into this. + ctx := context.Background() + ldb, _, stateRoot, err := abci.InitStateStorage( + ctx, + &abci.ApplicationConfig{ + DataDir: filepath.Join(dataDir, tendermint.StateDir), + StorageBackend: storageDB.BackendNameBadgerDB, // No other backend for now. + MemoryOnlyStorage: false, + ReadOnlyStorage: viper.GetBool(cfgDumpReadOnlyDB), + }, + ) + if err != nil { + logger.Error("failed to initialize ABCI storage backend", + "err", err, + ) + } + defer ldb.Cleanup() + + latestVersion := int64(stateRoot.Version) + dumpVersion := viper.GetInt64(cfgDumpVersion) + if dumpVersion == 0 { + dumpVersion = latestVersion + } + if dumpVersion <= 0 || dumpVersion > latestVersion { + logger.Error("dump requested for version that does not exist", + "dump_version", dumpVersion, + "latest_version", latestVersion, + ) + return + } + + // Generate the dump by querying all of the relevant backends, and + // extracting the immutable parameters from the current genesis + // document. + // + // WARNING: The state is not guaranteed to be usable as a genesis + // document without manual intervention, and only the state that + // would be exported by the normal dump process will be present + // in the dump. + qs := &dumpQueryState{ + ldb: ldb, + height: dumpVersion, + } + doc := &genesis.Document{ + Height: qs.BlockHeight(), + Time: time.Now(), // XXX: Make this deterministic? + ChainID: oldDoc.ChainID, + EpochTime: oldDoc.EpochTime, + HaltEpoch: oldDoc.HaltEpoch, + ExtraData: oldDoc.ExtraData, + } + + // Registry + registrySt, err := dumpRegistry(ctx, qs) + if err != nil { + logger.Error("failed to dump registry state", + "err", err, + ) + } + doc.Registry = *registrySt + + // RootHash + rootHashSt, err := dumpRootHash(ctx, qs) + if err != nil { + logger.Error("failed to dump root hash state", + "err", err, + ) + return + } + doc.RootHash = *rootHashSt + + // Staking + stakingSt, err := dumpStaking(ctx, qs) + if err != nil { + logger.Error("failed to dump staking state", + "err", err, + ) + return + } + doc.Staking = *stakingSt + + // KeyManager + keyManagerSt, err := dumpKeyManager(ctx, qs) + if err != nil { + logger.Error("failed to dump key manager state", + "err", err, + ) + return + } + doc.KeyManager = *keyManagerSt + + // Scheduler + schedulerSt, err := dumpScheduler(ctx, qs) + if err != nil { + logger.Error("failed to dump scheduler state", + "err", err, + ) + return + } + doc.Scheduler = *schedulerSt + + // Beacon + beaconSt, err := dumpBeacon(ctx, qs) + if err != nil { + logger.Error("failed to dump beacon state", + "err", err, + ) + return + } + doc.Beacon = *beaconSt + + // Consensus + consensusSt, err := dumpConsensus(ctx, qs) + if err != nil { + logger.Error("failed to dump consensus state", + "err", err, + ) + return + } + doc.Consensus = *consensusSt + + logger.Info("writing state dump", + "output", viper.GetString(cfgDumpOutput), + ) + + // Write out the document. + w, shouldClose, err := cmdCommon.GetOutputWriter(cmd, cfgDumpOutput) + if err != nil { + logger.Error("failed to get output writer for state dump", + "err", err, + ) + return + } + if shouldClose { + defer w.Close() + } + raw, err := json.Marshal(doc) + if err != nil { + logger.Error("failed to marshal state dump into JSON", + "err", err, + ) + return + } + if _, err := w.Write(raw); err != nil { + logger.Error("failed to write state dump file", + "err", err, + ) + return + } + + ok = true +} + +func dumpRegistry(ctx context.Context, qs *dumpQueryState) (*registry.Genesis, error) { + qf := registryApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create registry query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump registry state: %w", err) + } + return st, nil +} + +func dumpRootHash(ctx context.Context, qs *dumpQueryState) (*roothash.Genesis, error) { + qf := roothashApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create root hash query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump root hash state: %w", err) + } + return st, nil +} + +func dumpStaking(ctx context.Context, qs *dumpQueryState) (*staking.Genesis, error) { + qf := stakingApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create staking query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump staking state: %w", err) + } + return st, nil +} + +func dumpKeyManager(ctx context.Context, qs *dumpQueryState) (*keymanager.Genesis, error) { + qf := keymanagerApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create key manager query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump key manager state: %w", err) + } + return st, nil +} + +func dumpScheduler(ctx context.Context, qs *dumpQueryState) (*scheduler.Genesis, error) { + qf := schedulerApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create scheduler query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump scheduler state: %w", err) + } + return st, nil +} + +func dumpBeacon(ctx context.Context, qs *dumpQueryState) (*beacon.Genesis, error) { + qf := beaconApp.NewQueryFactory(qs) + q, err := qf.QueryAt(ctx, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to create beacon query: %w", err) + } + st, err := q.Genesis(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to dump beacon state: %w", err) + } + return st, nil +} + +func dumpConsensus(ctx context.Context, qs *dumpQueryState) (*consensus.Genesis, error) { + is, err := abciState.NewImmutableState(ctx, qs, qs.BlockHeight()) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to get consensus state: %w", err) + } + params, err := is.ConsensusParameters(ctx) + if err != nil { + return nil, fmt.Errorf("dumpdb: failed to get consensus params: %w", err) + } + _ = params + return &consensus.Genesis{ + Backend: tendermintAPI.BackendName, + Parameters: *params, + }, nil +} + +type dumpQueryState struct { + ldb storage.LocalBackend + height int64 +} + +func (qs *dumpQueryState) Storage() storage.LocalBackend { + return qs.ldb +} + +func (qs *dumpQueryState) BlockHeight() int64 { + return qs.height +} + +func (qs *dumpQueryState) GetEpoch(ctx context.Context, blockHeight int64) (epochtime.EpochTime, error) { + // This is only required because certain registry backend queries + // need the epoch to filter out expired nodes. It is not + // implemented because acquiring a full state dump does not + // involve any of the relevant queries. + return epochtime.EpochTime(0), fmt.Errorf("dumpdb/dumpQueryState: GetEpoch not supported") +} + +// Register registers the dumpdb sub-commands. +func Register(parentCmd *cobra.Command) { + dumpDBCmd.Flags().AddFlagSet(flags.GenesisFileFlags) + dumpDBCmd.Flags().AddFlagSet(dumpDBFlags) + parentCmd.AddCommand(dumpDBCmd) +} + +func init() { + dumpDBFlags.String(cfgDumpOutput, "dump.json", "path to dumped ABCI state") + dumpDBFlags.Bool(cfgDumpReadOnlyDB, true, "read-only DB access") + dumpDBFlags.Int64(cfgDumpVersion, 0, "ABCI state version to dump (0 = most recent)") + _ = viper.BindPFlags(dumpDBFlags) +}