diff --git a/baseapp/abci.go b/baseapp/abci.go index ade6af6cccdf..16a5ede2ebf1 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -23,8 +23,21 @@ import ( // InitChain implements the ABCI interface. It runs the initialization logic // directly on the CommitMultiStore. func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) { + // On a new chain, we consider the init chain block height as 0, even though + // req.InitialHeight is 1 by default. initHeader := tmproto.Header{ChainID: req.ChainId, Time: req.Time} + // If req.InitialHeight is > 1, then we set the initial version in the + // stores. + if req.InitialHeight > 1 { + app.initialHeight = req.InitialHeight + initHeader = tmproto.Header{ChainID: req.ChainId, Height: req.InitialHeight, Time: req.Time} + err := app.cms.SetInitialVersion(req.InitialHeight) + if err != nil { + panic(err) + } + } + // initialize the deliver state and check state with a correct header app.setDeliverState(initHeader) app.setCheckState(initHeader) @@ -60,13 +73,13 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC sort.Sort(abci.ValidatorUpdates(res.Validators)) for i := range res.Validators { - if proto.Equal(&res.Validators[i], &req.Validators[i]) { + if !proto.Equal(&res.Validators[i], &req.Validators[i]) { panic(fmt.Errorf("genesisValidators[%d] != req.Validators[%d] ", i, i)) } } } - // NOTE: We don't commit, but BeginBlock for block 1 starts from this + // NOTE: We don't commit, but BeginBlock for block `initial_height` starts from this // deliverState. return res } @@ -97,7 +110,7 @@ func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { return abci.ResponseQuery{} } -// FilterPeerByIDfilters peers by node ID. +// FilterPeerByID filters peers by node ID. func (app *BaseApp) FilterPeerByID(info string) abci.ResponseQuery { if app.idPeerFilter != nil { return app.idPeerFilter(info) diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 4455696afb61..7a4316e7ef68 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -81,6 +81,9 @@ type BaseApp struct { // nolint: maligned // transaction. This is mainly used for DoS and spam prevention. minGasPrices sdk.DecCoins + // initialHeight is the initial height at which we start the baseapp + initialHeight int64 + // flag for sealing options and parameters to a BaseApp sealed bool @@ -206,12 +209,6 @@ func (app *BaseApp) MountMemoryStores(keys map[string]*sdk.MemoryStoreKey) { } } -// MountStoreWithDB mounts a store to the provided key in the BaseApp -// multistore, using a specified DB. -func (app *BaseApp) MountStoreWithDB(key sdk.StoreKey, typ sdk.StoreType, db dbm.DB) { - app.cms.MountStoreWithDB(key, typ, db) -} - // MountStore mounts a store to the provided key in the BaseApp multistore, // using the default DB. func (app *BaseApp) MountStore(key sdk.StoreKey, typ sdk.StoreType) { @@ -422,9 +419,23 @@ func (app *BaseApp) validateHeight(req abci.RequestBeginBlock) error { return fmt.Errorf("invalid height: %d", req.Header.Height) } - prevHeight := app.LastBlockHeight() - if req.Header.Height != prevHeight+1 { - return fmt.Errorf("invalid height: %d; expected: %d", req.Header.Height, prevHeight+1) + // expectedHeight holds the expected height to validate. + var expectedHeight int64 + if app.LastBlockHeight() == 0 && app.initialHeight > 1 { + // In this case, we're validating the first block of the chain (no + // previous commit). The height we're expecting is the initial height. + expectedHeight = app.initialHeight + } else { + // This case can means two things: + // - either there was already a previous commit in the store, in which + // case we increment the version from there, + // - or there was no previous commit, and initial version was not set, + // in which case we start at version 1. + expectedHeight = app.LastBlockHeight() + 1 + } + + if req.Header.Height != expectedHeight { + return fmt.Errorf("invalid height: %d; expected: %d", req.Header.Height, expectedHeight) } return nil diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index 05038ce2c36f..4371be44f6e9 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -535,6 +535,52 @@ func TestInitChainer(t *testing.T) { require.Equal(t, value, res.Value) } +func TestInitChain_WithInitialHeight(t *testing.T) { + name := t.Name() + db := dbm.NewMemDB() + logger := defaultLogger() + app := NewBaseApp(name, logger, db, nil) + + app.InitChain( + abci.RequestInitChain{ + InitialHeight: 3, + }, + ) + app.Commit() + + require.Equal(t, int64(3), app.LastBlockHeight()) +} + +func TestBeginBlock_WithInitialHeight(t *testing.T) { + name := t.Name() + db := dbm.NewMemDB() + logger := defaultLogger() + app := NewBaseApp(name, logger, db, nil) + + app.InitChain( + abci.RequestInitChain{ + InitialHeight: 3, + }, + ) + + require.PanicsWithError(t, "invalid height: 4; expected: 3", func() { + app.BeginBlock(abci.RequestBeginBlock{ + Header: tmproto.Header{ + Height: 4, + }, + }) + }) + + app.BeginBlock(abci.RequestBeginBlock{ + Header: tmproto.Header{ + Height: 3, + }, + }) + app.Commit() + + require.Equal(t, int64(3), app.LastBlockHeight()) +} + // Simple tx with a list of Msgs. type txTest struct { Msgs []sdk.Msg @@ -1292,7 +1338,6 @@ func TestCustomRunTxPanicHandler(t *testing.T) { anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error) { panic(sdkerrors.Wrap(anteErr, "anteHandler")) - return }) } routerOpt := func(bapp *BaseApp) { diff --git a/server/export.go b/server/export.go index 18eb00dc7380..55746d9aa938 100644 --- a/server/export.go +++ b/server/export.go @@ -3,12 +3,12 @@ package server // DONTCOVER import ( - "encoding/json" "fmt" "io/ioutil" "os" "github.com/spf13/cobra" + tmjson "github.com/tendermint/tendermint/libs/json" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtypes "github.com/tendermint/tendermint/types" @@ -35,6 +35,10 @@ func ExportCmd(appExporter types.AppExporter, defaultNodeHome string) *cobra.Com homeDir, _ := cmd.Flags().GetString(flags.FlagHome) config.SetRoot(homeDir) + if _, err := os.Stat(config.GenesisFile()); os.IsNotExist(err) { + return err + } + db, err := openDB(config.RootDir) if err != nil { return err @@ -64,7 +68,7 @@ func ExportCmd(appExporter types.AppExporter, defaultNodeHome string) *cobra.Com forZeroHeight, _ := cmd.Flags().GetBool(FlagForZeroHeight) jailAllowedAddrs, _ := cmd.Flags().GetStringSlice(FlagJailAllowedAddrs) - appState, validators, cp, err := appExporter(serverCtx.Logger, db, traceWriter, height, forZeroHeight, jailAllowedAddrs) + exported, err := appExporter(serverCtx.Logger, db, traceWriter, height, forZeroHeight, jailAllowedAddrs) if err != nil { return fmt.Errorf("error exporting state: %v", err) } @@ -74,29 +78,30 @@ func ExportCmd(appExporter types.AppExporter, defaultNodeHome string) *cobra.Com return err } - doc.AppState = appState - doc.Validators = validators + doc.AppState = exported.AppState + doc.Validators = exported.Validators + doc.InitialHeight = exported.Height doc.ConsensusParams = &tmproto.ConsensusParams{ Block: tmproto.BlockParams{ - MaxBytes: cp.Block.MaxBytes, - MaxGas: cp.Block.MaxGas, + MaxBytes: exported.ConsensusParams.Block.MaxBytes, + MaxGas: exported.ConsensusParams.Block.MaxGas, TimeIotaMs: doc.ConsensusParams.Block.TimeIotaMs, }, Evidence: tmproto.EvidenceParams{ - MaxAgeNumBlocks: cp.Evidence.MaxAgeNumBlocks, - MaxAgeDuration: cp.Evidence.MaxAgeDuration, - MaxNum: cp.Evidence.MaxNum, - ProofTrialPeriod: cp.Evidence.ProofTrialPeriod, + MaxAgeNumBlocks: exported.ConsensusParams.Evidence.MaxAgeNumBlocks, + MaxAgeDuration: exported.ConsensusParams.Evidence.MaxAgeDuration, + MaxNum: exported.ConsensusParams.Evidence.MaxNum, + ProofTrialPeriod: exported.ConsensusParams.Evidence.ProofTrialPeriod, }, Validator: tmproto.ValidatorParams{ - PubKeyTypes: cp.Validator.PubKeyTypes, + PubKeyTypes: exported.ConsensusParams.Validator.PubKeyTypes, }, } - // NOTE: for now we're just using standard JSON marshaling for the root GenesisDoc. - // These types are in Tendermint, don't support proto and as far as we know, don't need it. - // All of the protobuf/amino state is inside AppState - encoded, err := json.MarshalIndent(doc, "", " ") + // NOTE: Tendermint uses a custom JSON decoder for GenesisDoc + // (except for stuff inside AppState). Inside AppState, we're free + // to encode as protobuf or amino. + encoded, err := tmjson.Marshal(doc) if err != nil { return err } diff --git a/server/export_test.go b/server/export_test.go index 585807812655..4e96d8114887 100644 --- a/server/export_test.go +++ b/server/export_test.go @@ -1,4 +1,4 @@ -package server +package server_test import ( "bytes" @@ -10,15 +10,20 @@ import ( "path" "testing" + "github.com/spf13/cobra" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" + tmjson "github.com/tendermint/tendermint/libs/json" "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtypes "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-db" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/types" "github.com/cosmos/cosmos-sdk/simapp" "github.com/cosmos/cosmos-sdk/testutil" "github.com/cosmos/cosmos-sdk/types/errors" @@ -29,21 +34,118 @@ func TestExportCmd_ConsensusParams(t *testing.T) { tempDir, clean := testutil.NewTestCaseDir(t) defer clean() + _, ctx, genDoc, cmd := setupApp(t, tempDir) + + output := &bytes.Buffer{} + cmd.SetOut(output) + cmd.SetArgs([]string{fmt.Sprintf("--%s=%s", flags.FlagHome, tempDir)}) + require.NoError(t, cmd.ExecuteContext(ctx)) + + var exportedGenDoc tmtypes.GenesisDoc + err := tmjson.Unmarshal(output.Bytes(), &exportedGenDoc) + if err != nil { + t.Fatalf("error unmarshaling exported genesis doc: %s", err) + } + + require.Equal(t, genDoc.ConsensusParams.Block.TimeIotaMs, exportedGenDoc.ConsensusParams.Block.TimeIotaMs) + require.Equal(t, simapp.DefaultConsensusParams.Block.MaxBytes, exportedGenDoc.ConsensusParams.Block.MaxBytes) + require.Equal(t, simapp.DefaultConsensusParams.Block.MaxGas, exportedGenDoc.ConsensusParams.Block.MaxGas) + + require.Equal(t, simapp.DefaultConsensusParams.Evidence.MaxAgeDuration, exportedGenDoc.ConsensusParams.Evidence.MaxAgeDuration) + require.Equal(t, simapp.DefaultConsensusParams.Evidence.MaxAgeNumBlocks, exportedGenDoc.ConsensusParams.Evidence.MaxAgeNumBlocks) + + require.Equal(t, simapp.DefaultConsensusParams.Validator.PubKeyTypes, exportedGenDoc.ConsensusParams.Validator.PubKeyTypes) +} + +func TestExportCmd_HomeDir(t *testing.T) { + tempDir, clean := testutil.NewTestCaseDir(t) + defer clean() + + _, ctx, _, cmd := setupApp(t, tempDir) + + cmd.SetArgs([]string{fmt.Sprintf("--%s=%s", flags.FlagHome, "foobar")}) + err := cmd.ExecuteContext(ctx) + require.EqualError(t, err, "stat foobar/config/genesis.json: no such file or directory") +} + +func TestExportCmd_Height(t *testing.T) { + testCases := []struct { + name string + flags []string + fastForward int64 + expHeight int64 + }{ + { + "should export correct height", + []string{}, + 5, 6, + }, + { + "should export correct height with --height", + []string{ + fmt.Sprintf("--%s=%d", server.FlagHeight, 3), + }, + 5, 4, + }, + { + "should export height 0 with --for-zero-height", + []string{ + fmt.Sprintf("--%s=%s", server.FlagForZeroHeight, "true"), + }, + 2, 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tempDir, clean := testutil.NewTestCaseDir(t) + defer clean() + + app, ctx, _, cmd := setupApp(t, tempDir) + + // Fast forward to block `tc.fastForward`. + for i := int64(2); i <= tc.fastForward; i++ { + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: i}}) + app.Commit() + } + + output := &bytes.Buffer{} + cmd.SetOut(output) + args := append(tc.flags, fmt.Sprintf("--%s=%s", flags.FlagHome, tempDir)) + cmd.SetArgs(args) + require.NoError(t, cmd.ExecuteContext(ctx)) + + var exportedGenDoc tmtypes.GenesisDoc + err := tmjson.Unmarshal(output.Bytes(), &exportedGenDoc) + if err != nil { + t.Fatalf("error unmarshaling exported genesis doc: %s", err) + } + + require.Equal(t, tc.expHeight, exportedGenDoc.InitialHeight) + }) + } + +} + +func setupApp(t *testing.T, tempDir string) (*simapp.SimApp, context.Context, *tmtypes.GenesisDoc, *cobra.Command) { err := createConfigFolder(tempDir) if err != nil { t.Fatalf("error creating config folder: %s", err) } + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) db := dbm.NewMemDB() - app := simapp.NewSimApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, tempDir, 0, simapp.MakeEncodingConfig()) + encCfg := simapp.MakeEncodingConfig() + app := simapp.NewSimApp(logger, db, nil, true, map[int64]bool{}, tempDir, 0, encCfg) - serverCtx := NewDefaultContext() + serverCtx := server.NewDefaultContext() serverCtx.Config.RootDir = tempDir clientCtx := client.Context{}.WithJSONMarshaler(app.AppCodec()) genDoc := newDefaultGenesisDoc() err = saveGenesisFile(genDoc, serverCtx.Config.GenesisFile()) + require.NoError(t, err) app.InitChain( abci.RequestInitChain{ @@ -55,34 +157,29 @@ func TestExportCmd_ConsensusParams(t *testing.T) { app.Commit() - cmd := ExportCmd( - func(logger log.Logger, db dbm.DB, writer io.Writer, i int64, b bool, strings []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error) { - return app.ExportAppStateAndValidators(true, []string{}) - }, tempDir) - - ctx := context.Background() - ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) - ctx = context.WithValue(ctx, ServerContextKey, serverCtx) + cmd := server.ExportCmd( + func(_ log.Logger, _ dbm.DB, _ io.Writer, height int64, forZeroHeight bool, jailAllowedAddrs []string) (types.ExportedApp, error) { + encCfg := simapp.MakeEncodingConfig() - output := &bytes.Buffer{} - cmd.SetOut(output) - cmd.SetArgs([]string{fmt.Sprintf("--%s=%s", flags.FlagHome, tempDir)}) - require.NoError(t, cmd.ExecuteContext(ctx)) + var simApp *simapp.SimApp + if height != -1 { + simApp = simapp.NewSimApp(logger, db, nil, false, map[int64]bool{}, "", 0, encCfg) - var exportedGenDoc tmtypes.GenesisDoc - err = json.Unmarshal(output.Bytes(), &exportedGenDoc) - if err != nil { - t.Fatalf("error unmarshaling exported genesis doc: %s", err) - } + if err := simApp.LoadHeight(height); err != nil { + return types.ExportedApp{}, err + } + } else { + simApp = simapp.NewSimApp(logger, db, nil, true, map[int64]bool{}, "", 0, encCfg) + } - require.Equal(t, genDoc.ConsensusParams.Block.TimeIotaMs, exportedGenDoc.ConsensusParams.Block.TimeIotaMs) - require.Equal(t, simapp.DefaultConsensusParams.Block.MaxBytes, exportedGenDoc.ConsensusParams.Block.MaxBytes) - require.Equal(t, simapp.DefaultConsensusParams.Block.MaxGas, exportedGenDoc.ConsensusParams.Block.MaxGas) + return simApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs) + }, tempDir) - require.Equal(t, simapp.DefaultConsensusParams.Evidence.MaxAgeDuration, exportedGenDoc.ConsensusParams.Evidence.MaxAgeDuration) - require.Equal(t, simapp.DefaultConsensusParams.Evidence.MaxAgeNumBlocks, exportedGenDoc.ConsensusParams.Evidence.MaxAgeNumBlocks) + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) - require.Equal(t, simapp.DefaultConsensusParams.Validator.PubKeyTypes, exportedGenDoc.ConsensusParams.Validator.PubKeyTypes) + return app, ctx, genDoc, cmd } func createConfigFolder(dir string) error { diff --git a/server/mock/store.go b/server/mock/store.go index d8e9cee1c200..7bc42d94b308 100644 --- a/server/mock/store.go +++ b/server/mock/store.go @@ -99,6 +99,10 @@ func (ms multiStore) SetInterBlockCache(_ sdk.MultiStorePersistentCache) { panic("not implemented") } +func (ms multiStore) SetInitialVersion(version int64) error { + panic("not implemented") +} + var _ sdk.KVStore = kvStore{} type kvStore struct { diff --git a/server/types/app.go b/server/types/app.go index f68a93207630..db1b2af8c77c 100644 --- a/server/types/app.go +++ b/server/types/app.go @@ -42,7 +42,20 @@ type ( // application using various configurations. AppCreator func(log.Logger, dbm.DB, io.Writer, AppOptions) Application + // ExportedApp represents an exported app state, along with + // validators, consensus params and latest app height. + ExportedApp struct { + // AppState is the application state as JSON. + AppState json.RawMessage + // Validators is the exported validator set. + Validators []tmtypes.GenesisValidator + // Height is the app's latest block height. + Height int64 + // ConsensusParams are the exported consensus params for ABCI. + ConsensusParams *abci.ConsensusParams + } + // AppExporter is a function that dumps all app state to // JSON-serializable structure and returns the current validator set. - AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error) + AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool, []string) (ExportedApp, error) ) diff --git a/simapp/app.go b/simapp/app.go index 10351e6dd616..feac2187209f 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -383,15 +383,17 @@ func NewSimApp( if err := app.LoadLatestVersion(); err != nil { tmos.Exit(err.Error()) } - } - // Initialize and seal the capability keeper so all persistent capabilities - // are loaded in-memory and prevent any further modules from creating scoped - // sub-keepers. - // This must be done during creation of baseapp rather than in InitChain so - // that in-memory capabilities get regenerated on app restart - ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) - app.CapabilityKeeper.InitializeAndSeal(ctx) + // Initialize and seal the capability keeper so all persistent capabilities + // are loaded in-memory and prevent any further modules from creating scoped + // sub-keepers. + // This must be done during creation of baseapp rather than in InitChain so + // that in-memory capabilities get regenerated on app restart. + // Note that since this reads from the store, we can only perform it when + // `loadLatest` is set to true. + ctx := app.BaseApp.NewUncachedContext(true, tmproto.Header{}) + app.CapabilityKeeper.InitializeAndSeal(ctx) + } app.ScopedIBCKeeper = scopedIBCKeeper app.ScopedTransferKeeper = scopedTransferKeeper diff --git a/simapp/app_test.go b/simapp/app_test.go index 84ba317d716d..4a8797562287 100644 --- a/simapp/app_test.go +++ b/simapp/app_test.go @@ -31,7 +31,7 @@ func TestSimAppExport(t *testing.T) { // Making a new app object with the db, so that initchain hasn't been called app2 := NewSimApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, MakeEncodingConfig()) - _, _, _, err = app2.ExportAppStateAndValidators(false, []string{}) + _, err = app2.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err, "ExportAppStateAndValidators should not have an error") } diff --git a/simapp/export.go b/simapp/export.go index 07c59c2fdcc6..08deec4619a9 100644 --- a/simapp/export.go +++ b/simapp/export.go @@ -4,10 +4,9 @@ import ( "encoding/json" "log" - abci "github.com/tendermint/tendermint/abci/types" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - tmtypes "github.com/tendermint/tendermint/types" + servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" "github.com/cosmos/cosmos-sdk/x/staking" @@ -19,23 +18,31 @@ import ( // file. func (app *SimApp) ExportAppStateAndValidators( forZeroHeight bool, jailAllowedAddrs []string, -) (appState json.RawMessage, validators []tmtypes.GenesisValidator, cp *abci.ConsensusParams, err error) { - +) (servertypes.ExportedApp, error) { // as if they could withdraw from the start of the next block ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + // We export at last height + 1, because that's the height at which + // Tendermint will start InitChain. + height := app.LastBlockHeight() + 1 if forZeroHeight { + height = 0 app.prepForZeroHeightGenesis(ctx, jailAllowedAddrs) } genState := app.mm.ExportGenesis(ctx, app.appCodec) - appState, err = json.MarshalIndent(genState, "", " ") + appState, err := json.MarshalIndent(genState, "", " ") if err != nil { - return nil, nil, nil, err + return servertypes.ExportedApp{}, err } - validators = staking.WriteValidators(ctx, app.StakingKeeper) - return appState, validators, app.BaseApp.GetConsensusParams(ctx), nil + validators := staking.WriteValidators(ctx, app.StakingKeeper) + return servertypes.ExportedApp{ + AppState: appState, + Validators: validators, + Height: height, + ConsensusParams: app.BaseApp.GetConsensusParams(ctx), + }, nil } // prepare for fresh start at zero height diff --git a/simapp/sim_test.go b/simapp/sim_test.go index 659ce601c120..b11090c14658 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -131,7 +131,7 @@ func TestAppImportExport(t *testing.T) { fmt.Printf("exporting genesis...\n") - appState, _, consensusParams, err := app.ExportAppStateAndValidators(false, []string{}) + exported, err := app.ExportAppStateAndValidators(false, []string{}) require.NoError(t, err) fmt.Printf("importing genesis...\n") @@ -148,13 +148,13 @@ func TestAppImportExport(t *testing.T) { require.Equal(t, "SimApp", newApp.Name()) var genesisState GenesisState - err = json.Unmarshal(appState, &genesisState) + err = json.Unmarshal(exported.AppState, &genesisState) require.NoError(t, err) ctxA := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) ctxB := newApp.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) newApp.mm.InitGenesis(ctxB, app.AppCodec(), genesisState) - newApp.StoreConsensusParams(ctxB, consensusParams) + newApp.StoreConsensusParams(ctxB, exported.ConsensusParams) fmt.Printf("comparing stores...\n") @@ -232,7 +232,7 @@ func TestAppSimulationAfterImport(t *testing.T) { fmt.Printf("exporting genesis...\n") - appState, _, _, err := app.ExportAppStateAndValidators(true, []string{}) + exported, err := app.ExportAppStateAndValidators(true, []string{}) require.NoError(t, err) fmt.Printf("importing genesis...\n") @@ -249,7 +249,7 @@ func TestAppSimulationAfterImport(t *testing.T) { require.Equal(t, "SimApp", newApp.Name()) newApp.InitChain(abci.RequestInitChain{ - AppStateBytes: appState, + AppStateBytes: exported.AppState, }) _, _, err = simulation.SimulateFromSeed( diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index ab961123e724..cd9649ed0820 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "encoding/json" "io" "os" @@ -11,10 +10,8 @@ import ( "github.com/spf13/cast" "github.com/spf13/cobra" - abci "github.com/tendermint/tendermint/abci/types" tmcli "github.com/tendermint/tendermint/libs/cli" "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-db" "github.com/cosmos/cosmos-sdk/baseapp" @@ -97,7 +94,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { debug.Cmd(), ) - server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, exportAppStateAndTMValidators) + server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, createSimappAndExport) // add keybase, auxiliary RPC, query, and tx child commands rootCmd.AddCommand( @@ -191,10 +188,11 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts serverty ) } -func exportAppStateAndTMValidators( +// createSimappAndExport creates a new simapp (optionally at a given height) +// and exports state. +func createSimappAndExport( logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailAllowedAddrs []string, -) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error) { - +) (servertypes.ExportedApp, error) { encCfg := simapp.MakeEncodingConfig() // Ideally, we would reuse the one created by NewRootCmd. encCfg.Marshaler = codec.NewProtoCodec(encCfg.InterfaceRegistry) var simApp *simapp.SimApp @@ -202,7 +200,7 @@ func exportAppStateAndTMValidators( simApp = simapp.NewSimApp(logger, db, traceStore, false, map[int64]bool{}, "", uint(1), encCfg) if err := simApp.LoadHeight(height); err != nil { - return nil, nil, nil, err + return servertypes.ExportedApp{}, err } } else { simApp = simapp.NewSimApp(logger, db, traceStore, true, map[int64]bool{}, "", uint(1), encCfg) diff --git a/simapp/types.go b/simapp/types.go index dab807cd04d5..0e190af1bc93 100644 --- a/simapp/types.go +++ b/simapp/types.go @@ -1,12 +1,10 @@ package simapp import ( - "encoding/json" - abci "github.com/tendermint/tendermint/abci/types" - tmtypes "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" ) @@ -36,7 +34,7 @@ type App interface { // Exports the state of the application for a genesis file. ExportAppStateAndValidators( forZeroHeight bool, jailAllowedAddrs []string, - ) (json.RawMessage, []tmtypes.GenesisValidator, *abci.ConsensusParams, error) + ) (types.ExportedApp, error) // All the registered module account addreses. ModuleAccountAddrs() map[string]bool diff --git a/simapp/utils.go b/simapp/utils.go index 1409310a0439..2067ed54b108 100644 --- a/simapp/utils.go +++ b/simapp/utils.go @@ -79,12 +79,12 @@ func CheckExportSimulation( ) error { if config.ExportStatePath != "" { fmt.Println("exporting app state...") - appState, _, _, err := app.ExportAppStateAndValidators(false, nil) + exported, err := app.ExportAppStateAndValidators(false, nil) if err != nil { return err } - if err := ioutil.WriteFile(config.ExportStatePath, []byte(appState), 0600); err != nil { + if err := ioutil.WriteFile(config.ExportStatePath, []byte(exported.AppState), 0600); err != nil { return err } } diff --git a/store/iavl/store.go b/store/iavl/store.go index 2e389f68978b..d826c730c4ec 100644 --- a/store/iavl/store.go +++ b/store/iavl/store.go @@ -26,10 +26,11 @@ const ( ) var ( - _ types.KVStore = (*Store)(nil) - _ types.CommitStore = (*Store)(nil) - _ types.CommitKVStore = (*Store)(nil) - _ types.Queryable = (*Store)(nil) + _ types.KVStore = (*Store)(nil) + _ types.CommitStore = (*Store)(nil) + _ types.CommitKVStore = (*Store)(nil) + _ types.Queryable = (*Store)(nil) + _ types.StoreWithInitialVersion = (*Store)(nil) ) // Store Implements types.KVStore and CommitKVStore. @@ -109,7 +110,7 @@ func (st *Store) Commit() types.CommitID { } } -// Implements Committer. +// LastCommitID implements Committer. func (st *Store) LastCommitID() types.CommitID { return types.CommitID{ Version: st.tree.Version(), @@ -205,6 +206,12 @@ func (st *Store) ReverseIterator(start, end []byte) types.Iterator { return newIAVLIterator(iTree, start, end, false) } +// SetInitialVersion sets the initial version of the IAVL tree. It is used when +// starting a new chain at an arbitrary height. +func (st *Store) SetInitialVersion(version int64) { + st.tree.SetInitialVersion(uint64(version)) +} + // Handle gatest the latest height, if height is 0 func getHeight(tree Tree, req abci.RequestQuery) int64 { height := req.Height diff --git a/store/iavl/store_test.go b/store/iavl/store_test.go index 56d439c39158..052021310f99 100644 --- a/store/iavl/store_test.go +++ b/store/iavl/store_test.go @@ -526,3 +526,54 @@ func BenchmarkIAVLIteratorNext(b *testing.B) { } } } + +func TestSetInitialVersion(t *testing.T) { + testCases := []struct { + name string + storeFn func(db *dbm.MemDB) *Store + expPanic bool + }{ + { + "works with a mutable tree", + func(db *dbm.MemDB) *Store { + tree, err := iavl.NewMutableTree(db, cacheSize) + require.NoError(t, err) + store := UnsafeNewStore(tree) + + return store + }, false, + }, + { + "throws error on immutable tree", + func(db *dbm.MemDB) *Store { + tree, err := iavl.NewMutableTree(db, cacheSize) + require.NoError(t, err) + store := UnsafeNewStore(tree) + _, version, err := store.tree.SaveVersion() + require.NoError(t, err) + require.Equal(t, int64(1), version) + store, err = store.GetImmutable(1) + require.NoError(t, err) + + return store + }, true, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + db := dbm.NewMemDB() + store := tc.storeFn(db) + + if tc.expPanic { + require.Panics(t, func() { store.SetInitialVersion(5) }) + } else { + store.SetInitialVersion(5) + cid := store.Commit() + require.Equal(t, int64(5), cid.GetVersion()) + } + }) + } +} diff --git a/store/iavl/tree.go b/store/iavl/tree.go index 66e209bcdd50..83d1ada301fd 100644 --- a/store/iavl/tree.go +++ b/store/iavl/tree.go @@ -30,6 +30,7 @@ type ( GetVersioned(key []byte, version int64) (int64, []byte) GetVersionedWithProof(key []byte, version int64) ([]byte, *iavl.RangeProof, error) GetImmutable(version int64) (*iavl.ImmutableTree, error) + SetInitialVersion(version uint64) } // immutableTree is a simple wrapper around a reference to an iavl.ImmutableTree @@ -60,6 +61,10 @@ func (it *immutableTree) DeleteVersions(_ ...int64) error { panic("cannot call 'DeleteVersions' on an immutable IAVL tree") } +func (it *immutableTree) SetInitialVersion(_ uint64) { + panic("cannot call 'SetInitialVersion' on an immutable IAVL tree") +} + func (it *immutableTree) VersionExists(version int64) bool { return it.Version() == version } diff --git a/store/rootmulti/store.go b/store/rootmulti/store.go index a07c8257e6b1..bb6db32da48f 100644 --- a/store/rootmulti/store.go +++ b/store/rootmulti/store.go @@ -40,6 +40,7 @@ type Store struct { keysByName map[string]types.StoreKey lazyLoading bool pruneHeights []int64 + initialVersion int64 traceWriter io.Writer traceContext types.TraceContext @@ -298,8 +299,22 @@ func (rs *Store) LastCommitID() types.CommitID { // Commit implements Committer/CommitStore. func (rs *Store) Commit() types.CommitID { - previousHeight := rs.lastCommitInfo.Version - version := previousHeight + 1 + var previousHeight, version int64 + if rs.lastCommitInfo.GetVersion() == 0 && rs.initialVersion > 1 { + // This case means that no commit has been made in the store, we + // start from initialVersion. + version = rs.initialVersion + + } else { + // This case can means two things: + // - either there was already a previous commit in the store, in which + // case we increment the version from there, + // - or there was no previous commit, and initial version was not set, + // in which case we start at version 1. + previousHeight = rs.lastCommitInfo.GetVersion() + version = previousHeight + 1 + } + rs.lastCommitInfo = commitStores(version, rs.stores) // Determine if pruneHeight height needs to be added to the list of heights to @@ -501,6 +516,22 @@ func (rs *Store) Query(req abci.RequestQuery) abci.ResponseQuery { return res } +// SetInitialVersion sets the initial version of the IAVL tree. It is used when +// starting a new chain at an arbitrary height. +func (rs *Store) SetInitialVersion(version int64) error { + rs.initialVersion = version + + // Loop through all the stores, if it's an IAVL store, then set initial + // version on it. + for _, commitKVStore := range rs.stores { + if storeWithVersion, ok := commitKVStore.(types.StoreWithInitialVersion); ok { + storeWithVersion.SetInitialVersion(version) + } + } + + return nil +} + // parsePath expects a format like /[/] // Must start with /, subpath may be empty // Returns error if it doesn't start with / diff --git a/store/rootmulti/store_test.go b/store/rootmulti/store_test.go index 1166adcc64ab..573d198daf12 100644 --- a/store/rootmulti/store_test.go +++ b/store/rootmulti/store_test.go @@ -505,6 +505,17 @@ func TestMultiStore_PruningRestart(t *testing.T) { } } +func TestSetInitialVersion(t *testing.T) { + db := dbm.NewMemDB() + multi := newMultiStoreWithMounts(db, types.PruneNothing) + + multi.SetInitialVersion(5) + require.Equal(t, int64(5), multi.initialVersion) + + multi.Commit() + require.Equal(t, int64(5), multi.LastCommitID().Version) +} + //----------------------------------------------------------------------- // utils diff --git a/store/types/store.go b/store/types/store.go index 4ca8f442c05d..087a28de6bc3 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -165,6 +165,10 @@ type CommitMultiStore interface { // Set an inter-block (persistent) cache that maintains a mapping from // StoreKeys to CommitKVStores. SetInterBlockCache(MultiStorePersistentCache) + + // SetInitialVersion sets the initial version of the IAVL tree. It is used when + // starting a new chain at an arbitrary height. + SetInitialVersion(version int64) error } //---------subsp------------------------------- @@ -394,3 +398,11 @@ type MultiStorePersistentCache interface { // Reset the entire set of internal caches. Reset() } + +// StoreWithInitialVersion is a store that can have an arbitrary initial +// version. +type StoreWithInitialVersion interface { + // SetInitialVersion sets the initial version of the IAVL tree. It is used when + // starting a new chain at an arbitrary height. + SetInitialVersion(version int64) +} diff --git a/testutil/network/network.go b/testutil/network/network.go index 83478271cd5f..3ee11cb7a67f 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -301,8 +301,11 @@ func New(t *testing.T, cfg Config) *Network { require.NoError(t, err) memo := fmt.Sprintf("%s@%s:%s", nodeIDs[i], p2pURL.Hostname(), p2pURL.Port()) + fee := sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), sdk.NewInt(0))) txBuilder := cfg.TxConfig.NewTxBuilder() require.NoError(t, txBuilder.SetMsgs(createValMsg)) + txBuilder.SetFeeAmount(fee) // Arbitrary fee + txBuilder.SetGasLimit(1000000) // Need at least 100386 txBuilder.SetMemo(memo) txFactory := tx.Factory{} diff --git a/x/auth/ante/sigverify.go b/x/auth/ante/sigverify.go index fc783c6b8523..e1d11ebc9b5e 100644 --- a/x/auth/ante/sigverify.go +++ b/x/auth/ante/sigverify.go @@ -232,7 +232,7 @@ func (svd SigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simul if err != nil { return ctx, sdkerrors.Wrapf( sdkerrors.ErrUnauthorized, - "signature verification failed; please verify account number (%d) and chain-id (%s)", acc.GetAccountNumber(), ctx.ChainID()) + "signature verification failed; please verify account number (%d) and chain-id (%s)", accNum, chainID) } } }