diff --git a/PENDING.md b/PENDING.md index 531509b0039b..059c6daa99f0 100644 --- a/PENDING.md +++ b/PENDING.md @@ -54,6 +54,8 @@ IMPROVEMENTS * SDK * \#3435 Test that store implementations do not allow nil values + * [\#556](https://github.com/cosmos/cosmos-sdk/issues/556) Increase `BaseApp` + test coverage. * Tendermint diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 950125180c85..2cc0fbc6a3fc 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -52,7 +52,6 @@ type BaseApp struct { // set upon LoadVersion or LoadLatestVersion. mainKey *sdk.KVStoreKey // Main KVStore in cms - // may be nil anteHandler sdk.AnteHandler // ante handler for fee and auth initChainer sdk.InitChainer // initialize state with validators and state blob beginBlocker sdk.BeginBlocker // logic to run before any txs @@ -61,8 +60,8 @@ type BaseApp struct { pubkeyPeerFilter sdk.PeerFilter // filter peers by public key fauxMerkleMode bool // if true, IAVL MountStores uses MountStoresDB for simulation speed. - //-------------------- - // Volatile + // -------------------- + // Volatile state // checkState is set on initialization and reset on Commit. // deliverState is set in InitChain and BeginBlock and cleared on Commit. // See methods setCheckState and setDeliverState. @@ -71,25 +70,28 @@ type BaseApp struct { voteInfos []abci.VoteInfo // absent validators from begin block // consensus params - // TODO move this in the future to baseapp param store on main store. + // TODO: Move this in the future to baseapp param store on main store. consensusParams *abci.ConsensusParams // The minimum gas prices a validator is willing to accept for processing a // transaction. This is mainly used for DoS and spam prevention. minGasPrices sdk.DecCoins - // flag for sealing + // flag for sealing options and parameters to a BaseApp sealed bool } var _ abci.Application = (*BaseApp)(nil) -// NewBaseApp returns a reference to an initialized BaseApp. +// NewBaseApp returns a reference to an initialized BaseApp. It accepts a +// variadic number of option functions, which act on the BaseApp to set +// configuration choices. // // NOTE: The db is used to store the version number for now. -// Accepts a user-defined txDecoder -// Accepts variable number of option functions, which act on the BaseApp to set configuration choices -func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp)) *BaseApp { +func NewBaseApp( + name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp), +) *BaseApp { + app := &BaseApp{ Logger: logger, name: name, @@ -103,10 +105,11 @@ func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecod for _, option := range options { option(app) } + return app } -// BaseApp Name +// Name returns the name of the BaseApp. func (app *BaseApp) Name() string { return app.name } @@ -117,7 +120,8 @@ func (app *BaseApp) SetCommitMultiStoreTracer(w io.Writer) { app.cms.WithTracer(w) } -// Mount IAVL or DB stores to the provided keys in the BaseApp multistore +// MountStores mounts all IAVL or DB stores to the provided keys in the BaseApp +// multistore. func (app *BaseApp) MountStores(keys ...*sdk.KVStoreKey) { for _, key := range keys { if !app.fauxMerkleMode { @@ -130,25 +134,28 @@ func (app *BaseApp) MountStores(keys ...*sdk.KVStoreKey) { } } -// Mount stores to the provided keys in the BaseApp multistore +// MountStoresTransient mounts transient stores to the provided keys in the +// BaseApp multistore. func (app *BaseApp) MountStoresTransient(keys ...*sdk.TransientStoreKey) { for _, key := range keys { app.MountStore(key, sdk.StoreTypeTransient) } } -// Mount a store to the provided key in the BaseApp multistore, using a specified DB +// 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) } -// Mount a store to the provided key in the BaseApp multistore, using the default 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) { app.cms.MountStoreWithDB(key, typ, nil) } -// load latest application version -// panics if called more than once on a running baseapp +// LoadLatestVersion loads the latest application version. It will panic if +// called more than once on a running BaseApp. func (app *BaseApp) LoadLatestVersion(mainKey *sdk.KVStoreKey) error { err := app.cms.LoadLatestVersion() if err != nil { @@ -157,8 +164,8 @@ func (app *BaseApp) LoadLatestVersion(mainKey *sdk.KVStoreKey) error { return app.initFromMainStore(mainKey) } -// load application version -// panics if called more than once on a running baseapp +// LoadVersion loads the BaseApp application version. It will panic if called +// more than once on a running baseapp. func (app *BaseApp) LoadVersion(version int64, mainKey *sdk.KVStoreKey) error { err := app.cms.LoadVersion(version) if err != nil { @@ -167,20 +174,18 @@ func (app *BaseApp) LoadVersion(version int64, mainKey *sdk.KVStoreKey) error { return app.initFromMainStore(mainKey) } -// the last CommitID of the multistore +// LastCommitID returns the last CommitID of the multistore. func (app *BaseApp) LastCommitID() sdk.CommitID { return app.cms.LastCommitID() } -// the last committed block height +// LastBlockHeight returns the last committed block height. func (app *BaseApp) LastBlockHeight() int64 { return app.cms.LastCommitID().Version } // initializes the remaining logic from app.cms func (app *BaseApp) initFromMainStore(mainKey *sdk.KVStoreKey) error { - - // main store should exist. mainStore := app.cms.GetKVStore(mainKey) if mainStore == nil { return errors.New("baseapp expects MultiStore with 'main' KVStore") @@ -192,23 +197,24 @@ func (app *BaseApp) initFromMainStore(mainKey *sdk.KVStoreKey) error { } app.mainKey = mainKey - // load consensus params from the main store + // Load the consensus params from the main store. If the consensus params are + // nil, it will be saved later during InitChain. + // + // TODO: assert that InitChain hasn't yet been called. consensusParamsBz := mainStore.Get(mainConsensusParamsKey) if consensusParamsBz != nil { var consensusParams = &abci.ConsensusParams{} + err := proto.Unmarshal(consensusParamsBz, consensusParams) if err != nil { panic(err) } + app.setConsensusParams(consensusParams) - } else { - // It will get saved later during InitChain. - // TODO assert that InitChain hasn't yet been called. } - // Needed for `gaiad export`, which inits from store but never calls initchain + // needed for `gaiad export`, which inits from store but never calls initchain app.setCheckState(abci.Header{}) - app.Seal() return nil @@ -218,7 +224,8 @@ func (app *BaseApp) setMinGasPrices(gasPrices sdk.DecCoins) { app.minGasPrices = gasPrices } -// NewContext returns a new Context with the correct store, the given header, and nil txBytes. +// NewContext returns a new Context with the correct store, the given header, +// and nil txBytes. func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { if isCheckTx { return sdk.NewContext(app.checkState.ms, header, true, app.Logger). @@ -228,18 +235,24 @@ func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { return sdk.NewContext(app.deliverState.ms, header, false, app.Logger) } -type state struct { - ms sdk.CacheMultiStore - ctx sdk.Context +// Router returns the router of the BaseApp. +func (app *BaseApp) Router() Router { + if app.sealed { + // We cannot return a router when the app is sealed because we can't have + // any routes modified which would cause unexpected routing behavior. + panic("Router() on sealed BaseApp") + } + return app.router } -func (st *state) CacheMultiStore() sdk.CacheMultiStore { - return st.ms.CacheMultiStore() -} +// QueryRouter returns the QueryRouter of a BaseApp. +func (app *BaseApp) QueryRouter() QueryRouter { return app.queryRouter } -func (st *state) Context() sdk.Context { - return st.ctx -} +// Seal seals a BaseApp. It prohibits any further modifications to a BaseApp. +func (app *BaseApp) Seal() { app.sealed = true } + +// IsSealed returns true if the BaseApp is sealed and false otherwise. +func (app *BaseApp) IsSealed() bool { return app.sealed } func (app *BaseApp) setCheckState(header abci.Header) { ms := app.cms.CacheMultiStore() @@ -280,11 +293,10 @@ func (app *BaseApp) getMaximumBlockGas() (maxGas uint64) { return uint64(app.consensusParams.BlockSize.MaxGas) } -//______________________________________________________________________________ - +// ---------------------------------------------------------------------------- // ABCI -// Implements ABCI +// Info implements the ABCI interface. func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo { lastCommitID := app.cms.LastCommitID() @@ -295,23 +307,23 @@ func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo { } } -// Implements ABCI +// SetOption implements the ABCI interface. func (app *BaseApp) SetOption(req abci.RequestSetOption) (res abci.ResponseSetOption) { - // TODO: Implement + // TODO: Implement! return } -// Implements ABCI -// InitChain runs the initialization logic directly on the CommitMultiStore. +// InitChain implements the ABCI interface. It runs the initialization logic +// directly on the CommitMultiStore. func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) { - // Stash the consensus params in the cms main store and memoize. + // stash the consensus params in the cms main store and memoize if req.ConsensusParams != nil { app.setConsensusParams(req.ConsensusParams) app.storeConsensusParams(req.ConsensusParams) } - // Initialize the deliver state and check state with ChainID and run initChain + // initialize the deliver state and check state with ChainID and run initChain app.setDeliverState(abci.Header{ChainID: req.ChainId}) app.setCheckState(abci.Header{ChainID: req.ChainId}) @@ -325,12 +337,12 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC res = app.initChainer(app.deliverState.ctx, req) - // NOTE: we don't commit, but BeginBlock for block 1 - // starts from this deliverState + // NOTE: We don't commit, but BeginBlock for block 1 starts from this + // deliverState. return } -// Filter peers by address / port +// FilterPeerByAddrPort filters peers by address/port. func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { if app.addrPeerFilter != nil { return app.addrPeerFilter(info) @@ -338,7 +350,7 @@ func (app *BaseApp) FilterPeerByAddrPort(info string) abci.ResponseQuery { return abci.ResponseQuery{} } -// Filter peers by public key +// FilterPeerByPubKey filters peers by a public key. func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery { if app.pubkeyPeerFilter != nil { return app.pubkeyPeerFilter(info) @@ -346,7 +358,8 @@ func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery { return abci.ResponseQuery{} } -// Splits a string path using the delimter '/'. i.e. "this/is/funny" becomes []string{"this", "is", "funny"} +// Splits a string path using the delimiter '/'. +// e.g. "this/is/funny" becomes []string{"this", "is", "funny"} func splitPath(requestPath string) (path []string) { path = strings.Split(requestPath, "/") // first element is empty string @@ -356,22 +369,26 @@ func splitPath(requestPath string) (path []string) { return path } -// Implements ABCI. -// Delegates to CommitMultiStore if it implements Queryable +// Query implements the ABCI interface. It delegates to CommitMultiStore if it +// implements Queryable. func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { path := splitPath(req.Path) if len(path) == 0 { msg := "no query path provided" return sdk.ErrUnknownRequest(msg).QueryResult() } + switch path[0] { // "/app" prefix for special application queries case "app": return handleQueryApp(app, path, req) + case "store": return handleQueryStore(app, path, req) + case "p2p": return handleQueryP2P(app, path, req) + case "custom": return handleQueryCustom(app, path, req) } @@ -383,6 +400,7 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { if len(path) >= 2 { var result sdk.Result + switch path[1] { case "simulate": txBytes := req.Data @@ -392,17 +410,18 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc } else { result = app.Simulate(tx) } + case "version": return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), Codespace: string(sdk.CodespaceRoot), Value: []byte(version.Version), } + default: result = sdk.ErrUnknownRequest(fmt.Sprintf("Unknown query: %s", path)).Result() } - // Encode with json value := codec.Cdc.MustMarshalBinaryLengthPrefixed(result) return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), @@ -410,6 +429,7 @@ func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abc Value: value, } } + msg := "Expected second parameter to be either simulate or version, neither was present" return sdk.ErrUnknownRequest(msg).QueryResult() } @@ -421,12 +441,12 @@ func handleQueryStore(app *BaseApp, path []string, req abci.RequestQuery) (res a msg := "multistore doesn't support queries" return sdk.ErrUnknownRequest(msg).QueryResult() } + req.Path = "/" + strings.Join(path[1:], "/") return queryable.Query(req) } -// nolint: unparam -func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { +func handleQueryP2P(app *BaseApp, path []string, _ abci.RequestQuery) (res abci.ResponseQuery) { // "/p2p" prefix for p2p queries if len(path) >= 4 { if path[1] == "filter" { @@ -449,23 +469,29 @@ func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abc } func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { - // path[0] should be "custom" because "/custom" prefix is required for keeper queries. - // the queryRouter routes using path[1]. For example, in the path "custom/gov/proposal", queryRouter routes using "gov" + // path[0] should be "custom" because "/custom" prefix is required for keeper + // queries. + // + // The queryRouter routes using path[1]. For example, in the path + // "custom/gov/proposal", queryRouter routes using "gov". if len(path) < 2 || path[1] == "" { return sdk.ErrUnknownRequest("No route for custom query specified").QueryResult() } + querier := app.queryRouter.Route(path[1]) if querier == nil { return sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult() } - // Cache wrap the commit-multistore for safety. + // cache wrap the commit-multistore for safety ctx := sdk.NewContext( app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger, ).WithMinGasPrices(app.minGasPrices) // Passes the rest of the path as an argument to the querier. - // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path + // + // For example, in the path "custom/gov/proposal/test", the gov querier gets + // []string{"proposal", "test"} as the path. resBytes, err := querier(ctx, path[2:], req) if err != nil { return abci.ResponseQuery{ @@ -474,6 +500,7 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res Log: err.ABCILog(), } } + return abci.ResponseQuery{ Code: uint32(sdk.CodeOK), Value: resBytes, @@ -522,15 +549,16 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg return } -// CheckTx implements ABCI -// CheckTx runs the "basic checks" to see whether or not a transaction can possibly be executed, -// first decoding, then the ante handler (which checks signatures/fees/ValidateBasic), -// then finally the route match to see whether a handler exists. CheckTx does not run the actual -// Msg handler function(s). +// CheckTx implements the ABCI interface. It runs the "basic checks" to see +// whether or not a transaction can possibly be executed, first decoding, then +// the ante handler (which checks signatures/fees/ValidateBasic), then finally +// the route match to see whether a handler exists. +// +// NOTE:CheckTx does not run the actual Msg handler function(s). func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { - // Decode the Tx. var result sdk.Result - var tx, err = app.txDecoder(txBytes) + + tx, err := app.txDecoder(txBytes) if err != nil { result = err.Result() } else { @@ -547,22 +575,17 @@ func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) { } } -// Implements ABCI +// DeliverTx implements the ABCI interface. func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { - - // Decode the Tx. - var tx, err = app.txDecoder(txBytes) var result sdk.Result + + tx, err := app.txDecoder(txBytes) if err != nil { result = err.Result() } else { result = app.runTx(runTxModeDeliver, txBytes, tx) } - // Even though the Result.Code is not OK, there are still effects, - // namely fee deductions and sequence incrementing. - - // Tell the blockchain engine (i.e. Tendermint). return abci.ResponseDeliverTx{ Code: uint32(result.Code), Codespace: string(result.Codespace), @@ -574,10 +597,10 @@ func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) { } } -// Basic validator for msgs +// validateBasicTxMsgs executes basic validator calls for messages. func validateBasicTxMsgs(msgs []sdk.Msg) sdk.Error { if msgs == nil || len(msgs) == 0 { - // TODO: probably shouldn't be ErrInternal. Maybe new ErrInvalidMessage, or ? + // TODO: Probably shouldn't be ErrInternal. Maybe ErrInvalidMessage? return sdk.ErrInternal("Tx.GetMsgs() must return at least one message in list") } @@ -598,22 +621,25 @@ func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) (ctx sdk.Con WithTxBytes(txBytes). WithVoteInfos(app.voteInfos). WithConsensusParams(app.consensusParams) + if mode == runTxModeSimulate { ctx, _ = ctx.CacheContext() } + return } -// Iterates through msgs and executes them +// runMsgs iterates through all the messages and executes them. func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (result sdk.Result) { - // accumulate results logs := make([]string, 0, len(msgs)) + var data []byte // NOTE: we just append them all (?!) var tags sdk.Tags // also just append them all var code sdk.CodeType var codespace sdk.CodespaceType + for msgIdx, msg := range msgs { - // Match route. + // match message route msgRoute := msg.Route() handler := app.router.Route(msgRoute) if handler == nil { @@ -621,20 +647,19 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re } var msgResult sdk.Result - // Skip actual execution for CheckTx + + // skip actual execution for CheckTx mode if mode != runTxModeCheck { msgResult = handler(ctx, msg) } - // NOTE: GasWanted is determined by ante handler and - // GasUsed by the GasMeter + // NOTE: GasWanted is determined by ante handler and GasUsed by the GasMeter. - // Append Data and Tags data = append(data, msgResult.Data...) tags = append(tags, sdk.MakeTag(sdk.TagAction, []byte(msg.Type()))) tags = append(tags, msgResult.Tags...) - // Stop execution and return on first failed message. + // stop execution and return on first failed message if !msgResult.IsOK() { logs = append(logs, fmt.Sprintf("Msg %d failed: %s", msgIdx, msgResult.Log)) code = msgResult.Code @@ -642,11 +667,10 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re break } - // Construct usable logs in multi-message transactions. + // construct usable logs in multi-message transactions logs = append(logs, fmt.Sprintf("Msg %d: %s", msgIdx, msgResult.Log)) } - // Set the final gas values. result = sdk.Result{ Code: code, Codespace: codespace, @@ -731,11 +755,11 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk result.GasUsed = ctx.GasMeter().GasConsumed() }() - // If BlockGasMeter() panics it will be caught by the above recover and - // return an error - in any case BlockGasMeter will consume gas past - // the limit. - // NOTE: this must exist in a separate defer function for the - // above recovery to recover from this one + // If BlockGasMeter() panics it will be caught by the above recover and will + // return an error - in any case BlockGasMeter will consume gas past the limit. + // + // NOTE: This must exist in a separate defer function for the above recovery + // to recover from this one. defer func() { if mode == runTxModeDeliver { ctx.BlockGasMeter().ConsumeGas( @@ -754,7 +778,6 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk return err.Result() } - // Execute the ante handler if one is defined. if app.anteHandler != nil { var anteCtx sdk.Context var msCache sdk.CacheMultiStore @@ -810,7 +833,7 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk return } -// EndBlock implements the ABCI application interface. +// EndBlock implements the ABCI interface. func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { if app.deliverState.ms.TracingEnabled() { app.deliverState.ms = app.deliverState.ms.ResetTraceContext().(sdk.CacheMultiStore) @@ -823,27 +846,42 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc return } -// Implements ABCI +// Commit implements the ABCI interface. func (app *BaseApp) Commit() (res abci.ResponseCommit) { header := app.deliverState.ctx.BlockHeader() - // Write the Deliver state and commit the MultiStore + // write the Deliver state and commit the MultiStore app.deliverState.ms.Write() commitID := app.cms.Commit() - // TODO: this is missing a module identifier and dumps byte array - app.Logger.Debug("Commit synced", - "commit", fmt.Sprintf("%X", commitID), - ) - // Reset the Check state to the latest committed + app.Logger.Debug("Commit synced", "commit", fmt.Sprintf("%X", commitID)) + + // Reset the Check state to the latest committed. + // // NOTE: safe because Tendermint holds a lock on the mempool for Commit. // Use the header from this latest block. app.setCheckState(header) - // Empty the Deliver state + // empty/reset the deliver state app.deliverState = nil return abci.ResponseCommit{ Data: commitID.Hash, } } + +// ---------------------------------------------------------------------------- +// State + +type state struct { + ms sdk.CacheMultiStore + ctx sdk.Context +} + +func (st *state) CacheMultiStore() sdk.CacheMultiStore { + return st.ms.CacheMultiStore() +} + +func (st *state) Context() sdk.Context { + return st.ctx +} diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index acccd1fbf6fe..74b9d272017d 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -21,14 +21,10 @@ import ( ) var ( - // make some cap keys capKey1 = sdk.NewKVStoreKey("key1") capKey2 = sdk.NewKVStoreKey("key2") ) -//------------------------------------------------------------------------------------------ -// Helpers for setup. Most tests should be able to use setupBaseApp - func defaultLogger() log.Logger { return log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") } @@ -70,9 +66,6 @@ func setupBaseApp(t *testing.T, options ...func(*BaseApp)) *BaseApp { return app } -//------------------------------------------------------------------------------------------ -// test mounting and loading stores - func TestMountStores(t *testing.T) { app := setupBaseApp(t) @@ -137,6 +130,41 @@ func TestLoadVersion(t *testing.T) { testLoadVersionHelper(t, app, int64(2), commitID2) } +func TestLoadVersionInvalid(t *testing.T) { + logger := log.NewNopLogger() + pruningOpt := SetPruning(store.PruneSyncable) + db := dbm.NewMemDB() + name := t.Name() + app := NewBaseApp(name, logger, db, nil, pruningOpt) + + capKey := sdk.NewKVStoreKey(MainStoreKey) + app.MountStores(capKey) + err := app.LoadLatestVersion(capKey) + require.Nil(t, err) + + // require error when loading an invalid version + err = app.LoadVersion(-1, capKey) + require.Error(t, err) + + header := abci.Header{Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + res := app.Commit() + commitID1 := sdk.CommitID{1, res.Data} + + // create a new app with the stores mounted under the same cap key + app = NewBaseApp(name, logger, db, nil, pruningOpt) + app.MountStores(capKey) + + // require we can load the latest version + err = app.LoadVersion(1, capKey) + require.Nil(t, err) + testLoadVersionHelper(t, app, int64(1), commitID1) + + // require error when loading an invalid version + err = app.LoadVersion(2, capKey) + require.Error(t, err) +} + func testLoadVersionHelper(t *testing.T, app *BaseApp, expectedHeight int64, expectedID sdk.CommitID) { lastHeight := app.LastBlockHeight() lastID := app.LastCommitID() @@ -157,39 +185,21 @@ func testChangeNameHelper(name string) func(*BaseApp) { } } -// Test that the app hash is static -// TODO: https://github.com/cosmos/cosmos-sdk/issues/520 -/*func TestStaticAppHash(t *testing.T) { - app := newBaseApp(t.Name()) - - // make a cap key and mount the store - capKey := sdk.NewKVStoreKey(MainStoreKey) - app.MountStores(capKey) - err := app.LoadLatestVersion(capKey) // needed to make stores non-nil - require.Nil(t, err) - - // execute some blocks - header := abci.Header{Height: 1} - app.BeginBlock(abci.RequestBeginBlock{Header: header}) - res := app.Commit() - commitID1 := sdk.CommitID{1, res.Data} - - header = abci.Header{Height: 2} - app.BeginBlock(abci.RequestBeginBlock{Header: header}) - res = app.Commit() - commitID2 := sdk.CommitID{2, res.Data} - - require.Equal(t, commitID1.Hash, commitID2.Hash) -} -*/ - -//------------------------------------------------------------------------------------------ -// test some basic abci/baseapp functionality - // Test that txs can be unmarshalled and read and that // correct error codes are returned when not func TestTxDecoder(t *testing.T) { - // TODO + codec := codec.New() + registerTestCodec(codec) + + app := newBaseApp(t.Name()) + tx := newTxCounter(1, 0) + txBytes := codec.MustMarshalBinaryLengthPrefixed(tx) + + dTx, err := app.txDecoder(txBytes) + require.NoError(t, err) + + cTx := dTx.(txTest) + require.Equal(t, tx.Counter, cTx.Counter) } // Test that Info returns the latest committed state. @@ -210,8 +220,46 @@ func TestInfo(t *testing.T) { // TODO } -//------------------------------------------------------------------------------------------ -// InitChain, BeginBlock, EndBlock +func TestBaseAppOptionSeal(t *testing.T) { + app := setupBaseApp(t) + + require.Panics(t, func() { + app.SetName("") + }) + require.Panics(t, func() { + app.SetDB(nil) + }) + require.Panics(t, func() { + app.SetCMS(nil) + }) + require.Panics(t, func() { + app.SetInitChainer(nil) + }) + require.Panics(t, func() { + app.SetBeginBlocker(nil) + }) + require.Panics(t, func() { + app.SetEndBlocker(nil) + }) + require.Panics(t, func() { + app.SetAnteHandler(nil) + }) + require.Panics(t, func() { + app.SetAddrPeerFilter(nil) + }) + require.Panics(t, func() { + app.SetPubKeyPeerFilter(nil) + }) + require.Panics(t, func() { + app.SetFauxMerkleMode() + }) +} + +func TestSetMinGasPrices(t *testing.T) { + minGasPrices := sdk.DecCoins{sdk.NewDecCoin("stake", 5000)} + app := newBaseApp(t.Name(), SetMinGasPrices(minGasPrices.String())) + require.Equal(t, minGasPrices, app.minGasPrices) +} func TestInitChainer(t *testing.T) { name := t.Name() @@ -280,11 +328,6 @@ func TestInitChainer(t *testing.T) { require.Equal(t, value, res.Value) } -//------------------------------------------------------------------------------------------ -// Mock tx, msgs, and mapper for the baseapp tests. -// Self-contained, just uses counters. -// We don't care about signatures, coins, accounts, etc. in the baseapp. - // Simple tx with a list of Msgs. type txTest struct { Msgs []sdk.Msg @@ -417,9 +460,6 @@ func handlerMsgCounter(t *testing.T, capKey *sdk.KVStoreKey, deliverKey []byte) } } -//----------------------------------------------------------------- -// simple int mapper - func i2b(i int64) []byte { return []byte{byte(i)} } @@ -686,10 +726,6 @@ func TestSimulateTx(t *testing.T) { } } -//------------------------------------------------------------------------------------------- -// Tx failure cases -// TODO: add more - func TestRunInvalidTransaction(t *testing.T) { anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { @@ -777,11 +813,6 @@ func TestTxGasLimits(t *testing.T) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasGranted)) - // NOTE/TODO/XXX: - // AnteHandlers must have their own defer/recover in order - // for the BaseApp to know how much gas was used used! - // This is because the GasMeter is created in the AnteHandler, - // but if it panics the context won't be set properly in runTx's recover ... defer func() { if r := recover(); r != nil { switch rType := r.(type) { @@ -866,11 +897,6 @@ func TestMaxBlockGasLimits(t *testing.T) { bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasGranted)) - // NOTE/TODO/XXX: - // AnteHandlers must have their own defer/recover in order - // for the BaseApp to know how much gas was used used! - // This is because the GasMeter is created in the AnteHandler, - // but if it panics the context won't be set properly in runTx's recover ... defer func() { if r := recover(); r != nil { switch rType := r.(type) { @@ -1031,3 +1057,162 @@ func TestBaseAppAnteHandler(t *testing.T) { app.EndBlock(abci.RequestEndBlock{}) app.Commit() } + +func TestGasConsumptionBadTx(t *testing.T) { + gasWanted := uint64(5) + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + newCtx = ctx.WithGasMeter(sdk.NewGasMeter(gasWanted)) + + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor) + res = sdk.ErrOutOfGas(log).Result() + res.GasWanted = gasWanted + res.GasUsed = newCtx.GasMeter().GasConsumed() + default: + panic(r) + } + } + }() + + txTest := tx.(txTest) + newCtx.GasMeter().ConsumeGas(uint64(txTest.Counter), "counter-ante") + if txTest.FailOnAnte { + return newCtx, sdk.ErrInternal("ante handler failure").Result(), true + } + + res = sdk.Result{ + GasWanted: gasWanted, + } + return + }) + } + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + count := msg.(msgCounter).Counter + ctx.GasMeter().ConsumeGas(uint64(count), "counter-handler") + return sdk.Result{} + }) + } + + cdc := codec.New() + registerTestCodec(cdc) + + app := setupBaseApp(t, anteOpt, routerOpt) + app.InitChain(abci.RequestInitChain{ + ConsensusParams: &abci.ConsensusParams{ + BlockSize: &abci.BlockSizeParams{ + MaxGas: 9, + }, + }, + }) + + app.InitChain(abci.RequestInitChain{}) + app.BeginBlock(abci.RequestBeginBlock{}) + + tx := newTxCounter(5, 0) + tx.setFailOnAnte(true) + txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx) + require.NoError(t, err) + + res := app.DeliverTx(txBytes) + require.False(t, res.IsOK(), fmt.Sprintf("%v", res)) + + // require next tx to fail due to black gas limit + tx = newTxCounter(5, 0) + txBytes, err = cdc.MarshalBinaryLengthPrefixed(tx) + require.NoError(t, err) + + res = app.DeliverTx(txBytes) + require.False(t, res.IsOK(), fmt.Sprintf("%v", res)) +} + +// Test that we can only query from the latest committed state. +func TestQuery(t *testing.T) { + key, value := []byte("hello"), []byte("goodbye") + anteOpt := func(bapp *BaseApp) { + bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { + store := ctx.KVStore(capKey1) + store.Set(key, value) + return + }) + } + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + store := ctx.KVStore(capKey1) + store.Set(key, value) + return sdk.Result{} + }) + } + + app := setupBaseApp(t, anteOpt, routerOpt) + + app.InitChain(abci.RequestInitChain{}) + + // NOTE: "/store/key1" tells us KVStore + // and the final "/key" says to use the data as the + // key in the given KVStore ... + query := abci.RequestQuery{ + Path: "/store/key1/key", + Data: key, + } + tx := newTxCounter(0, 0) + + // query is empty before we do anything + res := app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query is still empty after a CheckTx + resTx := app.Check(tx) + require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) + res = app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query is still empty after a DeliverTx before we commit + app.BeginBlock(abci.RequestBeginBlock{}) + resTx = app.Deliver(tx) + require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) + res = app.Query(query) + require.Equal(t, 0, len(res.Value)) + + // query returns correct value after Commit + app.Commit() + res = app.Query(query) + require.Equal(t, value, res.Value) +} + +// Test p2p filter queries +func TestP2PQuery(t *testing.T) { + addrPeerFilterOpt := func(bapp *BaseApp) { + bapp.SetAddrPeerFilter(func(addrport string) abci.ResponseQuery { + require.Equal(t, "1.1.1.1:8000", addrport) + return abci.ResponseQuery{Code: uint32(3)} + }) + } + + pubkeyPeerFilterOpt := func(bapp *BaseApp) { + bapp.SetPubKeyPeerFilter(func(pubkey string) abci.ResponseQuery { + require.Equal(t, "testpubkey", pubkey) + return abci.ResponseQuery{Code: uint32(4)} + }) + } + + app := setupBaseApp(t, addrPeerFilterOpt, pubkeyPeerFilterOpt) + + addrQuery := abci.RequestQuery{ + Path: "/p2p/filter/addr/1.1.1.1:8000", + } + res := app.Query(addrQuery) + require.Equal(t, uint32(3), res.Code) + + pubkeyQuery := abci.RequestQuery{ + Path: "/p2p/filter/pubkey/testpubkey", + } + res = app.Query(pubkeyQuery) + require.Equal(t, uint32(4), res.Code) +} diff --git a/baseapp/helpers.go b/baseapp/helpers.go index 0ac99887a3bb..a58440642ff1 100644 --- a/baseapp/helpers.go +++ b/baseapp/helpers.go @@ -1,13 +1,13 @@ package baseapp import ( - "github.com/tendermint/tendermint/abci/server" - abci "github.com/tendermint/tendermint/abci/types" - cmn "github.com/tendermint/tendermint/libs/common" + "regexp" sdk "github.com/cosmos/cosmos-sdk/types" ) +var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString + // nolint - Mostly for testing func (app *BaseApp) Check(tx sdk.Tx) (result sdk.Result) { return app.runTx(runTxModeCheck, nil, tx) @@ -22,28 +22,3 @@ func (app *BaseApp) Simulate(tx sdk.Tx) (result sdk.Result) { func (app *BaseApp) Deliver(tx sdk.Tx) (result sdk.Result) { return app.runTx(runTxModeDeliver, nil, tx) } - -// RunForever BasecoinApp execution and cleanup -func RunForever(app abci.Application) { - - // Start the ABCI server - srv, err := server.NewServer("0.0.0.0:26658", "socket", app) - if err != nil { - cmn.Exit(err.Error()) - return - } - err = srv.Start() - if err != nil { - cmn.Exit(err.Error()) - return - } - - // Wait forever - cmn.TrapSignal(func() { - // Cleanup - err := srv.Stop() - if err != nil { - cmn.Exit(err.Error()) - } - }) -} diff --git a/baseapp/options.go b/baseapp/options.go index 8e6687335a3f..5f44bf1b5293 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -18,7 +18,7 @@ func SetPruning(opts sdk.PruningOptions) func(*BaseApp) { return func(bap *BaseApp) { bap.cms.SetPruning(opts) } } -// SetMinimumGasPrices returns an option that sets the minimum gas prices on the app. +// SetMinGasPrices returns an option that sets the minimum gas prices on the app. func SetMinGasPrices(gasPricesStr string) func(*BaseApp) { gasPrices, err := sdk.ParseDecCoins(gasPricesStr) if err != nil { @@ -97,25 +97,3 @@ func (app *BaseApp) SetFauxMerkleMode() { } app.fauxMerkleMode = true } - -//---------------------------------------- -// TODO: move these out of this file? - -func (app *BaseApp) Router() Router { - if app.sealed { - panic("Router() on sealed BaseApp") - } - return app.router -} - -func (app *BaseApp) QueryRouter() QueryRouter { - return app.queryRouter -} - -func (app *BaseApp) Seal() { app.sealed = true } -func (app *BaseApp) IsSealed() bool { return app.sealed } -func (app *BaseApp) enforceSeal() { - if !app.sealed { - panic("enforceSeal() on BaseApp but not sealed") - } -} diff --git a/baseapp/query.go b/baseapp/query.go deleted file mode 100644 index 6527fda4f341..000000000000 --- a/baseapp/query.go +++ /dev/null @@ -1,54 +0,0 @@ -package baseapp - -/* - XXX Make this work with MultiStore. - XXX It will require some interfaces updates in store/types.go. - - if len(reqQuery.Data) == 0 { - resQuery.Log = "Query cannot be zero length" - resQuery.Code = abci.CodeType_EncodingError - return - } - - // set the query response height to current - tree := app.state.Committed() - - height := reqQuery.Height - if height == 0 { - // TODO: once the rpc actually passes in non-zero - // heights we can use to query right after a tx - // we must retrun most recent, even if apphash - // is not yet in the blockchain - - withProof := app.CommittedHeight() - 1 - if tree.Tree.VersionExists(withProof) { - height = withProof - } else { - height = app.CommittedHeight() - } - } - resQuery.Height = height - - switch reqQuery.Path { - case "/store", "/key": // Get by key - key := reqQuery.Data // Data holds the key bytes - resQuery.Key = key - if reqQuery.Prove { - value, proof, err := tree.GetVersionedWithProof(key, height) - if err != nil { - resQuery.Log = err.Error() - break - } - resQuery.Value = value - resQuery.Proof = proof.Bytes() - } else { - value := tree.Get(key) - resQuery.Value = value - } - - default: - resQuery.Code = abci.CodeType_UnknownRequest - resQuery.Log = cmn.Fmt("Unexpected Query path: %v", reqQuery.Path) - } - return -*/ diff --git a/baseapp/query_test.go b/baseapp/query_test.go deleted file mode 100644 index fed7ae47732f..000000000000 --- a/baseapp/query_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package baseapp - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// Test that we can only query from the latest committed state. -func TestQuery(t *testing.T) { - key, value := []byte("hello"), []byte("goodbye") - anteOpt := func(bapp *BaseApp) { - bapp.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) { - store := ctx.KVStore(capKey1) - store.Set(key, value) - return - }) - } - - routerOpt := func(bapp *BaseApp) { - bapp.Router().AddRoute(routeMsgCounter, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { - store := ctx.KVStore(capKey1) - store.Set(key, value) - return sdk.Result{} - }) - } - - app := setupBaseApp(t, anteOpt, routerOpt) - - app.InitChain(abci.RequestInitChain{}) - - // NOTE: "/store/key1" tells us KVStore - // and the final "/key" says to use the data as the - // key in the given KVStore ... - query := abci.RequestQuery{ - Path: "/store/key1/key", - Data: key, - } - tx := newTxCounter(0, 0) - - // query is empty before we do anything - res := app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query is still empty after a CheckTx - resTx := app.Check(tx) - require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) - res = app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query is still empty after a DeliverTx before we commit - app.BeginBlock(abci.RequestBeginBlock{}) - resTx = app.Deliver(tx) - require.True(t, resTx.IsOK(), fmt.Sprintf("%v", resTx)) - res = app.Query(query) - require.Equal(t, 0, len(res.Value)) - - // query returns correct value after Commit - app.Commit() - res = app.Query(query) - require.Equal(t, value, res.Value) -} - -// Test p2p filter queries -func TestP2PQuery(t *testing.T) { - addrPeerFilterOpt := func(bapp *BaseApp) { - bapp.SetAddrPeerFilter(func(addrport string) abci.ResponseQuery { - require.Equal(t, "1.1.1.1:8000", addrport) - return abci.ResponseQuery{Code: uint32(3)} - }) - } - - pubkeyPeerFilterOpt := func(bapp *BaseApp) { - bapp.SetPubKeyPeerFilter(func(pubkey string) abci.ResponseQuery { - require.Equal(t, "testpubkey", pubkey) - return abci.ResponseQuery{Code: uint32(4)} - }) - } - - app := setupBaseApp(t, addrPeerFilterOpt, pubkeyPeerFilterOpt) - - addrQuery := abci.RequestQuery{ - Path: "/p2p/filter/addr/1.1.1.1:8000", - } - res := app.Query(addrQuery) - require.Equal(t, uint32(3), res.Code) - - pubkeyQuery := abci.RequestQuery{ - Path: "/p2p/filter/pubkey/testpubkey", - } - res = app.Query(pubkeyQuery) - require.Equal(t, uint32(4), res.Code) -} diff --git a/baseapp/queryrouter.go b/baseapp/queryrouter.go index 23cfad07218c..178646b7ebb5 100644 --- a/baseapp/queryrouter.go +++ b/baseapp/queryrouter.go @@ -1,6 +1,8 @@ package baseapp import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -10,32 +12,34 @@ type QueryRouter interface { Route(path string) (h sdk.Querier) } -type queryrouter struct { +type queryRouter struct { routes map[string]sdk.Querier } -// nolint -// NewRouter - create new router -// TODO either make Function unexported or make return type (router) Exported -func NewQueryRouter() *queryrouter { - return &queryrouter{ +// NewQueryRouter returns a reference to a new queryRouter. +// +// TODO: Either make the function private or make return type (queryRouter) public. +func NewQueryRouter() *queryRouter { // nolint: golint + return &queryRouter{ routes: map[string]sdk.Querier{}, } } -// AddRoute - Adds an sdk.Querier to the route provided. Panics on duplicate -func (rtr *queryrouter) AddRoute(r string, q sdk.Querier) QueryRouter { - if !isAlphaNumeric(r) { +// AddRoute adds a query path to the router with a given Querier. It will panic +// if a duplicate route is given. The route must be alphanumeric. +func (qrt *queryRouter) AddRoute(path string, q sdk.Querier) QueryRouter { + if !isAlphaNumeric(path) { panic("route expressions can only contain alphanumeric characters") } - if rtr.routes[r] != nil { - panic("route has already been initialized") + if qrt.routes[path] != nil { + panic(fmt.Sprintf("route %s has already been initialized", path)) } - rtr.routes[r] = q - return rtr + + qrt.routes[path] = q + return qrt } -// Returns the sdk.Querier for a certain route path -func (rtr *queryrouter) Route(path string) (h sdk.Querier) { - return rtr.routes[path] +// Route returns the Querier for a given query route path. +func (qrt *queryRouter) Route(path string) sdk.Querier { + return qrt.routes[path] } diff --git a/baseapp/queryrouter_test.go b/baseapp/queryrouter_test.go new file mode 100644 index 000000000000..7743028313cc --- /dev/null +++ b/baseapp/queryrouter_test.go @@ -0,0 +1,33 @@ +package baseapp + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var testQuerier = func(_ sdk.Context, _ []string, _ abci.RequestQuery) (res []byte, err sdk.Error) { + return nil, nil +} + +func TestQueryRouter(t *testing.T) { + qr := NewQueryRouter() + + // require panic on invalid route + require.Panics(t, func() { + qr.AddRoute("*", testQuerier) + }) + + qr.AddRoute("testRoute", testQuerier) + q := qr.Route("testRoute") + require.NotNil(t, q) + + // require panic on duplicate route + require.Panics(t, func() { + qr.AddRoute("testRoute", testQuerier) + }) +} diff --git a/baseapp/router.go b/baseapp/router.go index 4be3aec74ae4..5e829d73cfa6 100644 --- a/baseapp/router.go +++ b/baseapp/router.go @@ -1,7 +1,7 @@ package baseapp import ( - "regexp" + "fmt" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -12,44 +12,36 @@ type Router interface { Route(path string) (h sdk.Handler) } -// map a transaction type to a handler and an initgenesis function -type route struct { - r string - h sdk.Handler -} - type router struct { - routes []route + routes map[string]sdk.Handler } -// nolint -// NewRouter - create new router -// TODO either make Function unexported or make return type (router) Exported -func NewRouter() *router { +// NewRouter returns a reference to a new router. +// +// TODO: Either make the function private or make return type (router) public. +func NewRouter() *router { // nolint: golint return &router{ - routes: make([]route, 0), + routes: make(map[string]sdk.Handler), } } -var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString - -// AddRoute - TODO add description -func (rtr *router) AddRoute(r string, h sdk.Handler) Router { - if !isAlphaNumeric(r) { +// AddRoute adds a route path to the router with a given handler. The route must +// be alphanumeric. +func (rtr *router) AddRoute(path string, h sdk.Handler) Router { + if !isAlphaNumeric(path) { panic("route expressions can only contain alphanumeric characters") } - rtr.routes = append(rtr.routes, route{r, h}) + if rtr.routes[path] != nil { + panic(fmt.Sprintf("route %s has already been initialized", path)) + } + rtr.routes[path] = h return rtr } -// Route - TODO add description -// TODO handle expressive matches. -func (rtr *router) Route(path string) (h sdk.Handler) { - for _, route := range rtr.routes { - if route.r == path { - return route.h - } - } - return nil +// Route returns a handler for a given route path. +// +// TODO: Handle expressive matches. +func (rtr *router) Route(path string) sdk.Handler { + return rtr.routes[path] } diff --git a/baseapp/router_test.go b/baseapp/router_test.go new file mode 100644 index 000000000000..86e09cf21b79 --- /dev/null +++ b/baseapp/router_test.go @@ -0,0 +1,31 @@ +package baseapp + +import ( + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var testHandler = func(_ sdk.Context, _ sdk.Msg) sdk.Result { + return sdk.Result{} +} + +func TestRouter(t *testing.T) { + rtr := NewRouter() + + // require panic on invalid route + require.Panics(t, func() { + rtr.AddRoute("*", testHandler) + }) + + rtr.AddRoute("testRoute", testHandler) + h := rtr.Route("testRoute") + require.NotNil(t, h) + + // require panic on duplicate route + require.Panics(t, func() { + rtr.AddRoute("testRoute", testHandler) + }) +} diff --git a/baseapp/testdata/genesis.json b/baseapp/testdata/genesis.json deleted file mode 100644 index ee8879fd288f..000000000000 --- a/baseapp/testdata/genesis.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chain_id": "foo_bar_chain", - "app_options": { - "accounts": [{ - "pub_key": { - "type": "ed25519", - "data": "6880db93598e283a67c4d88fc67a8858aa2de70f713fe94a5109e29c137100c2" - }, - "coins": [ - { - "denom": "blank", - "amount": 12345 - }, - { - "denom": "ETH", - "amount": 654321 - } - ] - }], - "plugin_options": ["plugin1/key1", "value1", "plugin1/key2", "value2"] - } -} diff --git a/baseapp/testdata/genesis2.json b/baseapp/testdata/genesis2.json deleted file mode 100644 index 18ec87164fd3..000000000000 --- a/baseapp/testdata/genesis2.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "chain_id": "addr_accounts_chain", - "app_options": { - "accounts": [{ - "name": "alice", - "pub_key": { - "type": "ed25519", - "data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36" - }, - "coins": [ - { - "denom": "one", - "amount": 111 - } - ] - }, { - "name": "bob", - "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", - "coins": [ - { - "denom": "two", - "amount": 222 - } - ] - }, { - "name": "sam", - "pub_key": { - "type": "secp256k1", - "data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E" - }, - "coins": [ - { - "denom": "four", - "amount": 444 - } - ] - }] - } -} diff --git a/baseapp/testdata/genesis2b.json b/baseapp/testdata/genesis2b.json deleted file mode 100644 index a880b3c64c02..000000000000 --- a/baseapp/testdata/genesis2b.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "chain_id": "addr_accounts_chain", - "app_options": { - "accounts": [{ - "name": "alice", - "pub_key": { - "type": "ed25519", - "data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36" - }, - "coins": [ - { - "denom": "one", - "amount": 111 - } - ] - }, { - "name": "bob", - "address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1", - "coins": [ - { - "denom": "two", - "amount": 222 - } - ] - }, { - "name": "carl", - "address": "1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9", - "pub_key": { - "type": "ed25519", - "data": "177C0AC45E86257F0708DC085D592AB22AAEECD1D26381B757F7C96135921858" - }, - "coins": [ - { - "denom": "three", - "amount": 333 - } - ] - }, { - "name": "sam", - "pub_key": { - "type": "secp256k1", - "data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E" - }, - "coins": [ - { - "denom": "four", - "amount": 444 - } - ] - }] - } -} diff --git a/baseapp/testdata/genesis3.json b/baseapp/testdata/genesis3.json deleted file mode 100644 index b58b1d740256..000000000000 --- a/baseapp/testdata/genesis3.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "chain_id": "foo_bar_chain" -}