diff --git a/CHANGELOG.md b/CHANGELOG.md index 4162b49a10..0b299f5d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +* Add StoreLoader wrapper to check configuration settings [#1792](https://github.com/provenance-io/provenance/pull/1792). * Create a default market in `make run`, `localnet`, `devnet` and the `provenanced testnet` command [#1757](https://github.com/provenance-io/provenance/issues/1757). * Updated documentation for each module to work with docusaurus [PR 1763](https://github.com/provenance-io/provenance/pull/1763) diff --git a/app/app.go b/app/app.go index c84a43edee..66a03bbcd8 100644 --- a/app/app.go +++ b/app/app.go @@ -1039,6 +1039,7 @@ func New( } // Currently in an upgrade hold for this block. + var storeLoader baseapp.StoreLoader if upgradeInfo.Name != "" && upgradeInfo.Height == app.LastBlockHeight()+1 { if app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { app.Logger().Info("Skipping upgrade based on height", @@ -1053,14 +1054,15 @@ func New( "lastHeight", app.LastBlockHeight(), ) // See if we have a custom store loader to use for upgrades. - storeLoader := GetUpgradeStoreLoader(app, upgradeInfo) - if storeLoader != nil { - app.SetStoreLoader(storeLoader) - } + storeLoader = GetUpgradeStoreLoader(app, upgradeInfo) } } // -- + // Verify configuration settings + storeLoader = ValidateWrapper(app.Logger(), appOpts, storeLoader) + app.SetStoreLoader(storeLoader) + if loadLatest { if err := app.LoadLatestVersion(); err != nil { tmos.Exit(err.Error()) diff --git a/app/store_loader.go b/app/store_loader.go new file mode 100644 index 0000000000..d2f878bd86 --- /dev/null +++ b/app/store_loader.go @@ -0,0 +1,75 @@ +package app + +import ( + "errors" + "fmt" + "time" + + "github.com/spf13/cast" + + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// StoreLoaderWrapper is a wrapper function that is called before the StoreLoader. +type StoreLoaderWrapper func(sdk.CommitMultiStore, baseapp.StoreLoader) error + +// WrapStoreLoader creates a new StoreLoader by wrapping an existing one. +func WrapStoreLoader(wrapper StoreLoaderWrapper, storeLoader baseapp.StoreLoader) baseapp.StoreLoader { + return func(ms sdk.CommitMultiStore) error { + if storeLoader == nil { + storeLoader = baseapp.DefaultStoreLoader + } + + if wrapper == nil { + return errors.New("wrapper must not be nil") + } + + return wrapper(ms, storeLoader) + } +} + +// ValidateWrapper creates a new StoreLoader that first checks the config settings before calling the provided StoreLoader. +func ValidateWrapper(logger log.Logger, appOpts servertypes.AppOptions, storeLoader baseapp.StoreLoader) baseapp.StoreLoader { + return WrapStoreLoader(func(ms sdk.CommitMultiStore, sl baseapp.StoreLoader) error { + const MaxPruningInterval = 999 + const SleepSeconds = 30 + backend := server.GetAppDBBackend(appOpts) + interval := cast.ToUint64(appOpts.Get("pruning-interval")) + txIndexer := cast.ToStringMap(appOpts.Get("tx_index")) + indexer := cast.ToString(txIndexer["indexer"]) + fastNode := cast.ToBool(appOpts.Get("iavl-disable-fastnode")) + var errs []string + + if interval > MaxPruningInterval { + errs = append(errs, fmt.Sprintf("pruning-interval %d EXCEEDS %d AND IS NOT RECOMMENDED, AS IT CAN LEAD TO MISSED BLOCKS ON VALIDATORS", interval, MaxPruningInterval)) + } + + if indexer != "" { + errs = append(errs, fmt.Sprintf("indexer \"%s\" IS NOT RECOMMENDED, AND IT IS RECOMMENDED TO USE \"%s\"", indexer, "")) + } + + if fastNode { + errs = append(errs, fmt.Sprintf("iavl-disable-fastnode \"%v\" IS NOT RECOMMENDED, AND IT IS RECOMMENDED TO USE \"%v\"", fastNode, !fastNode)) + } + + if backend != dbm.GoLevelDBBackend { + errs = append(errs, fmt.Sprintf("%s IS NO LONGER SUPPORTED. MIGRATE TO %s", backend, dbm.GoLevelDBBackend)) + } + + if len(errs) > 0 { + logger.Error(fmt.Sprintf("NODE WILL CONTINUE AFTER %d SECONDS", SleepSeconds)) + for _, err := range errs { + logger.Error(err) + } + time.Sleep(SleepSeconds * time.Second) + } + + return sl(ms) + }, storeLoader) +} diff --git a/app/store_loader_test.go b/app/store_loader_test.go new file mode 100644 index 0000000000..f8a6853a80 --- /dev/null +++ b/app/store_loader_test.go @@ -0,0 +1,196 @@ +package app + +import ( + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/store/rootmulti" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" +) + +func TestWrapStoreLoader(t *testing.T) { + var flag bool + tests := []struct { + name string + storeLoader baseapp.StoreLoader + wrapper StoreLoaderWrapper + err string + }{ + { + name: "nil store loader is set with valid value", + storeLoader: nil, + wrapper: createMockStoreWrapper(&flag), + }, + { + name: "nil wrapper is handled", + storeLoader: createMockStoreLoader(), + wrapper: nil, + err: "wrapper must not be nil", + }, + { + name: "contents of wrapper are called", + storeLoader: createMockStoreLoader(), + wrapper: createMockFlipWrapper(&flag), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + storeLoader := WrapStoreLoader(tc.wrapper, tc.storeLoader) + db := dbm.MemDB{} + ms := rootmulti.NewStore(&db, nil) + assert.NotNil(t, ms, "should create a new multistore for testing") + flag = false + + err := storeLoader(ms) + if len(tc.err) > 0 { + assert.EqualError(t, err, tc.err, "should have correct error") + assert.False(t, flag, "wrapper should not be executed") + } else { + assert.NoError(t, err, "should not return an error on success") + assert.True(t, flag, "wrapper should execute and have correct logic") + } + + }) + } +} + +func TestValidateWrapper(t *testing.T) { + tests := []struct { + name string + appOpts MockAppOptions + delta uint64 + }{ + { + name: "recommended pruning, indexer, db, and fastnode should not wait", + appOpts: MockAppOptions{ + pruning: "13", + db: "goleveldb", + fastNode: "false", + indexer: "", + }, + delta: 0, + }, + { + name: "non-recommended pruning should wait", + appOpts: MockAppOptions{ + pruning: "1000", + db: "goleveldb", + fastNode: "false", + indexer: "", + }, + delta: 30, + }, + { + name: "non-recommended indexer should wait", + appOpts: MockAppOptions{ + pruning: "13", + db: "goleveldb", + fastNode: "false", + indexer: "kv", + }, + delta: 30, + }, + { + name: "non-recommended db should wait", + appOpts: MockAppOptions{ + pruning: "13", + db: "cleveldb", + fastNode: "false", + indexer: "", + }, + delta: 30, + }, + { + name: "non-recommended fastnode should wait", + appOpts: MockAppOptions{ + pruning: "13", + db: "goleveldb", + fastNode: "true", + indexer: "", + }, + delta: 30, + }, + { + name: "multiple non-recommended should wait", + appOpts: MockAppOptions{ + pruning: "1000", + db: "cleveldb", + fastNode: "true", + indexer: "kv", + }, + delta: 30, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := log.NewNopLogger() + storeLoader := ValidateWrapper(logger, tc.appOpts, createMockStoreLoader()) + db := dbm.MemDB{} + ms := rootmulti.NewStore(&db, nil) + assert.NotNil(t, ms, "should create a new multistore for testing") + + start := time.Now() + err := storeLoader(ms) + delta := uint64(time.Now().Sub(start).Seconds()) + assert.NoError(t, err, "should not throw error") + assert.GreaterOrEqual(t, delta, tc.delta, "should wait correct amount of time") + }) + } +} + +// createMockStoreLoader creates an empty StoreLoader. +func createMockStoreLoader() baseapp.StoreLoader { + return func(ms sdk.CommitMultiStore) error { + return nil + } +} + +// createMockFlipWrapper creates a wrapper that has logic to flip a bit. +func createMockFlipWrapper(flag *bool) StoreLoaderWrapper { + return func(cms sdk.CommitMultiStore, sl baseapp.StoreLoader) error { + *flag = !(*flag) + return nil + } +} + +// createMockStoreWrapper creates a wrapper that checks if the StoreLoader is nil and sets the flag accordingly. +func createMockStoreWrapper(flag *bool) StoreLoaderWrapper { + return func(cms sdk.CommitMultiStore, sl baseapp.StoreLoader) error { + *flag = sl != nil + return nil + } +} + +// MockAppOptions is a mocked version of AppOpts that allows the developer to provide the pruning attribute. +type MockAppOptions struct { + pruning string + indexer string + db string + fastNode string +} + +// Get returns the value for the provided option. +func (m MockAppOptions) Get(opt string) interface{} { + switch opt { + case "pruning-interval": + return m.pruning + case "tx_index": + return map[string]interface{}{ + "indexer": m.indexer, + } + case "app-db-backend": + return m.db + case "db-backend": + return m.db + case "iavl-disable-fastnode": + return m.fastNode + } + + return nil +}