github.com/cosmos/cosmos-sdk@v0.50.10/server/util.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"os"
    10  	"os/signal"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  	"time"
    16  
    17  	cmtcmd "github.com/cometbft/cometbft/cmd/cometbft/commands"
    18  	cmtcfg "github.com/cometbft/cometbft/config"
    19  	dbm "github.com/cosmos/cosmos-db"
    20  	"github.com/rs/zerolog"
    21  	"github.com/spf13/cast"
    22  	"github.com/spf13/cobra"
    23  	"github.com/spf13/pflag"
    24  	"github.com/spf13/viper"
    25  	"golang.org/x/sync/errgroup"
    26  
    27  	"cosmossdk.io/log"
    28  	"cosmossdk.io/store"
    29  	"cosmossdk.io/store/snapshots"
    30  	snapshottypes "cosmossdk.io/store/snapshots/types"
    31  	storetypes "cosmossdk.io/store/types"
    32  
    33  	"github.com/cosmos/cosmos-sdk/baseapp"
    34  	"github.com/cosmos/cosmos-sdk/client/flags"
    35  	"github.com/cosmos/cosmos-sdk/server/config"
    36  	"github.com/cosmos/cosmos-sdk/server/types"
    37  	sdk "github.com/cosmos/cosmos-sdk/types"
    38  	"github.com/cosmos/cosmos-sdk/types/mempool"
    39  	"github.com/cosmos/cosmos-sdk/version"
    40  	genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
    41  )
    42  
    43  // ServerContextKey defines the context key used to retrieve a server.Context from
    44  // a command's Context.
    45  const ServerContextKey = sdk.ContextKey("server.context")
    46  
    47  // server context
    48  type Context struct {
    49  	Viper  *viper.Viper
    50  	Config *cmtcfg.Config
    51  	Logger log.Logger
    52  }
    53  
    54  func NewDefaultContext() *Context {
    55  	return NewContext(
    56  		viper.New(),
    57  		cmtcfg.DefaultConfig(),
    58  		log.NewLogger(os.Stdout),
    59  	)
    60  }
    61  
    62  func NewContext(v *viper.Viper, config *cmtcfg.Config, logger log.Logger) *Context {
    63  	return &Context{v, config, logger}
    64  }
    65  
    66  func bindFlags(basename string, cmd *cobra.Command, v *viper.Viper) (err error) {
    67  	defer func() {
    68  		if r := recover(); r != nil {
    69  			err = fmt.Errorf("bindFlags failed: %v", r)
    70  		}
    71  	}()
    72  
    73  	cmd.Flags().VisitAll(func(f *pflag.Flag) {
    74  		// Environment variables can't have dashes in them, so bind them to their equivalent
    75  		// keys with underscores, e.g. --favorite-color to STING_FAVORITE_COLOR
    76  		err = v.BindEnv(f.Name, fmt.Sprintf("%s_%s", basename, strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))))
    77  		if err != nil {
    78  			panic(err)
    79  		}
    80  
    81  		err = v.BindPFlag(f.Name, f)
    82  		if err != nil {
    83  			panic(err)
    84  		}
    85  
    86  		// Apply the viper config value to the flag when the flag is not set and
    87  		// viper has a value.
    88  		if !f.Changed && v.IsSet(f.Name) {
    89  			val := v.Get(f.Name)
    90  			err = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
    91  			if err != nil {
    92  				panic(err)
    93  			}
    94  		}
    95  	})
    96  
    97  	return err
    98  }
    99  
   100  // InterceptConfigsPreRunHandler is identical to InterceptConfigsAndCreateContext
   101  // except it also sets the server context on the command and the server logger.
   102  func InterceptConfigsPreRunHandler(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig interface{}, cmtConfig *cmtcfg.Config) error {
   103  	serverCtx, err := InterceptConfigsAndCreateContext(cmd, customAppConfigTemplate, customAppConfig, cmtConfig)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	// overwrite default server logger
   109  	logger, err := CreateSDKLogger(serverCtx, cmd.OutOrStdout())
   110  	if err != nil {
   111  		return err
   112  	}
   113  	serverCtx.Logger = logger.With(log.ModuleKey, "server")
   114  
   115  	// set server context
   116  	return SetCmdServerContext(cmd, serverCtx)
   117  }
   118  
   119  // InterceptConfigsAndCreateContext performs a pre-run function for the root daemon
   120  // application command. It will create a Viper literal and a default server
   121  // Context. The server CometBFT configuration will either be read and parsed
   122  // or created and saved to disk, where the server Context is updated to reflect
   123  // the CometBFT configuration. It takes custom app config template and config
   124  // settings to create a custom CometBFT configuration. If the custom template
   125  // is empty, it uses default-template provided by the server. The Viper literal
   126  // is used to read and parse the application configuration. Command handlers can
   127  // fetch the server Context to get the CometBFT configuration or to get access
   128  // to Viper.
   129  func InterceptConfigsAndCreateContext(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig interface{}, cmtConfig *cmtcfg.Config) (*Context, error) {
   130  	serverCtx := NewDefaultContext()
   131  
   132  	// Get the executable name and configure the viper instance so that environmental
   133  	// variables are checked based off that name. The underscore character is used
   134  	// as a separator.
   135  	executableName, err := os.Executable()
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	basename := path.Base(executableName)
   141  
   142  	// configure the viper instance
   143  	if err := serverCtx.Viper.BindPFlags(cmd.Flags()); err != nil {
   144  		return nil, err
   145  	}
   146  	if err := serverCtx.Viper.BindPFlags(cmd.PersistentFlags()); err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	serverCtx.Viper.SetEnvPrefix(basename)
   151  	serverCtx.Viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
   152  	serverCtx.Viper.AutomaticEnv()
   153  
   154  	// intercept configuration files, using both Viper instances separately
   155  	config, err := interceptConfigs(serverCtx.Viper, customAppConfigTemplate, customAppConfig, cmtConfig)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	// return value is a CometBFT configuration object
   161  	serverCtx.Config = config
   162  	if err = bindFlags(basename, cmd, serverCtx.Viper); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	return serverCtx, nil
   167  }
   168  
   169  // CreateSDKLogger creates a the default SDK logger.
   170  // It reads the log level and format from the server context.
   171  func CreateSDKLogger(ctx *Context, out io.Writer) (log.Logger, error) {
   172  	var opts []log.Option
   173  	if ctx.Viper.GetString(flags.FlagLogFormat) == flags.OutputFormatJSON {
   174  		opts = append(opts, log.OutputJSONOption())
   175  	}
   176  	opts = append(opts,
   177  		log.ColorOption(!ctx.Viper.GetBool(flags.FlagLogNoColor)),
   178  		// We use CometBFT flag (cmtcli.TraceFlag) for trace logging.
   179  		log.TraceOption(ctx.Viper.GetBool(FlagTrace)))
   180  
   181  	// check and set filter level or keys for the logger if any
   182  	logLvlStr := ctx.Viper.GetString(flags.FlagLogLevel)
   183  	if logLvlStr == "" {
   184  		return log.NewLogger(out, opts...), nil
   185  	}
   186  
   187  	logLvl, err := zerolog.ParseLevel(logLvlStr)
   188  	switch {
   189  	case err != nil:
   190  		// If the log level is not a valid zerolog level, then we try to parse it as a key filter.
   191  		filterFunc, err := log.ParseLogLevel(logLvlStr)
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  
   196  		opts = append(opts, log.FilterOption(filterFunc))
   197  	default:
   198  		opts = append(opts, log.LevelOption(logLvl))
   199  	}
   200  
   201  	return log.NewLogger(out, opts...), nil
   202  }
   203  
   204  // GetServerContextFromCmd returns a Context from a command or an empty Context
   205  // if it has not been set.
   206  func GetServerContextFromCmd(cmd *cobra.Command) *Context {
   207  	if v := cmd.Context().Value(ServerContextKey); v != nil {
   208  		serverCtxPtr := v.(*Context)
   209  		return serverCtxPtr
   210  	}
   211  
   212  	return NewDefaultContext()
   213  }
   214  
   215  // SetCmdServerContext sets a command's Context value to the provided argument.
   216  // If the context has not been set, set the given context as the default.
   217  func SetCmdServerContext(cmd *cobra.Command, serverCtx *Context) error {
   218  	v := cmd.Context().Value(ServerContextKey)
   219  	if v == nil {
   220  		v = serverCtx
   221  	}
   222  
   223  	serverCtxPtr := v.(*Context)
   224  	*serverCtxPtr = *serverCtx
   225  
   226  	return nil
   227  }
   228  
   229  // interceptConfigs parses and updates a CometBFT configuration file or
   230  // creates a new one and saves it. It also parses and saves the application
   231  // configuration file. The CometBFT configuration file is parsed given a root
   232  // Viper object, whereas the application is parsed with the private package-aware
   233  // viperCfg object.
   234  func interceptConfigs(rootViper *viper.Viper, customAppTemplate string, customConfig interface{}, cmtConfig *cmtcfg.Config) (*cmtcfg.Config, error) {
   235  	rootDir := rootViper.GetString(flags.FlagHome)
   236  	configPath := filepath.Join(rootDir, "config")
   237  	cmtCfgFile := filepath.Join(configPath, "config.toml")
   238  
   239  	conf := cmtConfig
   240  
   241  	switch _, err := os.Stat(cmtCfgFile); {
   242  	case os.IsNotExist(err):
   243  		cmtcfg.EnsureRoot(rootDir)
   244  
   245  		if err = conf.ValidateBasic(); err != nil {
   246  			return nil, fmt.Errorf("error in config file: %w", err)
   247  		}
   248  
   249  		defaultCometCfg := cmtcfg.DefaultConfig()
   250  		// The SDK is opinionated about those comet values, so we set them here.
   251  		// We verify first that the user has not changed them for not overriding them.
   252  		if conf.Consensus.TimeoutCommit == defaultCometCfg.Consensus.TimeoutCommit {
   253  			conf.Consensus.TimeoutCommit = 5 * time.Second
   254  		}
   255  		if conf.RPC.PprofListenAddress == defaultCometCfg.RPC.PprofListenAddress {
   256  			conf.RPC.PprofListenAddress = "localhost:6060"
   257  		}
   258  
   259  		cmtcfg.WriteConfigFile(cmtCfgFile, conf)
   260  
   261  	case err != nil:
   262  		return nil, err
   263  
   264  	default:
   265  		rootViper.SetConfigType("toml")
   266  		rootViper.SetConfigName("config")
   267  		rootViper.AddConfigPath(configPath)
   268  
   269  		if err := rootViper.ReadInConfig(); err != nil {
   270  			return nil, fmt.Errorf("failed to read in %s: %w", cmtCfgFile, err)
   271  		}
   272  	}
   273  
   274  	// Read into the configuration whatever data the viper instance has for it.
   275  	// This may come from the configuration file above but also any of the other
   276  	// sources viper uses.
   277  	if err := rootViper.Unmarshal(conf); err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	conf.SetRoot(rootDir)
   282  
   283  	appCfgFilePath := filepath.Join(configPath, "app.toml")
   284  	if _, err := os.Stat(appCfgFilePath); os.IsNotExist(err) {
   285  		if customAppTemplate != "" {
   286  			config.SetConfigTemplate(customAppTemplate)
   287  
   288  			if err = rootViper.Unmarshal(&customConfig); err != nil {
   289  				return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err)
   290  			}
   291  
   292  			config.WriteConfigFile(appCfgFilePath, customConfig)
   293  		} else {
   294  			appConf, err := config.ParseConfig(rootViper)
   295  			if err != nil {
   296  				return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err)
   297  			}
   298  
   299  			config.WriteConfigFile(appCfgFilePath, appConf)
   300  		}
   301  	}
   302  
   303  	rootViper.SetConfigType("toml")
   304  	rootViper.SetConfigName("app")
   305  	rootViper.AddConfigPath(configPath)
   306  
   307  	if err := rootViper.MergeInConfig(); err != nil {
   308  		return nil, fmt.Errorf("failed to merge configuration: %w", err)
   309  	}
   310  
   311  	return conf, nil
   312  }
   313  
   314  // add server commands
   315  func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, addStartFlags types.ModuleInitFlags) {
   316  	cometCmd := &cobra.Command{
   317  		Use:     "comet",
   318  		Aliases: []string{"cometbft", "tendermint"},
   319  		Short:   "CometBFT subcommands",
   320  	}
   321  
   322  	cometCmd.AddCommand(
   323  		ShowNodeIDCmd(),
   324  		ShowValidatorCmd(),
   325  		ShowAddressCmd(),
   326  		VersionCmd(),
   327  		cmtcmd.ResetAllCmd,
   328  		cmtcmd.ResetStateCmd,
   329  		BootstrapStateCmd(appCreator),
   330  	)
   331  
   332  	startCmd := StartCmd(appCreator, defaultNodeHome)
   333  	addStartFlags(startCmd)
   334  
   335  	rootCmd.AddCommand(
   336  		startCmd,
   337  		cometCmd,
   338  		ExportCmd(appExport, defaultNodeHome),
   339  		version.NewVersionCommand(),
   340  		NewRollbackCmd(appCreator, defaultNodeHome),
   341  		ModuleHashByHeightQuery(appCreator),
   342  	)
   343  }
   344  
   345  // AddCommandsWithStartCmdOptions adds server commands with the provided StartCmdOptions.
   346  func AddCommandsWithStartCmdOptions(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, opts StartCmdOptions) {
   347  	cometCmd := &cobra.Command{
   348  		Use:     "comet",
   349  		Aliases: []string{"cometbft", "tendermint"},
   350  		Short:   "CometBFT subcommands",
   351  	}
   352  
   353  	cometCmd.AddCommand(
   354  		ShowNodeIDCmd(),
   355  		ShowValidatorCmd(),
   356  		ShowAddressCmd(),
   357  		VersionCmd(),
   358  		cmtcmd.ResetAllCmd,
   359  		cmtcmd.ResetStateCmd,
   360  		BootstrapStateCmd(appCreator),
   361  	)
   362  
   363  	startCmd := StartCmdWithOptions(appCreator, defaultNodeHome, opts)
   364  
   365  	rootCmd.AddCommand(
   366  		startCmd,
   367  		cometCmd,
   368  		ExportCmd(appExport, defaultNodeHome),
   369  		version.NewVersionCommand(),
   370  		NewRollbackCmd(appCreator, defaultNodeHome),
   371  	)
   372  }
   373  
   374  // AddTestnetCreatorCommand allows chains to create a testnet from the state existing in their node's data directory.
   375  func AddTestnetCreatorCommand(rootCmd *cobra.Command, appCreator types.AppCreator, addStartFlags types.ModuleInitFlags) {
   376  	testnetCreateCmd := InPlaceTestnetCreator(appCreator)
   377  	addStartFlags(testnetCreateCmd)
   378  	rootCmd.AddCommand(testnetCreateCmd)
   379  }
   380  
   381  // https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go
   382  // TODO there must be a better way to get external IP
   383  func ExternalIP() (string, error) {
   384  	ifaces, err := net.Interfaces()
   385  	if err != nil {
   386  		return "", err
   387  	}
   388  
   389  	for _, iface := range ifaces {
   390  		if skipInterface(iface) {
   391  			continue
   392  		}
   393  		addrs, err := iface.Addrs()
   394  		if err != nil {
   395  			return "", err
   396  		}
   397  
   398  		for _, addr := range addrs {
   399  			ip := addrToIP(addr)
   400  			if ip == nil || ip.IsLoopback() {
   401  				continue
   402  			}
   403  			ip = ip.To4()
   404  			if ip == nil {
   405  				continue // not an ipv4 address
   406  			}
   407  			return ip.String(), nil
   408  		}
   409  	}
   410  	return "", errors.New("are you connected to the network?")
   411  }
   412  
   413  // ListenForQuitSignals listens for SIGINT and SIGTERM. When a signal is received,
   414  // the cleanup function is called, indicating the caller can gracefully exit or
   415  // return.
   416  //
   417  // Note, the blocking behavior of this depends on the block argument.
   418  // The caller must ensure the corresponding context derived from the cancelFn is used correctly.
   419  func ListenForQuitSignals(g *errgroup.Group, block bool, cancelFn context.CancelFunc, logger log.Logger) {
   420  	sigCh := make(chan os.Signal, 1)
   421  	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
   422  
   423  	f := func() {
   424  		sig := <-sigCh
   425  		cancelFn()
   426  
   427  		logger.Info("caught signal", "signal", sig.String())
   428  	}
   429  
   430  	if block {
   431  		g.Go(func() error {
   432  			f()
   433  			return nil
   434  		})
   435  	} else {
   436  		go f()
   437  	}
   438  }
   439  
   440  // GetAppDBBackend gets the backend type to use for the application DBs.
   441  func GetAppDBBackend(opts types.AppOptions) dbm.BackendType {
   442  	rv := cast.ToString(opts.Get("app-db-backend"))
   443  	if len(rv) == 0 {
   444  		rv = cast.ToString(opts.Get("db_backend"))
   445  	}
   446  
   447  	// Cosmos SDK has migrated to cosmos-db which does not support all the backends which tm-db supported
   448  	if rv == "cleveldb" || rv == "badgerdb" || rv == "boltdb" {
   449  		panic(fmt.Sprintf("invalid app-db-backend %q, use %q, %q, %q instead", rv, dbm.GoLevelDBBackend, dbm.PebbleDBBackend, dbm.RocksDBBackend))
   450  	}
   451  
   452  	if len(rv) != 0 {
   453  		return dbm.BackendType(rv)
   454  	}
   455  
   456  	return dbm.GoLevelDBBackend
   457  }
   458  
   459  func skipInterface(iface net.Interface) bool {
   460  	if iface.Flags&net.FlagUp == 0 {
   461  		return true // interface down
   462  	}
   463  
   464  	if iface.Flags&net.FlagLoopback != 0 {
   465  		return true // loopback interface
   466  	}
   467  
   468  	return false
   469  }
   470  
   471  func addrToIP(addr net.Addr) net.IP {
   472  	var ip net.IP
   473  
   474  	switch v := addr.(type) {
   475  	case *net.IPNet:
   476  		ip = v.IP
   477  	case *net.IPAddr:
   478  		ip = v.IP
   479  	}
   480  	return ip
   481  }
   482  
   483  func openDB(rootDir string, backendType dbm.BackendType) (dbm.DB, error) {
   484  	dataDir := filepath.Join(rootDir, "data")
   485  	return dbm.NewDB("application", backendType, dataDir)
   486  }
   487  
   488  func openTraceWriter(traceWriterFile string) (w io.WriteCloser, err error) {
   489  	if traceWriterFile == "" {
   490  		return
   491  	}
   492  	return os.OpenFile(
   493  		traceWriterFile,
   494  		os.O_WRONLY|os.O_APPEND|os.O_CREATE,
   495  		0o666,
   496  	)
   497  }
   498  
   499  // DefaultBaseappOptions returns the default baseapp options provided by the Cosmos SDK
   500  func DefaultBaseappOptions(appOpts types.AppOptions) []func(*baseapp.BaseApp) {
   501  	var cache storetypes.MultiStorePersistentCache
   502  
   503  	if cast.ToBool(appOpts.Get(FlagInterBlockCache)) {
   504  		cache = store.NewCommitKVStoreCacheManager()
   505  	}
   506  
   507  	pruningOpts, err := GetPruningOptionsFromFlags(appOpts)
   508  	if err != nil {
   509  		panic(err)
   510  	}
   511  
   512  	homeDir := cast.ToString(appOpts.Get(flags.FlagHome))
   513  	chainID := cast.ToString(appOpts.Get(flags.FlagChainID))
   514  	if chainID == "" {
   515  		// fallback to genesis chain-id
   516  		reader, err := os.Open(filepath.Join(homeDir, "config", "genesis.json"))
   517  		if err != nil {
   518  			panic(err)
   519  		}
   520  		defer reader.Close()
   521  
   522  		chainID, err = genutiltypes.ParseChainIDFromGenesis(reader)
   523  		if err != nil {
   524  			panic(fmt.Errorf("failed to parse chain-id from genesis file: %w", err))
   525  		}
   526  	}
   527  
   528  	snapshotStore, err := GetSnapshotStore(appOpts)
   529  	if err != nil {
   530  		panic(err)
   531  	}
   532  
   533  	snapshotOptions := snapshottypes.NewSnapshotOptions(
   534  		cast.ToUint64(appOpts.Get(FlagStateSyncSnapshotInterval)),
   535  		cast.ToUint32(appOpts.Get(FlagStateSyncSnapshotKeepRecent)),
   536  	)
   537  
   538  	defaultMempool := baseapp.SetMempool(mempool.NoOpMempool{})
   539  	if maxTxs := cast.ToInt(appOpts.Get(FlagMempoolMaxTxs)); maxTxs >= 0 {
   540  		defaultMempool = baseapp.SetMempool(
   541  			mempool.NewSenderNonceMempool(
   542  				mempool.SenderNonceMaxTxOpt(maxTxs),
   543  			),
   544  		)
   545  	}
   546  
   547  	return []func(*baseapp.BaseApp){
   548  		baseapp.SetPruning(pruningOpts),
   549  		baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(FlagMinGasPrices))),
   550  		baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(FlagHaltHeight))),
   551  		baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(FlagHaltTime))),
   552  		baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(FlagMinRetainBlocks))),
   553  		baseapp.SetInterBlockCache(cache),
   554  		baseapp.SetTrace(cast.ToBool(appOpts.Get(FlagTrace))),
   555  		baseapp.SetIndexEvents(cast.ToStringSlice(appOpts.Get(FlagIndexEvents))),
   556  		baseapp.SetSnapshot(snapshotStore, snapshotOptions),
   557  		baseapp.SetIAVLCacheSize(cast.ToInt(appOpts.Get(FlagIAVLCacheSize))),
   558  		baseapp.SetIAVLDisableFastNode(cast.ToBool(appOpts.Get(FlagDisableIAVLFastNode))),
   559  		defaultMempool,
   560  		baseapp.SetChainID(chainID),
   561  		baseapp.SetQueryGasLimit(cast.ToUint64(appOpts.Get(FlagQueryGasLimit))),
   562  	}
   563  }
   564  
   565  func GetSnapshotStore(appOpts types.AppOptions) (*snapshots.Store, error) {
   566  	homeDir := cast.ToString(appOpts.Get(flags.FlagHome))
   567  	snapshotDir := filepath.Join(homeDir, "data", "snapshots")
   568  	if err := os.MkdirAll(snapshotDir, 0o744); err != nil {
   569  		return nil, fmt.Errorf("failed to create snapshots directory: %w", err)
   570  	}
   571  
   572  	snapshotDB, err := dbm.NewDB("metadata", GetAppDBBackend(appOpts), snapshotDir)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  	snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  
   581  	return snapshotStore, nil
   582  }