From 405192718092a8bb602dbe34cbb440ae1b44904f Mon Sep 17 00:00:00 2001 From: yihuang Date: Thu, 11 May 2023 18:11:31 +0800 Subject: [PATCH] feat: add local snapshots management commands (#16067) Co-authored-by: Marko (cherry picked from commit c1ceb3bdda83ebc1bbe4878a67b0df31d2a50d5a) # Conflicts: # CHANGELOG.md # go.mod # server/types/app.go # simapp/simd/cmd/root.go # simapp/simd/cmd/root_v2.go # snapshots/store.go --- CHANGELOG.md | 41 +++++ client/snapshot/cmd.go | 24 +++ client/snapshot/delete.go | 35 ++++ client/snapshot/dump.go | 119 +++++++++++++ client/snapshot/export.go | 54 ++++++ client/snapshot/list.go | 30 ++++ client/snapshot/load.go | 113 ++++++++++++ client/snapshot/restore.go | 52 ++++++ go.mod | 7 + server/types/app.go | 13 ++ server/util.go | 30 ++-- simapp/simd/cmd/root.go | 11 ++ simapp/simd/cmd/root_v2.go | 343 +++++++++++++++++++++++++++++++++++++ snapshots/manager.go | 19 ++ snapshots/store.go | 48 ++++++ 15 files changed, 929 insertions(+), 10 deletions(-) create mode 100644 client/snapshot/cmd.go create mode 100644 client/snapshot/delete.go create mode 100644 client/snapshot/dump.go create mode 100644 client/snapshot/export.go create mode 100644 client/snapshot/list.go create mode 100644 client/snapshot/load.go create mode 100644 client/snapshot/restore.go create mode 100644 simapp/simd/cmd/root_v2.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f43850cf359..39c9451b1345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,47 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (client) [#16075](https://github.com/cosmos/cosmos-sdk/pull/16075) Partly revert [#15953](https://github.com/cosmos/cosmos-sdk/issues/15953) and `factory.Prepare` now does nothing in offline mode. * (server) [#15984](https://github.com/cosmos/cosmos-sdk/pull/15984) Use `cosmossdk.io/log` package for logging instead of CometBFT logger. NOTE: v0.45 and v0.46 were not using CometBFT logger either. This keeps the same underlying logger (zerolog) as in v0.45.x+ and v0.46.x+ but now properly supporting filtered logging. * (gov) [#15979](https://github.com/cosmos/cosmos-sdk/pull/15979) Improve gov error message when failing to convert v1 proposal to v1beta1. +<<<<<<< HEAD +======= +* (crypto) [#3129](https://github.com/cosmos/cosmos-sdk/pull/3129) New armor and keyring key derivation uses aead and encryption uses chacha20poly +* (x/slashing) [#15580](https://github.com/cosmos/cosmos-sdk/pull/15580) Refactor the validator's missed block signing window to be a chunked bitmap instead of a "logical" bitmap, significantly reducing the storage footprint. +* (x/gov) [#15554](https://github.com/cosmos/cosmos-sdk/pull/15554) Add proposal result log in `active_proposal` event. When a proposal passes but fails to execute, the proposal result is logged in the `active_proposal` event. +* (mempool) [#15328](https://github.com/cosmos/cosmos-sdk/pull/15328) Improve the `PriorityNonceMempool` + * Support generic transaction prioritization, instead of `ctx.Priority()` + * Improve construction through the use of a single `PriorityNonceMempoolConfig` instead of option functions +* (x/authz) [#15164](https://github.com/cosmos/cosmos-sdk/pull/15164) Add `MsgCancelUnbondingDelegation` to staking authorization +* (server) [#15358](https://github.com/cosmos/cosmos-sdk/pull/15358) Add `server.InterceptConfigsAndCreateContext` as alternative to `server.InterceptConfigsPreRunHandler` which does not set the server context and the default SDK logger. +* [#15011](https://github.com/cosmos/cosmos-sdk/pull/15011) Introduce `cosmossdk.io/log` package to provide a consistent logging interface through the SDK. CometBFT logger is now replaced by `cosmossdk.io/log.Logger`. +* (x/auth) [#14758](https://github.com/cosmos/cosmos-sdk/pull/14758) Allow transaction event queries to directly passed to Tendermint, which will allow for full query operator support, e.g. `>`. +* (server) [#15041](https://github.com/cosmos/cosmos-sdk/pull/15041) Remove unnecessary sleeps from gRPC and API server initiation. The servers will start and accept requests as soon as they're ready. +* (x/staking) [#14864](https://github.com/cosmos/cosmos-sdk/pull/14864) `create-validator` CLI command now takes a json file as an arg instead of having a bunch of required flags to it. +* (cli) [#14659](https://github.com/cosmos/cosmos-sdk/pull/14659) Added ability to query blocks by either height/hash ` q block --type=height|hash `. +* (store) [#14410](https://github.com/cosmos/cosmos-sdk/pull/14410) `rootmulti.Store.loadVersion` has validation to check if all the module stores' height is correct, it will error if any module store has incorrect height. +* (x/evidence) [#14757](https://github.com/cosmos/cosmos-sdk/pull/14757) Evidence messages do not need to implement a `.Type()` anymore. +* (x/auth/tx) [#14751](https://github.com/cosmos/cosmos-sdk/pull/14751) Remove `.Type()` and `Route()` methods from all msgs and `legacytx.LegacyMsg` interface. +* [#14529](https://github.com/cosmos/cosmos-sdk/pull/14529) Add new property `BondDenom` to `SimulationState` struct. +* (module) [#14415](https://github.com/cosmos/cosmos-sdk/pull/14415) Loosen assertions in SetOrderBeginBlockers() and SetOrderEndBlockers() +* (context)[#14384](https://github.com/cosmos/cosmos-sdk/pull/14384) refactor(context): Pass EventManager to the context as an interface. +* (types) [#14354](https://github.com/cosmos/cosmos-sdk/pull/14354) improve performance on Context.KVStore and Context.TransientStore by 40% +* (crypto/keyring) [#14151](https://github.com/cosmos/cosmos-sdk/pull/14151) Move keys presentation from `crypto/keyring` to `client/keys` +* (signing) [#14087](https://github.com/cosmos/cosmos-sdk/pull/14087) Add SignModeHandlerWithContext interface with a new `GetSignBytesWithContext` to get the sign bytes using `context.Context` as an argument to access state. +* (server) [#14062](https://github.com/cosmos/cosmos-sdk/pull/14062) Remove rosetta from server start. +* (baseapp) [#14417](https://github.com/cosmos/cosmos-sdk/pull/14417) `SetStreamingService` accepts appOptions, AppCodec and Storekeys needed to set streamers. + * Store pacakge no longer has a dependency on baseapp. +* (store) [#14438](https://github.com/cosmos/cosmos-sdk/pull/14438) Pass logger from baseapp to store. +* (store) [#14439](https://github.com/cosmos/cosmos-sdk/pull/14439) Remove global metric gatherer from store. + * By default store has a no op metric gatherer, the application developer must set another metric gatherer or us the provided one in `store/metrics`. +* [#14406](https://github.com/cosmos/cosmos-sdk/issues/14406) Migrate usage of types/store.go to store/types/.. +* (x/staking) [#14590](https://github.com/cosmos/cosmos-sdk/pull/14590) Return undelegate amount in MsgUndelegateResponse. +* (baseapp) [#15023](https://github.com/cosmos/cosmos-sdk/pull/15023) & [#15213](https://github.com/cosmos/cosmos-sdk/pull/15213) Add `MessageRouter` interface to baseapp and pass it to authz, gov and groups instead of concrete type. +* (simtestutil) [#15305](https://github.com/cosmos/cosmos-sdk/pull/15305) Add `AppStateFnWithExtendedCb` with callback function to extend rawState. +* (x/consensus) [#15553](https://github.com/cosmos/cosmos-sdk/pull/15553) Migrate consensus module to use collections +* (x/bank) [#15764](https://github.com/cosmos/cosmos-sdk/pull/15764) Speedup x/bank InitGenesis +* (x/auth) [#15867](https://github.com/cosmos/cosmos-sdk/pull/15867) Support better logging for signature verification failure. +* (types/query) [#16041](https://github.com/cosmos/cosmos-sdk/pull/16041) change pagination max limit to a variable in order to be modifed by application devs +* (server) [#16061](https://github.com/cosmos/cosmos-sdk/pull/16061) add comet bootstrap command +* (store) [#16067](https://github.com/cosmos/cosmos-sdk/pull/16067) Add local snapshots management commands. +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) ### Bug Fixes diff --git a/client/snapshot/cmd.go b/client/snapshot/cmd.go new file mode 100644 index 000000000000..f49f2b51c2b4 --- /dev/null +++ b/client/snapshot/cmd.go @@ -0,0 +1,24 @@ +package snapshot + +import ( + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/spf13/cobra" +) + +// Cmd returns the snapshots group command +func Cmd(appCreator servertypes.AppCreator) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshots", + Short: "Manage local snapshots", + Long: "Manage local snapshots", + } + cmd.AddCommand( + ListSnapshotsCmd, + RestoreSnapshotCmd(appCreator), + ExportSnapshotCmd(appCreator), + DumpArchiveCmd(), + LoadArchiveCmd(), + DeleteSnapshotCmd(), + ) + return cmd +} diff --git a/client/snapshot/delete.go b/client/snapshot/delete.go new file mode 100644 index 000000000000..0259032e1134 --- /dev/null +++ b/client/snapshot/delete.go @@ -0,0 +1,35 @@ +package snapshot + +import ( + "strconv" + + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" +) + +func DeleteSnapshotCmd() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a local snapshot", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + format, err := strconv.ParseUint(args[1], 10, 32) + if err != nil { + return err + } + + snapshotStore, err := server.GetSnapshotStore(ctx.Viper) + if err != nil { + return err + } + + return snapshotStore.Delete(height, uint32(format)) + }, + } +} diff --git a/client/snapshot/dump.go b/client/snapshot/dump.go new file mode 100644 index 000000000000..70f223a59055 --- /dev/null +++ b/client/snapshot/dump.go @@ -0,0 +1,119 @@ +package snapshot + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "strconv" + + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" +) + +// DumpArchiveCmd returns a command to dump the snapshot as portable archive format +func DumpArchiveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "dump ", + Short: "Dump the snapshot as portable archive format", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + snapshotStore, err := server.GetSnapshotStore(ctx.Viper) + if err != nil { + return err + } + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + format, err := strconv.ParseUint(args[1], 10, 32) + if err != nil { + return err + } + + if output == "" { + output = fmt.Sprintf("%d-%d.tar.gz", height, format) + } + + snapshot, err := snapshotStore.Get(height, uint32(format)) + if err != nil { + return err + } + + bz, err := snapshot.Marshal() + if err != nil { + return err + } + + fp, err := os.Create(output) + if err != nil { + return err + } + defer fp.Close() + + // since the chunk files are already compressed, we just use fastest compression here + gzipWriter, err := gzip.NewWriterLevel(fp, gzip.BestSpeed) + if err != nil { + return err + } + tarWriter := tar.NewWriter(gzipWriter) + if err := tarWriter.WriteHeader(&tar.Header{ + Name: SnapshotFileName, + Mode: 0o644, + Size: int64(len(bz)), + }); err != nil { + return fmt.Errorf("failed to write snapshot header to tar: %w", err) + } + if _, err := tarWriter.Write(bz); err != nil { + return fmt.Errorf("failed to write snapshot to tar: %w", err) + } + + for i := uint32(0); i < snapshot.Chunks; i++ { + path := snapshotStore.PathChunk(height, uint32(format), i) + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open chunk file %s: %w", path, err) + } + + st, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat chunk file %s: %w", path, err) + } + + if err := tarWriter.WriteHeader(&tar.Header{ + Name: strconv.FormatUint(uint64(i), 10), + Mode: 0o644, + Size: st.Size(), + }); err != nil { + return fmt.Errorf("failed to write chunk header to tar: %w", err) + } + + if _, err := io.Copy(tarWriter, file); err != nil { + return fmt.Errorf("failed to write chunk to tar: %w", err) + } + } + + if err := tarWriter.Close(); err != nil { + return fmt.Errorf("failed to close tar writer: %w", err) + } + + if err := gzipWriter.Close(); err != nil { + return fmt.Errorf("failed to close gzip writer: %w", err) + } + + return fp.Close() + }, + } + + cmd.Flags().StringP("output", "o", "", "output file") + + return cmd +} diff --git a/client/snapshot/export.go b/client/snapshot/export.go new file mode 100644 index 000000000000..6cef7531382c --- /dev/null +++ b/client/snapshot/export.go @@ -0,0 +1,54 @@ +package snapshot + +import ( + "fmt" + + "cosmossdk.io/log" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/spf13/cobra" +) + +// ExportSnapshotCmd returns a command to take a snapshot of the application state +func ExportSnapshotCmd(appCreator servertypes.AppCreator) *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "Export app state to snapshot store", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + + height, err := cmd.Flags().GetInt64("height") + if err != nil { + return err + } + + home := ctx.Config.RootDir + db, err := openDB(home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + return err + } + logger := log.NewLogger(cmd.OutOrStdout()) + app := appCreator(logger, db, nil, ctx.Viper) + + if height == 0 { + height = app.CommitMultiStore().LastCommitID().Version + } + + fmt.Printf("Exporting snapshot for height %d\n", height) + + sm := app.SnapshotManager() + snapshot, err := sm.Create(uint64(height)) + if err != nil { + return err + } + + fmt.Printf("Snapshot created at height %d, format %d, chunks %d\n", snapshot.Height, snapshot.Format, snapshot.Chunks) + return nil + }, + } + + cmd.Flags().Int64("height", 0, "Height to export, default to latest state height") + + return cmd +} diff --git a/client/snapshot/list.go b/client/snapshot/list.go new file mode 100644 index 000000000000..6ff6391d4211 --- /dev/null +++ b/client/snapshot/list.go @@ -0,0 +1,30 @@ +package snapshot + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" +) + +// ListSnapshotsCmd returns the command to list local snapshots +var ListSnapshotsCmd = &cobra.Command{ + Use: "list", + Short: "List local snapshots", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + snapshotStore, err := server.GetSnapshotStore(ctx.Viper) + if err != nil { + return err + } + snapshots, err := snapshotStore.List() + if err != nil { + return fmt.Errorf("failed to list snapshots: %w", err) + } + for _, snapshot := range snapshots { + fmt.Println("height:", snapshot.Height, "format:", snapshot.Format, "chunks:", snapshot.Chunks) + } + + return nil + }, +} diff --git a/client/snapshot/load.go b/client/snapshot/load.go new file mode 100644 index 000000000000..b2f33dac0e41 --- /dev/null +++ b/client/snapshot/load.go @@ -0,0 +1,113 @@ +package snapshot + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "os" + "reflect" + "strconv" + + "github.com/cosmos/cosmos-sdk/server" + "github.com/spf13/cobra" + + snapshottypes "cosmossdk.io/store/snapshots/types" +) + +const SnapshotFileName = "_snapshot" + +// LoadArchiveCmd load a portable archive format snapshot into snapshot store +func LoadArchiveCmd() *cobra.Command { + return &cobra.Command{ + Use: "load ", + Short: "Load a snapshot archive file into snapshot store", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + snapshotStore, err := server.GetSnapshotStore(ctx.Viper) + if err != nil { + return err + } + + path := args[0] + fp, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open archive file: %w", err) + } + reader, err := gzip.NewReader(fp) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + + var snapshot snapshottypes.Snapshot + tr := tar.NewReader(reader) + if err != nil { + return fmt.Errorf("failed to create tar reader: %w", err) + } + + hdr, err := tr.Next() + if err != nil { + return fmt.Errorf("failed to read snapshot file header: %w", err) + } + if hdr.Name != SnapshotFileName { + return fmt.Errorf("invalid archive, expect file: snapshot, got: %s", hdr.Name) + } + bz, err := io.ReadAll(tr) + if err != nil { + return fmt.Errorf("failed to read snapshot file: %w", err) + } + if err := snapshot.Unmarshal(bz); err != nil { + return fmt.Errorf("failed to unmarshal snapshot: %w", err) + } + + // make sure the channel is unbuffered, because the tar reader can't do concurrency + chunks := make(chan io.ReadCloser) + quitChan := make(chan *snapshottypes.Snapshot) + go func() { + defer close(quitChan) + + savedSnapshot, err := snapshotStore.Save(snapshot.Height, snapshot.Format, chunks) + if err != nil { + fmt.Println("failed to save snapshot", err) + return + } + quitChan <- savedSnapshot + }() + + for i := uint32(0); i < snapshot.Chunks; i++ { + hdr, err = tr.Next() + if err != nil { + if err == io.EOF { + break + } + return err + } + + if hdr.Name != strconv.FormatInt(int64(i), 10) { + return fmt.Errorf("invalid archive, expect file: %d, got: %s", i, hdr.Name) + } + + bz, err := io.ReadAll(tr) + if err != nil { + return fmt.Errorf("failed to read chunk file: %w", err) + } + chunks <- io.NopCloser(bytes.NewReader(bz)) + } + close(chunks) + + savedSnapshot := <-quitChan + if savedSnapshot == nil { + return fmt.Errorf("failed to save snapshot") + } + + if !reflect.DeepEqual(&snapshot, savedSnapshot) { + _ = snapshotStore.Delete(snapshot.Height, snapshot.Format) + return fmt.Errorf("invalid archive, the saved snapshot is not equal to the original one") + } + + return nil + }, + } +} diff --git a/client/snapshot/restore.go b/client/snapshot/restore.go new file mode 100644 index 000000000000..5d3f45099297 --- /dev/null +++ b/client/snapshot/restore.go @@ -0,0 +1,52 @@ +package snapshot + +import ( + "path/filepath" + "strconv" + + "cosmossdk.io/log" + "github.com/spf13/cobra" + + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" +) + +// RestoreSnapshotCmd returns a command to restore a snapshot +func RestoreSnapshotCmd(appCreator servertypes.AppCreator) *cobra.Command { + cmd := &cobra.Command{ + Use: "restore ", + Short: "Restore app state from local snapshot", + Long: "Restore app state from local snapshot", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := server.GetServerContextFromCmd(cmd) + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + format, err := strconv.ParseUint(args[1], 10, 32) + if err != nil { + return err + } + + home := ctx.Config.RootDir + db, err := openDB(home, server.GetAppDBBackend(ctx.Viper)) + if err != nil { + return err + } + logger := log.NewLogger(cmd.OutOrStdout()) + app := appCreator(logger, db, nil, ctx.Viper) + + sm := app.SnapshotManager() + return sm.RestoreLocalSnapshot(height, uint32(format)) + }, + } + return cmd +} + +func openDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) { + dataDir := filepath.Join(rootDir, "data") + return dbm.NewDB("application", backendType, dataDir) +} diff --git a/go.mod b/go.mod index f2a66dc2784b..48816abeac17 100644 --- a/go.mod +++ b/go.mod @@ -133,10 +133,17 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/nxadm/tail v1.4.8 // indirect +<<<<<<< HEAD github.com/onsi/gomega v1.20.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect +======= + github.com/oklog/run v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.7 // indirect + github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect + github.com/pkg/errors v0.9.1 // indirect +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect diff --git a/server/types/app.go b/server/types/app.go index 734b85c71b21..b9a21346bda4 100644 --- a/server/types/app.go +++ b/server/types/app.go @@ -5,7 +5,13 @@ import ( "io" "time" +<<<<<<< HEAD dbm "github.com/cometbft/cometbft-db" +======= + "cosmossdk.io/log" + "cosmossdk.io/store/snapshots" + storetypes "cosmossdk.io/store/types" +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -58,7 +64,14 @@ type ( RegisterNodeService(client.Context) // CommitMultiStore return the multistore instance +<<<<<<< HEAD CommitMultiStore() sdk.CommitMultiStore +======= + CommitMultiStore() storetypes.CommitMultiStore + + // Return the snapshot manager + SnapshotManager() *snapshots.Manager +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) } // AppCreator is a function that allows us to lazily initialize an diff --git a/server/util.go b/server/util.go index c601c078bba7..cd8b488ffe2d 100644 --- a/server/util.go +++ b/server/util.go @@ -462,16 +462,7 @@ func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) { chainID = appGenesis.ChainID } - snapshotDir := filepath.Join(homeDir, "data", "snapshots") - if err = os.MkdirAll(snapshotDir, os.ModePerm); err != nil { - panic(fmt.Errorf("failed to create snapshots directory: %w", err)) - } - - snapshotDB, err := dbm.NewDB("metadata", GetAppDBBackend(appOpts), snapshotDir) - if err != nil { - panic(err) - } - snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) + snapshotStore, err := GetSnapshotStore(appOpts) if err != nil { panic(err) } @@ -502,3 +493,22 @@ func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) { baseapp.SetChainID(chainID), } } + +func GetSnapshotStore(appOpts types.AppOptions) (*snapshots.Store, error) { + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + snapshotDir := filepath.Join(homeDir, "data", "snapshots") + if err := os.MkdirAll(snapshotDir, os.ModePerm); err != nil { + return nil, fmt.Errorf("failed to create snapshots directory: %w", err) + } + + snapshotDB, err := dbm.NewDB("metadata", GetAppDBBackend(appOpts), snapshotDir) + if err != nil { + return nil, err + } + snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) + if err != nil { + return nil, err + } + + return snapshotStore, nil +} diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index 0f1c7046c6e4..f5aef0bc0531 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -23,6 +23,11 @@ import ( "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/client/pruning" "github.com/cosmos/cosmos-sdk/client/rpc" +<<<<<<< HEAD +======= + "github.com/cosmos/cosmos-sdk/client/snapshot" + "github.com/cosmos/cosmos-sdk/codec" +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) "github.com/cosmos/cosmos-sdk/server" serverconfig "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" @@ -168,8 +173,14 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { genutilcli.InitCmd(simapp.ModuleBasics, simapp.DefaultNodeHome), NewTestnetCmd(simapp.ModuleBasics, banktypes.GenesisBalancesIterator{}), debug.Cmd(), +<<<<<<< HEAD config.Cmd(), pruning.PruningCmd(newApp), +======= + confixcmd.ConfigCommand(), + pruning.Cmd(newApp), + snapshot.Cmd(newApp), +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)) ) server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, appExport, addModuleInitFlags) diff --git a/simapp/simd/cmd/root_v2.go b/simapp/simd/cmd/root_v2.go new file mode 100644 index 000000000000..bfc8079414a9 --- /dev/null +++ b/simapp/simd/cmd/root_v2.go @@ -0,0 +1,343 @@ +//go:build !app_v1 + +package cmd + +import ( + "errors" + "io" + "os" + + cmtcfg "github.com/cometbft/cometbft/config" + dbm "github.com/cosmos/cosmos-db" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "cosmossdk.io/client/v2/autocli" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/simapp" + confixcmd "cosmossdk.io/tools/confix/cmd" + rosettaCmd "cosmossdk.io/tools/rosetta/cmd" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/config" + "github.com/cosmos/cosmos-sdk/client/debug" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/pruning" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/snapshot" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/server" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + txmodule "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" +) + +// NewRootCmd creates a new root command for simd. It is called once in the main function. +func NewRootCmd() *cobra.Command { + var ( + interfaceRegistry codectypes.InterfaceRegistry + appCodec codec.Codec + txConfig client.TxConfig + legacyAmino *codec.LegacyAmino + autoCliOpts autocli.AppOptions + moduleBasicManager module.BasicManager + ) + + if err := depinject.Inject(depinject.Configs(simapp.AppConfig, depinject.Supply(log.NewNopLogger())), + &interfaceRegistry, + &appCodec, + &txConfig, + &legacyAmino, + &autoCliOpts, + &moduleBasicManager, + ); err != nil { + panic(err) + } + + initClientCtx := client.Context{}. + WithCodec(appCodec). + WithInterfaceRegistry(interfaceRegistry). + WithLegacyAmino(legacyAmino). + WithInput(os.Stdin). + WithAccountRetriever(types.AccountRetriever{}). + WithHomeDir(simapp.DefaultNodeHome). + WithViper("") // In simapp, we don't use any prefix for env variables. + + rootCmd := &cobra.Command{ + Use: "simd", + Short: "simulation app", + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + // set the default command outputs + cmd.SetOut(cmd.OutOrStdout()) + cmd.SetErr(cmd.ErrOrStderr()) + + initClientCtx = initClientCtx.WithCmdContext(cmd.Context()) + initClientCtx, err := client.ReadPersistentCommandFlags(initClientCtx, cmd.Flags()) + if err != nil { + return err + } + + initClientCtx, err = config.ReadFromClientConfig(initClientCtx) + if err != nil { + return err + } + + // This needs to go after ReadFromClientConfig, as that function + // sets the RPC client needed for SIGN_MODE_TEXTUAL. + txConfigOpts := tx.ConfigOptions{ + TextualCoinMetadataQueryFn: txmodule.NewGRPCCoinMetadataQueryFn(initClientCtx), + } + txConfigWithTextual := tx.NewTxConfigWithOptions( + codec.NewProtoCodec(interfaceRegistry), + txConfigOpts, + ) + + initClientCtx = initClientCtx.WithTxConfig(txConfigWithTextual) + if err := client.SetCmdClientContextHandler(initClientCtx, cmd); err != nil { + return err + } + + customAppTemplate, customAppConfig := initAppConfig() + customCMTConfig := initCometBFTConfig() + + return server.InterceptConfigsPreRunHandler(cmd, customAppTemplate, customAppConfig, customCMTConfig) + }, + } + + initRootCmd(rootCmd, txConfig, interfaceRegistry, appCodec, moduleBasicManager) + + if err := autoCliOpts.EnhanceRootCommand(rootCmd); err != nil { + panic(err) + } + + return rootCmd +} + +// initCometBFTConfig helps to override default CometBFT Config values. +// return cmtcfg.DefaultConfig if no custom configuration is required for the application. +func initCometBFTConfig() *cmtcfg.Config { + cfg := cmtcfg.DefaultConfig() + + // these values put a higher strain on node memory + // cfg.P2P.MaxNumInboundPeers = 100 + // cfg.P2P.MaxNumOutboundPeers = 40 + + return cfg +} + +// initAppConfig helps to override default appConfig template and configs. +// return "", nil if no custom configuration is required for the application. +func initAppConfig() (string, interface{}) { + // The following code snippet is just for reference. + + // WASMConfig defines configuration for the wasm module. + type WASMConfig struct { + // This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries + QueryGasLimit uint64 `mapstructure:"query_gas_limit"` + + // Address defines the gRPC-web server to listen on + LruSize uint64 `mapstructure:"lru_size"` + } + + type CustomAppConfig struct { + serverconfig.Config + + WASM WASMConfig `mapstructure:"wasm"` + } + + // Optionally allow the chain developer to overwrite the SDK's default + // server config. + srvCfg := serverconfig.DefaultConfig() + // The SDK's default minimum gas price is set to "" (empty value) inside + // app.toml. If left empty by validators, the node will halt on startup. + // However, the chain developer can set a default app.toml value for their + // validators here. + // + // In summary: + // - if you leave srvCfg.MinGasPrices = "", all validators MUST tweak their + // own app.toml config, + // - if you set srvCfg.MinGasPrices non-empty, validators CAN tweak their + // own app.toml to override, or use this default value. + // + // In simapp, we set the min gas prices to 0. + srvCfg.MinGasPrices = "0stake" + // srvCfg.BaseConfig.IAVLDisableFastNode = true // disable fastnode by default + + customAppConfig := CustomAppConfig{ + Config: *srvCfg, + WASM: WASMConfig{ + LruSize: 1, + QueryGasLimit: 300000, + }, + } + + customAppTemplate := serverconfig.DefaultConfigTemplate + ` +[wasm] +# This is the maximum sdk gas (wasm and storage) that we allow for any x/wasm "smart" queries +query_gas_limit = 300000 +# This is the number of wasm vm instances we keep cached in memory for speed-up +# Warning: this is currently unstable and may lead to crashes, best to keep for 0 unless testing locally +lru_size = 0` + + return customAppTemplate, customAppConfig +} + +func initRootCmd( + rootCmd *cobra.Command, + txConfig client.TxConfig, + interfaceRegistry codectypes.InterfaceRegistry, + appCodec codec.Codec, + basicManager module.BasicManager, +) { + cfg := sdk.GetConfig() + cfg.Seal() + + rootCmd.AddCommand( + genutilcli.InitCmd(basicManager, simapp.DefaultNodeHome), + NewTestnetCmd(basicManager, banktypes.GenesisBalancesIterator{}), + debug.Cmd(), + confixcmd.ConfigCommand(), + pruning.Cmd(newApp), + snapshot.Cmd(newApp), + ) + + server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, appExport, addModuleInitFlags) + + // add keybase, auxiliary RPC, query, genesis, and tx child commands + rootCmd.AddCommand( + rpc.StatusCommand(), + genesisCommand(txConfig, basicManager), + queryCommand(), + txCommand(), + keys.Commands(simapp.DefaultNodeHome), + ) + + // add rosetta + rootCmd.AddCommand(rosettaCmd.RosettaCommand(interfaceRegistry, appCodec)) +} + +func addModuleInitFlags(startCmd *cobra.Command) { + crisis.AddModuleInitFlags(startCmd) +} + +// genesisCommand builds genesis-related `simd genesis` command. Users may provide application specific commands as a parameter +func genesisCommand(txConfig client.TxConfig, basicManager module.BasicManager, cmds ...*cobra.Command) *cobra.Command { + cmd := genutilcli.Commands(txConfig, basicManager, simapp.DefaultNodeHome) + + for _, subCmd := range cmds { + cmd.AddCommand(subCmd) + } + return cmd +} + +func queryCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "query", + Aliases: []string{"q"}, + Short: "Querying subcommands", + DisableFlagParsing: false, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand( + rpc.ValidatorCommand(), + server.QueryBlockCmd(), + authcmd.QueryTxsByEventsCmd(), + server.QueryBlocksCmd(), + authcmd.QueryTxCmd(), + ) + + return cmd +} + +func txCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "tx", + Short: "Transactions subcommands", + DisableFlagParsing: false, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand( + authcmd.GetSignCommand(), + authcmd.GetSignBatchCommand(), + authcmd.GetMultiSignCommand(), + authcmd.GetMultiSignBatchCmd(), + authcmd.GetValidateSignaturesCommand(), + authcmd.GetBroadcastCommand(), + authcmd.GetEncodeCommand(), + authcmd.GetDecodeCommand(), + authcmd.GetAuxToFeeCommand(), + ) + + return cmd +} + +// newApp creates the application +func newApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + appOpts servertypes.AppOptions, +) servertypes.Application { + baseappOptions := server.DefaultBaseappOptions(appOpts) + + return simapp.NewSimApp( + logger, db, traceStore, true, + appOpts, + baseappOptions..., + ) +} + +// appExport creates a new simapp (optionally at a given height) and exports state. +func appExport( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + height int64, + forZeroHeight bool, + jailAllowedAddrs []string, + appOpts servertypes.AppOptions, + modulesToExport []string, +) (servertypes.ExportedApp, error) { + // this check is necessary as we use the flag in x/upgrade. + // we can exit more gracefully by checking the flag here. + homePath, ok := appOpts.Get(flags.FlagHome).(string) + if !ok || homePath == "" { + return servertypes.ExportedApp{}, errors.New("application home not set") + } + + viperAppOpts, ok := appOpts.(*viper.Viper) + if !ok { + return servertypes.ExportedApp{}, errors.New("appOpts is not viper.Viper") + } + + // overwrite the FlagInvCheckPeriod + viperAppOpts.Set(server.FlagInvCheckPeriod, 1) + appOpts = viperAppOpts + + var simApp *simapp.SimApp + if height != -1 { + simApp = simapp.NewSimApp(logger, db, traceStore, false, appOpts) + + if err := simApp.LoadHeight(height); err != nil { + return servertypes.ExportedApp{}, err + } + } else { + simApp = simapp.NewSimApp(logger, db, traceStore, true, appOpts) + } + + return simApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) +} diff --git a/snapshots/manager.go b/snapshots/manager.go index d3b08e1bc545..d118ab41aea7 100644 --- a/snapshots/manager.go +++ b/snapshots/manager.go @@ -417,6 +417,25 @@ func (m *Manager) RestoreChunk(chunk []byte) (bool, error) { return false, nil } +// RestoreLocalSnapshot restores app state from a local snapshot. +func (m *Manager) RestoreLocalSnapshot(height uint64, format uint32) error { + snapshot, ch, err := m.store.Load(height, format) + if err != nil { + return err + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + err = m.beginLocked(opRestore) + if err != nil { + return err + } + defer m.endLocked() + + return m.restoreSnapshot(*snapshot, ch) +} + // sortedExtensionNames sort extension names for deterministic iteration. func (m *Manager) sortedExtensionNames() []string { names := make([]string, 0, len(m.extensions)) diff --git a/snapshots/store.go b/snapshots/store.go index 8e09e140ec66..63d59d69fa19 100644 --- a/snapshots/store.go +++ b/snapshots/store.go @@ -164,8 +164,13 @@ func (s *Store) Load(height uint64, format uint32) (*types.Snapshot, <-chan io.R // LoadChunk loads a chunk from disk, or returns nil if it does not exist. The caller must call // Close() on it when done. +<<<<<<< HEAD:snapshots/store.go func (s *Store) LoadChunk(height uint64, format uint32, chunk uint32) (io.ReadCloser, error) { path := s.pathChunk(height, format, chunk) +======= +func (s *Store) LoadChunk(height uint64, format, chunk uint32) (io.ReadCloser, error) { + path := s.PathChunk(height, format, chunk) +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)):store/snapshots/store.go file, err := os.Open(path) if os.IsNotExist(err) { return nil, nil @@ -174,8 +179,13 @@ func (s *Store) LoadChunk(height uint64, format uint32, chunk uint32) (io.ReadCl } // loadChunkFile loads a chunk from disk, and errors if it does not exist. +<<<<<<< HEAD:snapshots/store.go func (s *Store) loadChunkFile(height uint64, format uint32, chunk uint32) (io.ReadCloser, error) { path := s.pathChunk(height, format, chunk) +======= +func (s *Store) loadChunkFile(height uint64, format, chunk uint32) (io.ReadCloser, error) { + path := s.PathChunk(height, format, chunk) +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)):store/snapshots/store.go return os.Open(path) } @@ -293,6 +303,39 @@ func (s *Store) Save( return snapshot, s.saveSnapshot(snapshot) } +<<<<<<< HEAD:snapshots/store.go +======= +// saveChunk saves the given chunkBody with the given index to its appropriate path on disk. +// The hash of the chunk is appended to the snapshot's metadata, +// and the overall snapshot hash is updated with the chunk content too. +func (s *Store) saveChunk(chunkBody io.ReadCloser, index uint32, snapshot *types.Snapshot, chunkHasher, snapshotHasher hash.Hash) error { + defer chunkBody.Close() + + path := s.PathChunk(snapshot.Height, snapshot.Format, index) + chunkFile, err := os.Create(path) + if err != nil { + return errors.Wrapf(err, "failed to create snapshot chunk file %q", path) + } + defer chunkFile.Close() + + chunkHasher.Reset() + if _, err := io.Copy(io.MultiWriter(chunkFile, chunkHasher, snapshotHasher), chunkBody); err != nil { + return errors.Wrapf(err, "failed to generate snapshot chunk %d", index) + } + + if err := chunkFile.Close(); err != nil { + return errors.Wrapf(err, "failed to close snapshot chunk file %d", index) + } + + if err := chunkBody.Close(); err != nil { + return errors.Wrapf(err, "failed to close snapshot chunk body %d", index) + } + + snapshot.Metadata.ChunkHashes = append(snapshot.Metadata.ChunkHashes, chunkHasher.Sum(nil)) + return nil +} + +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)):store/snapshots/store.go // saveSnapshot saves snapshot metadata to the database. func (s *Store) saveSnapshot(snapshot *types.Snapshot) error { value, err := proto.Marshal(snapshot) @@ -313,8 +356,13 @@ func (s *Store) pathSnapshot(height uint64, format uint32) string { return filepath.Join(s.pathHeight(height), strconv.FormatUint(uint64(format), 10)) } +<<<<<<< HEAD:snapshots/store.go // pathChunk generates a snapshot chunk path. func (s *Store) pathChunk(height uint64, format uint32, chunk uint32) string { +======= +// PathChunk generates a snapshot chunk path. +func (s *Store) PathChunk(height uint64, format, chunk uint32) string { +>>>>>>> c1ceb3bdd (feat: add local snapshots management commands (#16067)):store/snapshots/store.go return filepath.Join(s.pathSnapshot(height, format), strconv.FormatUint(uint64(chunk), 10)) }