From 228c92d240882a86461491c8c47a0654831bba34 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Fri, 2 Jun 2023 14:02:08 +0200 Subject: [PATCH] chore: small snapshot commands & docs improvement (#16404) (cherry picked from commit b590b091076078c7e5c740e9c9f3f2f98f7e85e7) # Conflicts: # client/snapshot/export.go # server/cmt_cmds.go --- client/snapshot/cmd.go | 1 - client/snapshot/export.go | 8 +- client/snapshot/list.go | 2 +- client/snapshot/load.go | 4 +- docs/docs/run-node/01-run-node.md | 35 ++- go.mod | 2 +- server/cmt_cmds.go | 350 ++++++++++++++++++++++++++++++ simapp/go.mod | 2 +- snapshots/README.md | 23 +- 9 files changed, 404 insertions(+), 23 deletions(-) create mode 100644 server/cmt_cmds.go diff --git a/client/snapshot/cmd.go b/client/snapshot/cmd.go index f49f2b51c2b4..14388bc8d05e 100644 --- a/client/snapshot/cmd.go +++ b/client/snapshot/cmd.go @@ -10,7 +10,6 @@ func Cmd(appCreator servertypes.AppCreator) *cobra.Command { cmd := &cobra.Command{ Use: "snapshots", Short: "Manage local snapshots", - Long: "Manage local snapshots", } cmd.AddCommand( ListSnapshotsCmd, diff --git a/client/snapshot/export.go b/client/snapshot/export.go index 6465b7f1e2d6..f81626120d81 100644 --- a/client/snapshot/export.go +++ b/client/snapshot/export.go @@ -1,8 +1,12 @@ package snapshot import ( +<<<<<<< HEAD "fmt" +======= + "cosmossdk.io/log" +>>>>>>> b590b0910 (chore: small snapshot commands & docs improvement (#16404)) "github.com/cosmos/cosmos-sdk/server" servertypes "github.com/cosmos/cosmos-sdk/server/types" "github.com/spf13/cobra" @@ -33,7 +37,7 @@ func ExportSnapshotCmd(appCreator servertypes.AppCreator) *cobra.Command { height = app.CommitMultiStore().LastCommitID().Version } - fmt.Printf("Exporting snapshot for height %d\n", height) + cmd.Printf("Exporting snapshot for height %d\n", height) sm := app.SnapshotManager() snapshot, err := sm.Create(uint64(height)) @@ -41,7 +45,7 @@ func ExportSnapshotCmd(appCreator servertypes.AppCreator) *cobra.Command { return err } - fmt.Printf("Snapshot created at height %d, format %d, chunks %d\n", snapshot.Height, snapshot.Format, snapshot.Chunks) + cmd.Printf("Snapshot created at height %d, format %d, chunks %d\n", snapshot.Height, snapshot.Format, snapshot.Chunks) return nil }, } diff --git a/client/snapshot/list.go b/client/snapshot/list.go index 6ff6391d4211..78612bf916ee 100644 --- a/client/snapshot/list.go +++ b/client/snapshot/list.go @@ -22,7 +22,7 @@ var ListSnapshotsCmd = &cobra.Command{ return fmt.Errorf("failed to list snapshots: %w", err) } for _, snapshot := range snapshots { - fmt.Println("height:", snapshot.Height, "format:", snapshot.Format, "chunks:", snapshot.Chunks) + cmd.Println("height:", snapshot.Height, "format:", snapshot.Format, "chunks:", snapshot.Chunks) } return nil diff --git a/client/snapshot/load.go b/client/snapshot/load.go index 6797d58bafec..7834aa21e2e7 100644 --- a/client/snapshot/load.go +++ b/client/snapshot/load.go @@ -22,7 +22,7 @@ const SnapshotFileName = "_snapshot" func LoadArchiveCmd() *cobra.Command { return &cobra.Command{ Use: "load ", - Short: "Load a snapshot archive file into snapshot store", + Short: "Load a snapshot archive file (.tar.gz) into snapshot store", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := server.GetServerContextFromCmd(cmd) @@ -70,7 +70,7 @@ func LoadArchiveCmd() *cobra.Command { savedSnapshot, err := snapshotStore.Save(snapshot.Height, snapshot.Format, chunks) if err != nil { - fmt.Println("failed to save snapshot", err) + cmd.Println("failed to save snapshot", err) return } quitChan <- savedSnapshot diff --git a/docs/docs/run-node/01-run-node.md b/docs/docs/run-node/01-run-node.md index 8369920707be..a040380272da 100644 --- a/docs/docs/run-node/01-run-node.md +++ b/docs/docs/run-node/01-run-node.md @@ -152,12 +152,41 @@ log_level: "state:info,p2p:info,consensus:info,x/staking:info,x/ibc:info,*error" ## State Sync -State sync is the act in which a node syncs the latest or close to the latest state of a blockchain. This is useful for users who don't want to sync all the blocks in history. You can read more here: https://docs.cometbft.com/v0.37/core/state-sync +State sync is the act in which a node syncs the latest or close to the latest state of a blockchain. This is useful for users who don't want to sync all the blocks in history. Read more in [CometBFT documentation](https://docs.cometbft.com/v0.37/core/state-sync). + +State sync works thanks to snapshots. Read how the SDK handles snapshots [here](https://github.com/cosmos/cosmos-sdk/blob/825245d/store/snapshots/README.md). ### Local State Sync Local state sync work similar to normal state sync except that it works off a local snapshot of state instead of one provided via the p2p network. The steps to start local state sync are similar to normal state sync with a few different designs. 1. As mentioned in https://docs.cometbft.com/v0.37/core/state-sync, one must set a height and hash in the config.toml along with a few rpc servers (the afromentioned link has instructions on how to do this). -2. Bootsrapping Comet state in order to start the node after the snapshot has been ingested. This can be done with the bootstrap command ` comet bootstrap-state` - +2. Run ` ` to restore a local snapshot (note: first load it from a file with the *load* command). +3. Bootsrapping Comet state in order to start the node after the snapshot has been ingested. This can be done with the bootstrap command ` comet bootstrap-state` + +### Snapshots Commands + +The Cosmos SDK provides commands for managing snapshots. +These commands can be added in an app with the following snippet in `cmd//root.go`: + +```go +import ( + "github.com/cosmos/cosmos-sdk/client/snapshot" +) + +func initRootCmd(/* ... */) { + // ... + rootCmd.AddCommand( + snapshot.Cmd(appCreator), + ) +} +``` + +Then following commands are available at ` snapshots [command]`: + +* **list**: list local snapshots +* **load**: Load a snapshot archive file into snapshot store +* **restore**: Restore app state from local snapshot +* **export**: Export app state to snapshot store +* **dump**: Dump the snapshot as portable archive format +* **delete**: Delete a local snapshot diff --git a/go.mod b/go.mod index 3fd531b8d423..60939e4457e7 100644 --- a/go.mod +++ b/go.mod @@ -177,7 +177,7 @@ replace ( // Fix upstream GHSA-h395-qcrw-5vmq and GHSA-3vp4-m3rf-835h vulnerabilities. // TODO Remove it: https://github.com/cosmos/cosmos-sdk/issues/10409 github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.0 - // Downgraded to avoid bugs in following commits which caused simulations to fail. + // replace broken goleveldb github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 ) diff --git a/server/cmt_cmds.go b/server/cmt_cmds.go new file mode 100644 index 000000000000..02b14a7b4aa4 --- /dev/null +++ b/server/cmt_cmds.go @@ -0,0 +1,350 @@ +package server + +import ( + "fmt" + "strconv" + "strings" + + "cosmossdk.io/log" + cmtcfg "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/light" + "github.com/cometbft/cometbft/node" + "github.com/cometbft/cometbft/p2p" + pvm "github.com/cometbft/cometbft/privval" + cmtstore "github.com/cometbft/cometbft/proto/tendermint/store" + sm "github.com/cometbft/cometbft/state" + "github.com/cometbft/cometbft/statesync" + "github.com/cometbft/cometbft/store" + cmtversion "github.com/cometbft/cometbft/version" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + rpc "github.com/cosmos/cosmos-sdk/client/rpc" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + servercmtlog "github.com/cosmos/cosmos-sdk/server/log" + "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/version" + auth "github.com/cosmos/cosmos-sdk/x/auth/client/cli" +) + +// ShowNodeIDCmd - ported from CometBFT, dump node ID to stdout +func ShowNodeIDCmd() *cobra.Command { + return &cobra.Command{ + Use: "show-node-id", + Short: "Show this node's ID", + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := GetServerContextFromCmd(cmd) + cfg := serverCtx.Config + + nodeKey, err := p2p.LoadNodeKey(cfg.NodeKeyFile()) + if err != nil { + return err + } + + fmt.Println(nodeKey.ID()) + return nil + }, + } +} + +// ShowValidatorCmd - ported from CometBFT, show this node's validator info +func ShowValidatorCmd() *cobra.Command { + cmd := cobra.Command{ + Use: "show-validator", + Short: "Show this node's CometBFT validator info", + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := GetServerContextFromCmd(cmd) + cfg := serverCtx.Config + + privValidator := pvm.LoadFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()) + pk, err := privValidator.GetPubKey() + if err != nil { + return err + } + + sdkPK, err := cryptocodec.FromCmtPubKeyInterface(pk) + if err != nil { + return err + } + + clientCtx := client.GetClientContextFromCmd(cmd) + bz, err := clientCtx.Codec.MarshalInterfaceJSON(sdkPK) + if err != nil { + return err + } + + fmt.Println(string(bz)) + return nil + }, + } + + return &cmd +} + +// ShowAddressCmd - show this node's validator address +func ShowAddressCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "show-address", + Short: "Shows this node's CometBFT validator consensus address", + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := GetServerContextFromCmd(cmd) + cfg := serverCtx.Config + + privValidator := pvm.LoadFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()) + + valConsAddr := (sdk.ConsAddress)(privValidator.GetAddress()) + fmt.Println(valConsAddr.String()) + return nil + }, + } + + return cmd +} + +// VersionCmd prints CometBFT and ABCI version numbers. +func VersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print CometBFT libraries' version", + Long: "Print protocols' and libraries' version numbers against which this app has been compiled.", + RunE: func(cmd *cobra.Command, args []string) error { + bs, err := yaml.Marshal(&struct { + CometBFT string + ABCI string + BlockProtocol uint64 + P2PProtocol uint64 + }{ + CometBFT: cmtversion.TMCoreSemVer, + ABCI: cmtversion.ABCIVersion, + BlockProtocol: cmtversion.BlockProtocol, + P2PProtocol: cmtversion.P2PProtocol, + }) + if err != nil { + return err + } + + cmd.Print(string(bs)) + return nil + }, + } +} + +// QueryBlocksCmd returns a command to search through blocks by events. +func QueryBlocksCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "blocks", + Short: "Query for paginated blocks that match a set of events", + Long: `Search for blocks that match the exact given events where results are paginated. +The events query is directly passed to CometBFT's RPC BlockSearch method and must +conform to CometBFT's query syntax. +Please refer to each module's documentation for the full set of events to query +for. Each module documents its respective events under 'xx_events.md'. +`, + Example: fmt.Sprintf( + "$ %s query blocks --query \"message.sender='cosmos1...' AND block.height > 7\" --page 1 --limit 30 --order-by ASC", + version.AppName, + ), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + query, _ := cmd.Flags().GetString(auth.FlagQuery) + page, _ := cmd.Flags().GetInt(flags.FlagPage) + limit, _ := cmd.Flags().GetInt(flags.FlagLimit) + orderBy, _ := cmd.Flags().GetString(auth.FlagOrderBy) + + blocks, err := rpc.QueryBlocks(clientCtx, page, limit, query, orderBy) + if err != nil { + return err + } + + return clientCtx.PrintProto(blocks) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + cmd.Flags().Int(flags.FlagPage, query.DefaultPage, "Query a specific page of paginated results") + cmd.Flags().Int(flags.FlagLimit, query.DefaultLimit, "Query number of transactions results per page returned") + cmd.Flags().String(auth.FlagQuery, "", "The blocks events query per CometBFT's query semantics") + cmd.Flags().String(auth.FlagOrderBy, "", "The ordering semantics (asc|dsc)") + _ = cmd.MarkFlagRequired(auth.FlagQuery) + + return cmd +} + +// QueryBlockCmd implements the default command for a Block query. +func QueryBlockCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "block --type=[height|hash] [height|hash]", + Short: "Query for a committed block by height, hash, or event(s)", + Long: "Query for a specific committed block using the CometBFT RPC `block` and `block_by_hash` method", + Example: strings.TrimSpace(fmt.Sprintf(` +$ %s query block --%s=%s +$ %s query block --%s=%s +`, + version.AppName, auth.FlagType, auth.TypeHeight, + version.AppName, auth.FlagType, auth.TypeHash)), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + typ, _ := cmd.Flags().GetString(auth.FlagType) + + switch typ { + case auth.TypeHeight: + + if args[0] == "" { + return fmt.Errorf("argument should be a block height") + } + + var height *int64 + + // optional height + if len(args) > 0 { + h, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + if h > 0 { + tmp := int64(h) + height = &tmp + } + } + + output, err := rpc.GetBlockByHeight(clientCtx, height) + if err != nil { + return err + } + + if output.Header.Height == 0 { + return fmt.Errorf("no block found with height %s", args[0]) + } + + return clientCtx.PrintProto(output) + + case auth.TypeHash: + + if args[0] == "" { + return fmt.Errorf("argument should be a tx hash") + } + + // If hash is given, then query the tx by hash. + output, err := rpc.GetBlockByHash(clientCtx, args[0]) + if err != nil { + return err + } + + if output.Header.AppHash == nil { + return fmt.Errorf("no block found with hash %s", args[0]) + } + + return clientCtx.PrintProto(output) + + default: + return fmt.Errorf("unknown --%s value %s", auth.FlagType, typ) + } + }, + } + + flags.AddQueryFlagsToCmd(cmd) + cmd.Flags().String(auth.FlagType, auth.TypeHash, fmt.Sprintf("The type to be used when querying tx, can be one of \"%s\", \"%s\"", auth.TypeHeight, auth.TypeHash)) + + return cmd +} + +func BootstrapStateCmd(appCreator types.AppCreator) *cobra.Command { + cmd := &cobra.Command{ + Use: "bootstrap-state", + Short: "Bootstrap CometBFT state at an arbitrary block height using a light client", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + serverCtx := GetServerContextFromCmd(cmd) + logger := log.NewLogger(cmd.OutOrStdout()) + cfg := serverCtx.Config + + height, err := cmd.Flags().GetInt64("height") + if err != nil { + return err + } + if height == 0 { + home := serverCtx.Viper.GetString(flags.FlagHome) + db, err := openDB(home, GetAppDBBackend(serverCtx.Viper)) + if err != nil { + return err + } + + app := appCreator(logger, db, nil, serverCtx.Viper) + height = app.CommitMultiStore().LastCommitID().Version + } + + blockStoreDB, err := cmtcfg.DefaultDBProvider(&cmtcfg.DBContext{ID: "blockstore", Config: cfg}) + if err != nil { + return err + } + blockStore := store.NewBlockStore(blockStoreDB) + + stateDB, err := cmtcfg.DefaultDBProvider(&cmtcfg.DBContext{ID: "state", Config: cfg}) + if err != nil { + return err + } + stateStore := sm.NewStore(stateDB, sm.StoreOptions{ + DiscardABCIResponses: cfg.Storage.DiscardABCIResponses, + }) + + genState, _, err := node.LoadStateFromDBOrGenesisDocProvider(stateDB, node.DefaultGenesisDocProviderFunc(cfg)) + if err != nil { + return err + } + + stateProvider, err := statesync.NewLightClientStateProvider( + cmd.Context(), + genState.ChainID, genState.Version, genState.InitialHeight, + cfg.StateSync.RPCServers, light.TrustOptions{ + Period: cfg.StateSync.TrustPeriod, + Height: cfg.StateSync.TrustHeight, + Hash: cfg.StateSync.TrustHashBytes(), + }, servercmtlog.CometLoggerWrapper{Logger: logger.With("module", "light")}) + if err != nil { + return fmt.Errorf("failed to set up light client state provider: %w", err) + } + + state, err := stateProvider.State(cmd.Context(), uint64(height)) + if err != nil { + return fmt.Errorf("failed to get state: %w", err) + } + + commit, err := stateProvider.Commit(cmd.Context(), uint64(height)) + if err != nil { + return fmt.Errorf("failed to get commit: %w", err) + } + + if err := stateStore.Bootstrap(state); err != nil { + return fmt.Errorf("failed to bootstrap state: %w", err) + } + + if err := blockStore.SaveSeenCommit(state.LastBlockHeight, commit); err != nil { + return fmt.Errorf("failed to save seen commit: %w", err) + } + + store.SaveBlockStoreState(&cmtstore.BlockStoreState{ + // it breaks the invariant that blocks in range [Base, Height] must exists, but it do works in practice. + Base: state.LastBlockHeight, + Height: state.LastBlockHeight, + }, blockStoreDB) + + return nil + }, + } + + cmd.Flags().Int64("height", 0, "Block height to bootstrap state at, if not provided it uses the latest block height in app state") + + return cmd +} diff --git a/simapp/go.mod b/simapp/go.mod index c6b260784057..7a2315dd6823 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -170,6 +170,6 @@ replace ( // Fix upstream GHSA-h395-qcrw-5vmq and GHSA-3vp4-m3rf-835h vulnerabilities. // TODO Remove it: https://github.com/cosmos/cosmos-sdk/issues/10409 github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.0 - // Downgraded to avoid bugs in following commits which caused simulations to fail. + // replace broken goleveldb github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 ) diff --git a/snapshots/README.md b/snapshots/README.md index c4d011f1de26..6de723246885 100644 --- a/snapshots/README.md +++ b/snapshots/README.md @@ -55,16 +55,16 @@ Snapshot settings are optional. However, if set, they have an effect on how prun persisting the heights that are multiples of `state-sync.snapshot-interval` until after the snapshot is complete. If pruning is enabled (not `pruning = "nothing"`), we avoid pruning heights that are multiples of -`state-sync.snapshot-interval` in the regular logic determined by the -pruning settings and applied after every `Commit()`. This is done to prevent a -height from being removed before a snapshot is complete. Therefore, we keep -such heights until after a snapshot is done. At this point, the height is sent to +`state-sync.snapshot-interval` in the regular logic determined by the +pruning settings and applied after every `Commit()`. This is done to prevent a +height from being removed before a snapshot is complete. Therefore, we keep +such heights until after a snapshot is done. At this point, the height is sent to the `pruning.Manager` to be pruned according to the pruning settings after the next `Commit()`. To illustrate, assume that we are currently at height 960 with `pruning-keep-recent = 50`, `pruning-interval = 10`, and `state-sync.snapshot-interval = 100`. Let's assume that the snapshot that was triggered at height `900` **just finishes**. Then, we can prune height -`900` right away (that is, when we call `Commit()` at height 960 because 900 is less than `960 - 50 = 910`. +`900` right away (that is, when we call `Commit()` at height 960 because 900 is less than `960 - 50 = 910`). Let's now assume that all conditions stay the same but the snapshot at height 900 is **not complete yet**. Then, we cannot prune it to avoid deleting a height that is still being snapshotted. Therefore, we keep track @@ -78,23 +78,22 @@ Note that in both examples, if we let current height = C, and previous height P P - `pruning-keep-recent` - `pruning-interval` <= h <= P - `pruning-keep-recent` -we can prune height h. In our first example, all heights 899 - 909 fall in this range and are pruned at height 960 as long as +we can prune height h. In our first example, all heights 899 - 909 fall in this range and are pruned at height 960 as long as h is not a snapshot height (E.g. 900). That is, we always use current height to determine at which height to prune (960) while we use previous to determine which heights are to be pruned (959 - 50 - 10 = 899-909 = 959 - 50). - ## Configuration * `state-sync.snapshot-interval` - * the interval at which to take snapshots. - * the value of 0 disables snapshots. - * if pruning is enabled, it is done after a snapshot is complete for the heights that are multiples of this interval. + * the interval at which to take snapshots. + * the value of 0 disables snapshots. + * if pruning is enabled, it is done after a snapshot is complete for the heights that are multiples of this interval. * `state-sync.snapshot-keep-recent`: - * the number of recent snapshots to keep. - * 0 means keep all. + * the number of recent snapshots to keep. + * 0 means keep all. ## Snapshot Metadata