diff --git a/server/v2/store/server.go b/server/v2/store/server.go new file mode 100644 index 000000000000..ec6d2d14e133 --- /dev/null +++ b/server/v2/store/server.go @@ -0,0 +1,90 @@ +package store + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "cosmossdk.io/core/server" + "cosmossdk.io/core/transaction" + serverv2 "cosmossdk.io/server/v2" + storev2 "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/root" +) + +var ( + _ serverv2.ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil) + _ serverv2.HasConfig = (*Server[transaction.Tx])(nil) + _ serverv2.HasCLICommands = (*Server[transaction.Tx])(nil) +) + +const ServerName = "store" + +// Server manages store config and contains prune & snapshot commands +type Server[T transaction.Tx] struct { + config *root.Config + store storev2.Backend +} + +func New[T transaction.Tx](store storev2.Backend, cfg server.ConfigMap) (*Server[T], error) { + config, err := UnmarshalConfig(cfg) + if err != nil { + return nil, err + } + return &Server[T]{ + store: store, + config: config, + }, nil +} + +func (s *Server[T]) Name() string { + return ServerName +} + +func (s *Server[T]) Start(context.Context) error { + return nil +} + +func (s *Server[T]) Stop(context.Context) error { + return nil +} + +func (s *Server[T]) CLICommands() serverv2.CLIConfig { + return serverv2.CLIConfig{ + Commands: []*cobra.Command{ + s.PrunesCmd(), + s.ExportSnapshotCmd(), + s.DeleteSnapshotCmd(), + s.ListSnapshotsCmd(), + s.DumpArchiveCmd(), + s.LoadArchiveCmd(), + s.RestoreSnapshotCmd(), + }, + } +} + +func (s *Server[T]) Config() any { + if s.config == nil || s.config.AppDBBackend == "" { + return root.DefaultConfig() + } + + return s.config +} + +// UnmarshalConfig unmarshals the store config from the given map. +// If the config is not found in the map, the default config is returned. +// If the home directory is found in the map, it sets the home directory in the config. +// An empty home directory *is* permitted at this stage, but attempting to build +// the store with an empty home directory will fail. +func UnmarshalConfig(cfg map[string]any) (*root.Config, error) { + config := root.DefaultConfig() + if err := serverv2.UnmarshalSubConfig(cfg, ServerName, config); err != nil { + return nil, fmt.Errorf("failed to unmarshal store config: %w", err) + } + home := cfg[serverv2.FlagHome] + if home != nil { + config.Home = home.(string) + } + return config, nil +} diff --git a/simapp/v2/app_test.go b/simapp/v2/app_test.go index 3ea361e3becb..2c11beca7caf 100644 --- a/simapp/v2/app_test.go +++ b/simapp/v2/app_test.go @@ -153,13 +153,13 @@ func TestSimAppExportAndBlockedAddrs_WithOneBlockProduced(t *testing.T) { MoveNextBlock(t, app, ctx) - _, err := app.ExportAppStateAndValidators(nil) + _, err := app.ExportAppStateAndValidators(false, nil) require.NoError(t, err) } func TestSimAppExportAndBlockedAddrs_NoBlocksProduced(t *testing.T) { app, _ := NewTestApp(t) - _, err := app.ExportAppStateAndValidators(nil) + _, err := app.ExportAppStateAndValidators(false, nil) require.NoError(t, err) } diff --git a/simapp/v2/export.go b/simapp/v2/export.go index 61175f41607f..04701bc6874f 100644 --- a/simapp/v2/export.go +++ b/simapp/v2/export.go @@ -13,7 +13,9 @@ import ( // file. // This is a demonstation of how to export a genesis file. Export may need extended at // the user discretion for cleaning the genesis state at the end provided with jailAllowedAddrs +// Same applies for forZeroHeight preprocessing. func (app *SimApp[T]) ExportAppStateAndValidators( + forZeroHeight bool, jailAllowedAddrs []string, ) (v2.ExportedApp, error) { ctx := context.Background() @@ -44,5 +46,9 @@ func (app *SimApp[T]) ExportAppStateAndValidators( exportedApp.AppState = genesis exportedApp.Height = int64(latestHeight) + if forZeroHeight { + exportedApp.Height = 0 + } + return exportedApp, nil } diff --git a/store/db/prefixdb_test.go b/store/db/prefixdb_test.go new file mode 100644 index 000000000000..3f25feabe3be --- /dev/null +++ b/store/db/prefixdb_test.go @@ -0,0 +1,50 @@ +package db_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "cosmossdk.io/store/db" + "cosmossdk.io/store/mock" +) + +func TestPrefixDB(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockDB := mock.NewMockKVStoreWithBatch(mockCtrl) + prefix := []byte("test:") + pdb := db.NewPrefixDB(mockDB, prefix) + + key := []byte("key1") + value := []byte("value1") + mockDB.EXPECT().Set(gomock.Eq(append(prefix, key...)), gomock.Eq(value)).Return(nil) + + err := pdb.Set(key, value) + require.NoError(t, err) + + mockDB.EXPECT().Get(gomock.Eq(append(prefix, key...))).Return(value, nil) + + returnedValue, err := pdb.Get(key) + require.NoError(t, err) + require.Equal(t, value, returnedValue) + + mockDB.EXPECT().Has(gomock.Eq(append(prefix, key...))).Return(true, nil) + + has, err := pdb.Has(key) + require.NoError(t, err) + require.True(t, has) + + mockDB.EXPECT().Delete(gomock.Eq(append(prefix, key...))).Return(nil) + + err = pdb.Delete(key) + require.NoError(t, err) + + mockDB.EXPECT().Has(gomock.Eq(append(prefix, key...))).Return(false, nil) + + has, err = pdb.Has(key) + require.NoError(t, err) + require.False(t, has) +} diff --git a/store/v2/root/builder.go b/store/v2/root/builder.go new file mode 100644 index 000000000000..a282c9fe3328 --- /dev/null +++ b/store/v2/root/builder.go @@ -0,0 +1,100 @@ +package root + +import ( + "fmt" + "path/filepath" + + "cosmossdk.io/log" + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/db" +) + +// Builder is the interface for a store/v2 RootStore builder. +// RootStores built by the Cosmos SDK typically involve a 2 phase initialization: +// 1. Namespace registration +// 2. Configuration and loading +// +// The Builder interface is used to facilitate this pattern. Namespaces (store keys) are registered +// by calling RegisterKey before Build is called. Build is then called with a Config +// object and a RootStore is returned. Calls to Get may return the `RootStore` if Build +// was successful, but that's left up to the implementation. +type Builder interface { + // Build creates a new store/v2 RootStore from the given Config. + Build(log.Logger, *Config) (store.RootStore, error) + // RegisterKey registers a store key (namespace) to be used when building the RootStore. + RegisterKey(string) + // Get returns the Store. Build should be called before calling Get or the result will be nil. + Get() store.RootStore +} + +var _ Builder = (*builder)(nil) + +// builder is the default builder for a store/v2 RootStore satisfying the Store interface. +// Tangibly it combines store key registration and a top-level Config to create a RootStore by calling +// the CreateRootStore factory function. +type builder struct { + // input + storeKeys map[string]struct{} + + // output + store store.RootStore +} + +func NewBuilder() Builder { + return &builder{storeKeys: make(map[string]struct{})} +} + +// Build creates a new store/v2 RootStore. +func (sb *builder) Build( + logger log.Logger, + config *Config, +) (store.RootStore, error) { + if sb.store != nil { + return sb.store, nil + } + if config.Home == "" { + return nil, fmt.Errorf("home directory is required") + } + + if len(config.AppDBBackend) == 0 { + return nil, fmt.Errorf("application db backend is required") + } + + scRawDb, err := db.NewDB( + db.DBType(config.AppDBBackend), + "application", + filepath.Join(config.Home, "data"), + nil, + ) + if err != nil { + return nil, fmt.Errorf("failed to create SCRawDB: %w", err) + } + + var storeKeys []string + for key := range sb.storeKeys { + storeKeys = append(storeKeys, key) + } + + factoryOptions := &FactoryOptions{ + Logger: logger.With("module", "store"), + RootDir: config.Home, + Options: config.Options, + StoreKeys: storeKeys, + SCRawDB: scRawDb, + } + + rs, err := CreateRootStore(factoryOptions) + if err != nil { + return nil, fmt.Errorf("failed to create root store: %w", err) + } + sb.store = rs + return sb.store, nil +} + +func (sb *builder) Get() store.RootStore { + return sb.store +} + +func (sb *builder) RegisterKey(key string) { + sb.storeKeys[key] = struct{}{} +} diff --git a/tests/integration/v2/distribution/fixture_test.go b/tests/integration/v2/distribution/fixture_test.go new file mode 100644 index 000000000000..f84278be5555 --- /dev/null +++ b/tests/integration/v2/distribution/fixture_test.go @@ -0,0 +1,159 @@ +package distribution + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/comet" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/router" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/runtime/v2" + _ "cosmossdk.io/x/accounts" // import as blank for app wiring + _ "cosmossdk.io/x/bank" // import as blank for app wiring + bankkeeper "cosmossdk.io/x/bank/keeper" + banktypes "cosmossdk.io/x/bank/types" + _ "cosmossdk.io/x/consensus" // import as blank for app wiring + _ "cosmossdk.io/x/distribution" // import as blank for app wiring + distrkeeper "cosmossdk.io/x/distribution/keeper" + _ "cosmossdk.io/x/mint" // import as blank for app wiring + _ "cosmossdk.io/x/protocolpool" // import as blank for app wiring + poolkeeper "cosmossdk.io/x/protocolpool/keeper" + _ "cosmossdk.io/x/staking" // import as blank for app wiring + stakingkeeper "cosmossdk.io/x/staking/keeper" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/tests/integration/v2" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/cosmos-sdk/x/auth" // import as blank for app wiring + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + _ "github.com/cosmos/cosmos-sdk/x/auth/tx/config" // import as blank for app wiring`` + _ "github.com/cosmos/cosmos-sdk/x/auth/vesting" // import as blank for app wiring + _ "github.com/cosmos/cosmos-sdk/x/genutil" // import as blank for app wiring +) + +var ( + emptyDelAddr sdk.AccAddress + emptyValAddr sdk.ValAddress +) + +var ( + PKS = simtestutil.CreateTestPubKeys(3) + + valConsPk0 = PKS[0] +) + +type fixture struct { + app *integration.App + + ctx context.Context + cdc codec.Codec + + queryClient distrkeeper.Querier + + authKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + distrKeeper distrkeeper.Keeper + stakingKeeper *stakingkeeper.Keeper + poolKeeper poolkeeper.Keeper + + addr sdk.AccAddress + valAddr sdk.ValAddress +} + +func createTestFixture(t *testing.T) *fixture { + t.Helper() + res := fixture{} + + moduleConfigs := []configurator.ModuleOption{ + configurator.AccountsModule(), + configurator.AuthModule(), + configurator.BankModule(), + configurator.StakingModule(), + configurator.TxModule(), + configurator.ValidateModule(), + configurator.ConsensusModule(), + configurator.GenutilModule(), + configurator.DistributionModule(), + configurator.MintModule(), + configurator.ProtocolPoolModule(), + } + + var err error + startupCfg := integration.DefaultStartUpConfig(t) + + msgRouterService := integration.NewRouterService() + res.registerMsgRouterService(msgRouterService) + + var routerFactory runtime.RouterServiceFactory = func(_ []byte) router.Service { + return msgRouterService + } + + queryRouterService := integration.NewRouterService() + res.registerQueryRouterService(queryRouterService) + + serviceBuilder := runtime.NewRouterBuilder(routerFactory, queryRouterService) + + startupCfg.BranchService = &integration.BranchService{} + startupCfg.RouterServiceBuilder = serviceBuilder + startupCfg.HeaderService = &integration.HeaderService{} + + res.app, err = integration.NewApp( + depinject.Configs(configurator.NewAppV2Config(moduleConfigs...), depinject.Supply(log.NewNopLogger())), + startupCfg, + &res.bankKeeper, &res.distrKeeper, &res.authKeeper, &res.stakingKeeper, &res.poolKeeper, &res.cdc) + require.NoError(t, err) + + addr := sdk.AccAddress(PKS[0].Address()) + valAddr := sdk.ValAddress(addr) + valConsAddr := sdk.ConsAddress(valConsPk0.Address()) + + ctx := res.app.StateLatestContext(t) + res.addr = addr + res.valAddr = valAddr + + // set proposer and vote infos + res.ctx = context.WithValue(ctx, corecontext.CometInfoKey, comet.Info{ + LastCommit: comet.CommitInfo{ + Votes: []comet.VoteInfo{ + { + Validator: comet.Validator{ + Address: valAddr, + Power: 100, + }, + BlockIDFlag: comet.BlockIDFlagCommit, + }, + }, + }, + ProposerAddress: valConsAddr, + }) + + res.queryClient = distrkeeper.NewQuerier(res.distrKeeper) + + return &res +} + +func (s *fixture) registerMsgRouterService(router *integration.RouterService) { + // register custom router service + bankSendHandler := func(ctx context.Context, req transaction.Msg) (transaction.Msg, error) { + msg, ok := req.(*banktypes.MsgSend) + if !ok { + return nil, integration.ErrInvalidMsgType + } + msgServer := bankkeeper.NewMsgServerImpl(s.bankKeeper) + resp, err := msgServer.Send(ctx, msg) + return resp, err + } + + router.RegisterHandler(bankSendHandler, "cosmos.bank.v1beta1.MsgSend") +} + +func (s *fixture) registerQueryRouterService(router *integration.RouterService) { + // register custom router service +} diff --git a/tests/systemtests/go.sum b/tests/systemtests/go.sum index 5302e18bccc6..21ec58a6cddf 100644 --- a/tests/systemtests/go.sum +++ b/tests/systemtests/go.sum @@ -18,8 +18,13 @@ cosmossdk.io/store v1.1.0 h1:LnKwgYMc9BInn9PhpTFEQVbL9UK475G2H911CGGnWHk= cosmossdk.io/store v1.1.0/go.mod h1:oZfW/4Fc/zYqu3JmQcQdUJ3fqu5vnYTn3LZFFy8P8ng= cosmossdk.io/systemtests v1.0.0-rc.3 h1:W1ZdfHtWxbzRCiBwcMb1nMKkmUNyAcHapJOrfh1lX20= cosmossdk.io/systemtests v1.0.0-rc.3/go.mod h1:B3RY1tY/iwLjQ9MUTz+GsiXV9gEdS8mfUvSQtWUwaAo= +<<<<<<< HEAD cosmossdk.io/x/tx v1.0.0-alpha.2 h1:UW80FMm7B0fiAMsrfe5+HabSJ3XBg+tQa6/GK9prqWk= cosmossdk.io/x/tx v1.0.0-alpha.2/go.mod h1:r4yTKSJ7ZCCR95YbBfY3nfvbgNw6m9F6f25efWYYQWo= +======= +cosmossdk.io/x/tx v0.13.3-0.20240419091757-db5906b1e894 h1:kHEvzVqpNv/9pnaEPBsgE/FMc+cVmWjSsInRufkZkpQ= +cosmossdk.io/x/tx v0.13.3-0.20240419091757-db5906b1e894/go.mod h1:Tb6/tpONmtL5qFdOMdv1pdvrtJNxcazZBoz04HB71ss= +>>>>>>> 34f407d63 (test(systemtests): fix export v2 (#22799)) dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= diff --git a/x/genutil/v2/cli/commands.go b/x/genutil/v2/cli/commands.go index 6812345d23ba..8e27cbafc04b 100644 --- a/x/genutil/v2/cli/commands.go +++ b/x/genutil/v2/cli/commands.go @@ -20,7 +20,7 @@ type genesisMM interface { } type ExportableApp interface { - ExportAppStateAndValidators([]string) (v2.ExportedApp, error) + ExportAppStateAndValidators(forZeroHeight bool, jailAllowedAddrs []string) (v2.ExportedApp, error) LoadHeight(uint64) error } diff --git a/x/genutil/v2/cli/export.go b/x/genutil/v2/cli/export.go index c53236d49329..812201c322fd 100644 --- a/x/genutil/v2/cli/export.go +++ b/x/genutil/v2/cli/export.go @@ -17,6 +17,7 @@ import ( const ( flagHeight = "height" + flagForZeroHeight = "for-zero-height" flagJailAllowedAddrs = "jail-allowed-addrs" ) @@ -56,6 +57,7 @@ func ExportCmd(app ExportableApp) *cobra.Command { } height, _ := cmd.Flags().GetInt64(flagHeight) + forZeroHeight, _ := cmd.Flags().GetBool(flagForZeroHeight) jailAllowedAddrs, _ := cmd.Flags().GetStringSlice(flagJailAllowedAddrs) outputDocument, _ := cmd.Flags().GetString(flags.FlagOutputDocument) if height != -1 { @@ -63,7 +65,7 @@ func ExportCmd(app ExportableApp) *cobra.Command { return err } } - exported, err := app.ExportAppStateAndValidators(jailAllowedAddrs) + exported, err := app.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs) if err != nil { return fmt.Errorf("error exporting state: %w", err) } @@ -105,6 +107,7 @@ func ExportCmd(app ExportableApp) *cobra.Command { StringSlice(flagJailAllowedAddrs, []string{}, "Comma-separated list of operator addresses of jailed validators to unjail") cmd.Flags(). String(flags.FlagOutputDocument, "", "Exported state is written to the given file instead of STDOUT") + cmd.Flags().Bool(flagForZeroHeight, false, "Export state to start at height zero (perform preproccessing)") return cmd }