refactor: Revert middlewares to antehandlers (part 1/2: baseapp) (#11979)

## Description

We decided to remove middlewares, and revert to antehandlers (exactly like in v045) for this release. A better middleware solution will be implemented after v046.

ref: #11955 

Because this refactor is big, so I decided to cut it into two. This PR is part 1 of 2:
- part 1: Revert baseapp and middlewares to v0.45.4
- part 2: Add posthandler, tips, priority

---
 Suggestion for reviewers:

This PR might still be hard to review though. I think it's easier to actually review the diff between v0.45.4 and this PR:
- `git difftool -d v0.45.4..am/revert-045-baseapp baseapp`
  - most important parts to review: runTx, runMsgs
- `git difftool -d v0.45.4..am/revert-045-baseapp x/auth/ante`
  - only cosmetic changes
- `git difftool -d v0.45.4..am/revert-045-baseapp simapp`



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Amaury
2022-05-20 09:27:27 +00:00
committed by GitHub
parent cf750b85d7
commit 01832e6239
76 changed files with 3158 additions and 4692 deletions
+27 -60
View File
@@ -17,9 +17,9 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types"
"github.com/cosmos/cosmos-sdk/telemetry"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// Supported ABCI Query prefixes
@@ -233,8 +233,8 @@ func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBloc
// CheckTx implements the ABCI interface and executes a tx in CheckTx mode. In
// CheckTx mode, messages are not executed. This means messages are only validated
// and only the wired middlewares are executed. State is persisted to the BaseApp's
// internal CheckTx state if the middlewares' CheckTx pass. Otherwise, the ResponseCheckTx
// and only the AnteHandler is executed. State is persisted to the BaseApp's
// internal CheckTx state if the AnteHandler passes. Otherwise, the ResponseCheckTx
// will contain releveant error information. Regardless of tx execution outcome,
// the ResponseCheckTx will contain relevant gas execution context.
func (app *BaseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
@@ -251,18 +251,18 @@ func (app *BaseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
panic(fmt.Sprintf("unknown RequestCheckTx type: %s", req.Type))
}
ctx := app.getContextForTx(mode, req.Tx)
res, checkRes, err := app.txHandler.CheckTx(ctx, tx.Request{TxBytes: req.Tx}, tx.RequestCheckTx{Type: req.Type})
gInfo, result, anteEvents, err := app.runTx(mode, req.Tx)
if err != nil {
return sdkerrors.ResponseCheckTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, app.trace)
}
abciRes, err := convertTxResponseToCheckTx(res, checkRes)
if err != nil {
return sdkerrors.ResponseCheckTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
return abci.ResponseCheckTx{
GasWanted: int64(gInfo.GasWanted), // TODO: Should type accept unsigned ints?
GasUsed: int64(gInfo.GasUsed), // TODO: Should type accept unsigned ints?
Log: result.Log,
Data: result.Data,
Events: sdk.MarkEventsToIndex(result.Events, app.indexEvents),
}
return abciRes
}
// DeliverTx implements the ABCI interface and executes a tx in DeliverTx mode.
@@ -271,28 +271,29 @@ func (app *BaseApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
// Regardless of tx execution outcome, the ResponseDeliverTx will contain relevant
// gas execution context.
func (app *BaseApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
var abciRes abci.ResponseDeliverTx
gInfo := sdk.GasInfo{}
resultStr := "successful"
defer func() {
for _, streamingListener := range app.abciListeners {
if err := streamingListener.ListenDeliverTx(app.deliverState.ctx, req, abciRes); err != nil {
app.logger.Error("DeliverTx listening hook failed", "err", err)
}
}
telemetry.IncrCounter(1, "tx", "count")
telemetry.IncrCounter(1, "tx", resultStr)
telemetry.SetGauge(float32(gInfo.GasUsed), "tx", "gas", "used")
telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted")
}()
ctx := app.getContextForTx(runTxModeDeliver, req.Tx)
res, err := app.txHandler.DeliverTx(ctx, tx.Request{TxBytes: req.Tx})
gInfo, result, anteEvents, err := app.runTx(runTxModeDeliver, req.Tx)
if err != nil {
abciRes = sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
return abciRes
resultStr = "failed"
return sdkerrors.ResponseDeliverTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, app.trace)
}
abciRes, err = convertTxResponseToDeliverTx(res)
if err != nil {
return sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
return abci.ResponseDeliverTx{
GasWanted: int64(gInfo.GasWanted), // TODO: Should type accept unsigned ints?
GasUsed: int64(gInfo.GasUsed), // TODO: Should type accept unsigned ints?
Log: result.Log,
Data: result.Data,
Events: sdk.MarkEventsToIndex(result.Events, app.indexEvents),
}
return abciRes
}
// Commit implements the ABCI interface. It will commit all state that exists in
@@ -853,37 +854,3 @@ func SplitABCIQueryPath(requestPath string) (path []string) {
return path
}
// makeABCIData generates the Data field to be sent to ABCI Check/DeliverTx.
func makeABCIData(txRes tx.Response) ([]byte, error) {
return proto.Marshal(&sdk.TxMsgData{MsgResponses: txRes.MsgResponses})
}
// convertTxResponseToCheckTx converts a tx.Response into a abci.ResponseCheckTx.
func convertTxResponseToCheckTx(txRes tx.Response, checkRes tx.ResponseCheckTx) (abci.ResponseCheckTx, error) {
data, err := makeABCIData(txRes)
if err != nil {
return abci.ResponseCheckTx{}, nil
}
return abci.ResponseCheckTx{
Data: data,
Log: txRes.Log,
Events: txRes.Events,
Priority: checkRes.Priority,
}, nil
}
// convertTxResponseToDeliverTx converts a tx.Response into a abci.ResponseDeliverTx.
func convertTxResponseToDeliverTx(txRes tx.Response) (abci.ResponseDeliverTx, error) {
data, err := makeABCIData(txRes)
if err != nil {
return abci.ResponseDeliverTx{}, nil
}
return abci.ResponseDeliverTx{
Data: data,
Log: txRes.Log,
Events: txRes.Events,
}, nil
}
+36 -36
View File
@@ -1,14 +1,14 @@
package baseapp_test
package baseapp
import (
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmprototypes "github.com/tendermint/tendermint/proto/tendermint/types"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
pruningtypes "github.com/cosmos/cosmos-sdk/pruning/types"
"github.com/cosmos/cosmos-sdk/snapshots"
snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types"
@@ -24,81 +24,81 @@ func TestGetBlockRentionHeight(t *testing.T) {
require.NoError(t, err)
testCases := map[string]struct {
bapp *baseapp.BaseApp
bapp *BaseApp
maxAgeBlocks int64
commitHeight int64
expected int64
}{
"defaults": {
bapp: baseapp.NewBaseApp(name, logger, db),
bapp: NewBaseApp(name, logger, db, nil),
maxAgeBlocks: 0,
commitHeight: 499000,
expected: 0,
},
"pruning unbonding time only": {
bapp: baseapp.NewBaseApp(name, logger, db, baseapp.SetMinRetainBlocks(1)),
bapp: NewBaseApp(name, logger, db, nil, SetMinRetainBlocks(1)),
maxAgeBlocks: 362880,
commitHeight: 499000,
expected: 136120,
},
"pruning iavl snapshot only": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetPruning(pruningtypes.NewPruningOptions(pruningtypes.PruningNothing)),
baseapp.SetMinRetainBlocks(1),
baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(10000, 1)),
bapp: NewBaseApp(
name, logger, db, nil,
SetPruning(pruningtypes.NewPruningOptions(pruningtypes.PruningNothing)),
SetMinRetainBlocks(1),
SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(10000, 1)),
),
maxAgeBlocks: 0,
commitHeight: 499000,
expected: 489000,
},
"pruning state sync snapshot only": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
baseapp.SetMinRetainBlocks(1),
bapp: NewBaseApp(
name, logger, db, nil,
SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
SetMinRetainBlocks(1),
),
maxAgeBlocks: 0,
commitHeight: 499000,
expected: 349000,
},
"pruning min retention only": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetMinRetainBlocks(400000),
bapp: NewBaseApp(
name, logger, db, nil,
SetMinRetainBlocks(400000),
),
maxAgeBlocks: 0,
commitHeight: 499000,
expected: 99000,
},
"pruning all conditions": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
baseapp.SetMinRetainBlocks(400000),
baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
bapp: NewBaseApp(
name, logger, db, nil,
SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
SetMinRetainBlocks(400000),
SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
),
maxAgeBlocks: 362880,
commitHeight: 499000,
expected: 99000,
},
"no pruning due to no persisted state": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
baseapp.SetMinRetainBlocks(400000),
baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
bapp: NewBaseApp(
name, logger, db, nil,
SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
SetMinRetainBlocks(400000),
SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
),
maxAgeBlocks: 362880,
commitHeight: 10000,
expected: 0,
},
"disable pruning": {
bapp: baseapp.NewBaseApp(
name, logger, db,
baseapp.SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
baseapp.SetMinRetainBlocks(0),
baseapp.SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
bapp: NewBaseApp(
name, logger, db, nil,
SetPruning(pruningtypes.NewCustomPruningOptions(0, 0)),
SetMinRetainBlocks(0),
SetSnapshot(snapshotStore, snapshottypes.NewSnapshotOptions(50000, 3)),
),
maxAgeBlocks: 362880,
commitHeight: 499000,
@@ -134,12 +134,12 @@ func TestBaseAppCreateQueryContext(t *testing.T) {
logger := defaultLogger()
db := dbm.NewMemDB()
name := t.Name()
app := baseapp.NewBaseApp(name, logger, db)
app := NewBaseApp(name, logger, db, nil)
app.BeginBlock(abci.RequestBeginBlock{Header: tmprototypes.Header{Height: 1}})
app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: 1}})
app.Commit()
app.BeginBlock(abci.RequestBeginBlock{Header: tmprototypes.Header{Height: 2}})
app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: 2}})
app.Commit()
testCases := []struct {
@@ -156,7 +156,7 @@ func TestBaseAppCreateQueryContext(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := app.CreateQueryContext(tc.height, tc.prove)
_, err := app.createQueryContext(tc.height, tc.prove)
if tc.expErr {
require.Error(t, err)
} else {
+304 -14
View File
@@ -1,21 +1,25 @@
package baseapp
import (
"context"
"fmt"
"strings"
"github.com/gogo/protobuf/proto"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/codec/types"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/snapshots"
"github.com/cosmos/cosmos-sdk/store"
"github.com/cosmos/cosmos-sdk/store/rootmulti"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx"
)
const (
@@ -46,11 +50,14 @@ type BaseApp struct { // nolint: maligned
db dbm.DB // common DB backend
cms sdk.CommitMultiStore // Main (uncached) state
storeLoader StoreLoader // function to handle store loading, may be overridden with SetStoreLoader()
router sdk.Router // handle any kind of legacy message
queryRouter sdk.QueryRouter // router for redirecting query calls
grpcQueryRouter *GRPCQueryRouter // router for redirecting gRPC query calls
msgServiceRouter *MsgServiceRouter // router for redirecting Msg service messages
interfaceRegistry types.InterfaceRegistry
txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx
txHandler tx.Handler // txHandler for {Deliver,Check}Tx and simulations
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
endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes
@@ -113,6 +120,9 @@ type BaseApp struct { // nolint: maligned
// if BaseApp is passed to the upgrade keeper's NewKeeper method.
appVersion uint64
// recovery handler for app.runTx method
runTxRecoveryMiddleware recoveryMiddleware
// trace set will return full stack traces for errors in ABCI Log field
trace bool
@@ -131,17 +141,20 @@ type BaseApp struct { // nolint: maligned
//
// NOTE: The db is used to store the version number for now.
func NewBaseApp(
name string, logger log.Logger, db dbm.DB, options ...func(*BaseApp),
name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp),
) *BaseApp {
app := &BaseApp{
logger: logger,
name: name,
db: db,
cms: store.NewCommitMultiStore(db),
storeLoader: DefaultStoreLoader,
queryRouter: NewQueryRouter(),
grpcQueryRouter: NewGRPCQueryRouter(),
fauxMerkleMode: false,
logger: logger,
name: name,
db: db,
cms: store.NewCommitMultiStore(db),
storeLoader: DefaultStoreLoader,
router: NewRouter(),
queryRouter: NewQueryRouter(),
grpcQueryRouter: NewGRPCQueryRouter(),
msgServiceRouter: NewMsgServiceRouter(),
txDecoder: txDecoder,
fauxMerkleMode: false,
}
for _, option := range options {
@@ -152,6 +165,8 @@ func NewBaseApp(
app.cms.SetInterBlockCache(app.interBlockCache)
}
app.runTxRecoveryMiddleware = newDefaultRecoveryMiddleware()
return app
}
@@ -180,6 +195,9 @@ func (app *BaseApp) Trace() bool {
return app.trace
}
// MsgServiceRouter returns the MsgServiceRouter of a BaseApp.
func (app *BaseApp) MsgServiceRouter() *MsgServiceRouter { return app.msgServiceRouter }
// MountStores mounts all IAVL or DB stores to the provided keys in the BaseApp
// multistore.
func (app *BaseApp) MountStores(keys ...storetypes.StoreKey) {
@@ -344,6 +362,17 @@ func (app *BaseApp) setIndexEvents(ie []string) {
}
}
// Router returns the legacy router of the BaseApp.
func (app *BaseApp) Router() sdk.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
}
// QueryRouter returns the QueryRouter of a BaseApp.
func (app *BaseApp) QueryRouter() sdk.QueryRouter { return app.queryRouter }
@@ -410,6 +439,13 @@ func (app *BaseApp) GetConsensusParams(ctx sdk.Context) *tmproto.ConsensusParams
return cp
}
// AddRunTxRecoveryHandler adds custom app.runTx method panic handlers.
func (app *BaseApp) AddRunTxRecoveryHandler(handlers ...RecoveryHandler) {
for _, h := range handlers {
app.runTxRecoveryMiddleware = newRecoveryMiddleware(h, app.runTxRecoveryMiddleware)
}
}
// StoreConsensusParams sets the consensus parameters to the baseapp's param store.
func (app *BaseApp) StoreConsensusParams(ctx sdk.Context, cp *tmproto.ConsensusParams) {
if app.paramStore == nil {
@@ -477,6 +513,22 @@ func (app *BaseApp) validateHeight(req abci.RequestBeginBlock) error {
return nil
}
// validateBasicTxMsgs executes basic validator calls for messages.
func validateBasicTxMsgs(msgs []sdk.Msg) error {
if len(msgs) == 0 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "must contain at least one message")
}
for _, msg := range msgs {
err := msg.ValidateBasic()
if err != nil {
return err
}
}
return nil
}
// Returns the applications's deliverState if app is in runTxModeDeliver,
// otherwise it returns the application's checkstate.
func (app *BaseApp) getState(mode runTxMode) *state {
@@ -488,7 +540,7 @@ func (app *BaseApp) getState(mode runTxMode) *state {
}
// retrieve the context for the tx w/ txBytes and other memoized values.
func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) context.Context {
func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) sdk.Context {
ctx := app.getState(mode).ctx.
WithTxBytes(txBytes).
WithVoteInfos(app.voteInfos)
@@ -503,5 +555,243 @@ func (app *BaseApp) getContextForTx(mode runTxMode, txBytes []byte) context.Cont
ctx, _ = ctx.CacheContext()
}
return sdk.WrapSDKContext(ctx)
return ctx
}
// cacheTxContext returns a new context based off of the provided context with
// a branched multi-store.
func (app *BaseApp) cacheTxContext(ctx sdk.Context, txBytes []byte) (sdk.Context, sdk.CacheMultiStore) {
ms := ctx.MultiStore()
// TODO: https://github.com/cosmos/cosmos-sdk/issues/2824
msCache := ms.CacheMultiStore()
if msCache.TracingEnabled() {
msCache = msCache.SetTracingContext(
sdk.TraceContext(
map[string]interface{}{
"txHash": fmt.Sprintf("%X", tmhash.Sum(txBytes)),
},
),
).(sdk.CacheMultiStore)
}
return ctx.WithMultiStore(msCache), msCache
}
// runTx processes a transaction within a given execution mode, encoded transaction
// bytes, and the decoded transaction itself. All state transitions occur through
// a cached Context depending on the mode provided. State only gets persisted
// if all messages get executed successfully and the execution mode is DeliverTx.
// Note, gas execution info is always returned. A reference to a Result is
// returned if the tx does not run out of gas and if all the messages are valid
// and execute successfully. An error is returned otherwise.
func (app *BaseApp) runTx(mode runTxMode, txBytes []byte) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) {
// NOTE: GasWanted should be returned by the AnteHandler. GasUsed is
// determined by the GasMeter. We need access to the context to get the gas
// meter so we initialize upfront.
var gasWanted uint64
ctx := app.getContextForTx(mode, txBytes)
ms := ctx.MultiStore()
// only run the tx if there is block gas remaining
if mode == runTxModeDeliver && ctx.BlockGasMeter().IsOutOfGas() {
return gInfo, nil, nil, sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "no block gas left to run tx")
}
defer func() {
if r := recover(); r != nil {
recoveryMW := newOutOfGasRecoveryMiddleware(gasWanted, ctx, app.runTxRecoveryMiddleware)
err, result = processRecovery(r, recoveryMW), nil
}
gInfo = sdk.GasInfo{GasWanted: gasWanted, GasUsed: ctx.GasMeter().GasConsumed()}
}()
blockGasConsumed := false
// consumeBlockGas makes sure block gas is consumed at most once. It must happen after
// tx processing, and must be execute even if tx processing fails. Hence we use trick with `defer`
consumeBlockGas := func() {
if !blockGasConsumed {
blockGasConsumed = true
ctx.BlockGasMeter().ConsumeGas(
ctx.GasMeter().GasConsumedToLimit(), "block gas meter",
)
}
}
// 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.
if mode == runTxModeDeliver {
defer consumeBlockGas()
}
tx, err := app.txDecoder(txBytes)
if err != nil {
return sdk.GasInfo{}, nil, nil, err
}
msgs := tx.GetMsgs()
if err := validateBasicTxMsgs(msgs); err != nil {
return sdk.GasInfo{}, nil, nil, err
}
if app.anteHandler != nil {
var (
anteCtx sdk.Context
msCache sdk.CacheMultiStore
)
// Branch context before AnteHandler call in case it aborts.
// This is required for both CheckTx and DeliverTx.
// Ref: https://github.com/cosmos/cosmos-sdk/issues/2772
//
// NOTE: Alternatively, we could require that AnteHandler ensures that
// writes do not happen if aborted/failed. This may have some
// performance benefits, but it'll be more difficult to get right.
anteCtx, msCache = app.cacheTxContext(ctx, txBytes)
anteCtx = anteCtx.WithEventManager(sdk.NewEventManager())
newCtx, err := app.anteHandler(anteCtx, tx, mode == runTxModeSimulate)
if !newCtx.IsZero() {
// At this point, newCtx.MultiStore() is a store branch, or something else
// replaced by the AnteHandler. We want the original multistore.
//
// Also, in the case of the tx aborting, we need to track gas consumed via
// the instantiated gas meter in the AnteHandler, so we update the context
// prior to returning.
ctx = newCtx.WithMultiStore(ms)
}
events := ctx.EventManager().Events()
// GasMeter expected to be set in AnteHandler
gasWanted = ctx.GasMeter().Limit()
if err != nil {
return gInfo, nil, nil, err
}
msCache.Write()
anteEvents = events.ToABCIEvents()
}
// Create a new Context based off of the existing Context with a MultiStore branch
// in case message processing fails. At this point, the MultiStore
// is a branch of a branch.
runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes)
// Attempt to execute all messages and only update state if all messages pass
// and we're in DeliverTx. Note, runMsgs will never return a reference to a
// Result if any single message fails or does not have a registered Handler.
result, err = app.runMsgs(runMsgCtx, msgs, mode)
if err == nil && mode == runTxModeDeliver {
// When block gas exceeds, it'll panic and won't commit the cached store.
consumeBlockGas()
msCache.Write()
if len(anteEvents) > 0 {
// append the events in the order of occurrence
result.Events = append(anteEvents, result.Events...)
}
}
return gInfo, result, anteEvents, err
}
// runMsgs iterates through a list of messages and executes them with the provided
// Context and execution mode. Messages will only be executed during simulation
// and DeliverTx. An error is returned if any single message fails or if a
// Handler does not exist for a given message route. Otherwise, a reference to a
// Result is returned. The caller must not commit state if an error is returned.
func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (*sdk.Result, error) {
msgLogs := make(sdk.ABCIMessageLogs, 0, len(msgs))
events := sdk.EmptyEvents()
var msgResponses []*codectypes.Any
// NOTE: GasWanted is determined by the AnteHandler and GasUsed by the GasMeter.
for i, msg := range msgs {
// skip actual execution for (Re)CheckTx mode
if mode == runTxModeCheck || mode == runTxModeReCheck {
break
}
var (
msgResult *sdk.Result
eventMsgName string // name to use as value in event `message.action`
err error
)
if handler := app.msgServiceRouter.Handler(msg); handler != nil {
// ADR 031 request type routing
msgResult, err = handler(ctx, msg)
eventMsgName = sdk.MsgTypeURL(msg)
} else if legacyMsg, ok := msg.(legacytx.LegacyMsg); ok {
// legacy sdk.Msg routing
// Assuming that the app developer has migrated all their Msgs to
// proto messages and has registered all `Msg services`, then this
// path should never be called, because all those Msgs should be
// registered within the `msgServiceRouter` already.
msgRoute := legacyMsg.Route()
eventMsgName = legacyMsg.Type()
handler := app.router.Route(ctx, msgRoute)
if handler == nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized message route: %s; message index: %d", msgRoute, i)
}
msgResult, err = handler(ctx, msg)
} else {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg)
}
if err != nil {
return nil, sdkerrors.Wrapf(err, "failed to execute message; message index: %d", i)
}
msgEvents := sdk.Events{
sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyAction, eventMsgName)),
}
msgEvents = msgEvents.AppendEvents(msgResult.GetEvents())
// append message events, data and logs
//
// Note: Each message result's data must be length-prefixed in order to
// separate each result.
events = events.AppendEvents(msgEvents)
// Each individual sdk.Result that went through the MsgServiceRouter
// (which should represent 99% of the Msgs now, since everyone should
// be using protobuf Msgs) has exactly one Msg response, set inside
// `WrapServiceResult`. We take that Msg response, and aggregate it
// into an array.
if len(msgResult.MsgResponses) > 0 {
msgResponse := msgResult.MsgResponses[0]
if msgResponse == nil {
return nil, sdkerrors.ErrLogic.Wrapf("got nil Msg response at index %d for msg %s", i, sdk.MsgTypeURL(msg))
}
msgResponses = append(msgResponses, msgResponse)
}
msgLogs = append(msgLogs, sdk.NewABCIMessageLog(uint32(i), msgResult.Log, msgEvents))
}
data, err := makeABCIData(msgResponses)
if err != nil {
return nil, sdkerrors.Wrap(err, "failed to marshal tx data")
}
return &sdk.Result{
Data: data,
Log: strings.TrimSpace(msgLogs.String()),
Events: events.ToABCIEvents(),
MsgResponses: msgResponses,
}, nil
}
// makeABCIData generates the Data field to be sent to ABCI Check/DeliverTx.
func makeABCIData(msgResponses []*codectypes.Any) ([]byte, error) {
return proto.Marshal(&sdk.TxMsgData{MsgResponses: msgResponses})
}
+381 -685
View File
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
package baseapp_test
import (
"fmt"
"math"
"testing"
"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"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
txtypes "github.com/cosmos/cosmos-sdk/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
)
var blockMaxGas = uint64(simapp.DefaultConsensusParams.Block.MaxGas)
func TestBaseApp_BlockGas(t *testing.T) {
testcases := []struct {
name string
gasToConsume uint64 // gas to consume in the msg execution
panicTx bool // panic explicitly in tx execution
expErr bool
}{
{"less than block gas meter", 10, false, false},
{"more than block gas meter", blockMaxGas, false, true},
{"more than block gas meter", uint64(float64(blockMaxGas) * 1.2), false, true},
{"consume MaxUint64", math.MaxUint64, false, true},
{"consume MaxGasWanted", txtypes.MaxGasWanted, false, true},
{"consume block gas when paniced", 10, true, true},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var app *simapp.SimApp
routerOpt := func(bapp *baseapp.BaseApp) {
route := (&testdata.TestMsg{}).Route()
bapp.Router().AddRoute(sdk.NewRoute(route, func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
_, ok := msg.(*testdata.TestMsg)
if !ok {
return &sdk.Result{}, fmt.Errorf("Wrong Msg type, expected %T, got %T", (*testdata.TestMsg)(nil), msg)
}
ctx.KVStore(app.GetKey(banktypes.ModuleName)).Set([]byte("ok"), []byte("ok"))
ctx.GasMeter().ConsumeGas(tc.gasToConsume, "TestMsg")
if tc.panicTx {
panic("panic in tx execution")
}
return &sdk.Result{}, nil
}))
}
encCfg := simapp.MakeTestEncodingConfig()
encCfg.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil)
encCfg.InterfaceRegistry.RegisterImplementations((*sdk.Msg)(nil),
&testdata.TestMsg{},
)
app = simapp.NewSimApp(log.NewNopLogger(), dbm.NewMemDB(), nil, true, map[int64]bool{}, "", 0, encCfg, simapp.EmptyAppOptions{}, routerOpt)
genState := simapp.GenesisStateWithSingleValidator(t, app)
stateBytes, err := tmjson.MarshalIndent(genState, "", " ")
require.NoError(t, err)
app.InitChain(abci.RequestInitChain{
Validators: []abci.ValidatorUpdate{},
ConsensusParams: simapp.DefaultConsensusParams,
AppStateBytes: stateBytes,
})
ctx := app.NewContext(false, tmproto.Header{})
// tx fee
feeCoin := sdk.NewCoin("atom", sdk.NewInt(150))
feeAmount := sdk.NewCoins(feeCoin)
// test account and fund
priv1, _, addr1 := testdata.KeyTestPubAddr()
err = app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, feeAmount)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr1, feeAmount)
require.NoError(t, err)
require.Equal(t, feeCoin.Amount, app.BankKeeper.GetBalance(ctx, addr1, feeCoin.Denom).Amount)
seq, _ := app.AccountKeeper.GetSequence(ctx, addr1)
require.Equal(t, uint64(0), seq)
// msg and signatures
msg := testdata.NewTestMsg(addr1)
txBuilder := encCfg.TxConfig.NewTxBuilder()
require.NoError(t, txBuilder.SetMsgs(msg))
txBuilder.SetFeeAmount(feeAmount)
txBuilder.SetGasLimit(txtypes.MaxGasWanted) // tx validation checks that gasLimit can't be bigger than this
privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{7}, []uint64{0}
_, txBytes, err := createTestTx(encCfg.TxConfig, txBuilder, privs, accNums, accSeqs, ctx.ChainID())
require.NoError(t, err)
app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: 1}})
rsp := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
// check result
ctx = app.GetContextForDeliverTx(txBytes)
okValue := ctx.KVStore(app.GetKey(banktypes.ModuleName)).Get([]byte("ok"))
if tc.expErr {
if tc.panicTx {
require.Equal(t, sdkerrors.ErrPanic.ABCICode(), rsp.Code)
} else {
require.Equal(t, sdkerrors.ErrOutOfGas.ABCICode(), rsp.Code)
}
require.Empty(t, okValue)
} else {
require.Equal(t, uint32(0), rsp.Code)
require.Equal(t, []byte("ok"), okValue)
}
// check block gas is always consumed
baseGas := uint64(63724) // baseGas is the gas consumed before tx msg
expGasConsumed := addUint64Saturating(tc.gasToConsume, baseGas)
if expGasConsumed > txtypes.MaxGasWanted {
// capped by gasLimit
expGasConsumed = txtypes.MaxGasWanted
}
require.Equal(t, expGasConsumed, ctx.BlockGasMeter().GasConsumed())
// tx fee is always deducted
require.Equal(t, int64(0), app.BankKeeper.GetBalance(ctx, addr1, feeCoin.Denom).Amount.Int64())
// sender's sequence is always increased
seq, err = app.AccountKeeper.GetSequence(ctx, addr1)
require.NoError(t, err)
require.Equal(t, uint64(1), seq)
})
}
}
func createTestTx(txConfig client.TxConfig, txBuilder client.TxBuilder, privs []cryptotypes.PrivKey, accNums []uint64, accSeqs []uint64, chainID string) (xauthsigning.Tx, []byte, error) {
// First round: we gather all the signer infos. We use the "set empty
// signature" hack to do that.
var sigsV2 []signing.SignatureV2
for i, priv := range privs {
sigV2 := signing.SignatureV2{
PubKey: priv.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: txConfig.SignModeHandler().DefaultMode(),
Signature: nil,
},
Sequence: accSeqs[i],
}
sigsV2 = append(sigsV2, sigV2)
}
err := txBuilder.SetSignatures(sigsV2...)
if err != nil {
return nil, nil, err
}
// Second round: all signer infos are set, so each signer can sign.
sigsV2 = []signing.SignatureV2{}
for i, priv := range privs {
signerData := xauthsigning.SignerData{
Address: sdk.AccAddress(priv.PubKey().Bytes()).String(),
ChainID: chainID,
AccountNumber: accNums[i],
Sequence: accSeqs[i],
}
sigV2, err := tx.SignWithPrivKey(
txConfig.SignModeHandler().DefaultMode(), signerData,
txBuilder, priv, txConfig, accSeqs[i])
if err != nil {
return nil, nil, err
}
sigsV2 = append(sigsV2, sigV2)
}
err = txBuilder.SetSignatures(sigsV2...)
if err != nil {
return nil, nil, err
}
txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, nil, err
}
return txBuilder.GetTx(), txBytes, nil
}
func addUint64Saturating(a, b uint64) uint64 {
if math.MaxUint64-a < b {
return math.MaxUint64
}
return a + b
}
-116
View File
@@ -1,116 +0,0 @@
package baseapp_test
import (
"context"
"fmt"
"github.com/tendermint/tendermint/crypto/tmhash"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
)
type handlerFun func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error)
type customTxHandler struct {
handler handlerFun
next tx.Handler
}
var _ tx.Handler = customTxHandler{}
// CustomTxMiddleware is being used in tests for testing custom pre-`runMsgs` logic.
func CustomTxHandlerMiddleware(handler handlerFun) tx.Middleware {
return func(txHandler tx.Handler) tx.Handler {
return customTxHandler{
handler: handler,
next: txHandler,
}
}
}
// CheckTx implements tx.Handler.CheckTx method.
func (txh customTxHandler) CheckTx(ctx context.Context, req tx.Request, checkReq tx.RequestCheckTx) (tx.Response, tx.ResponseCheckTx, error) {
sdkCtx, err := txh.runHandler(ctx, req.Tx, req.TxBytes, false)
if err != nil {
return tx.Response{}, tx.ResponseCheckTx{}, err
}
return txh.next.CheckTx(sdk.WrapSDKContext(sdkCtx), req, checkReq)
}
// DeliverTx implements tx.Handler.DeliverTx method.
func (txh customTxHandler) DeliverTx(ctx context.Context, req tx.Request) (tx.Response, error) {
sdkCtx, err := txh.runHandler(ctx, req.Tx, req.TxBytes, false)
if err != nil {
return tx.Response{}, err
}
return txh.next.DeliverTx(sdk.WrapSDKContext(sdkCtx), req)
}
// SimulateTx implements tx.Handler.SimulateTx method.
func (txh customTxHandler) SimulateTx(ctx context.Context, req tx.Request) (tx.Response, error) {
sdkCtx, err := txh.runHandler(ctx, req.Tx, req.TxBytes, true)
if err != nil {
return tx.Response{}, err
}
return txh.next.SimulateTx(sdk.WrapSDKContext(sdkCtx), req)
}
func (txh customTxHandler) runHandler(ctx context.Context, tx sdk.Tx, txBytes []byte, isSimulate bool) (sdk.Context, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
if txh.handler == nil {
return sdkCtx, nil
}
ms := sdkCtx.MultiStore()
// Branch context before Handler call in case it aborts.
// This is required for both CheckTx and DeliverTx.
// Ref: https://github.com/cosmos/cosmos-sdk/issues/2772
//
// NOTE: Alternatively, we could require that Handler ensures that
// writes do not happen if aborted/failed. This may have some
// performance benefits, but it'll be more difficult to get right.
cacheCtx, msCache := cacheTxContext(sdkCtx, txBytes)
cacheCtx = cacheCtx.WithEventManager(sdk.NewEventManager())
newCtx, err := txh.handler(cacheCtx, tx, isSimulate)
if err != nil {
return sdk.Context{}, err
}
if !newCtx.IsZero() {
// At this point, newCtx.MultiStore() is a store branch, or something else
// replaced by the Handler. We want the original multistore.
//
// Also, in the case of the tx aborting, we need to track gas consumed via
// the instantiated gas meter in the Handler, so we update the context
// prior to returning.
sdkCtx = newCtx.WithMultiStore(ms)
}
msCache.Write()
return sdkCtx, nil
}
// cacheTxContext returns a new context based off of the provided context with
// a branched multi-store.
func cacheTxContext(sdkCtx sdk.Context, txBytes []byte) (sdk.Context, sdk.CacheMultiStore) {
ms := sdkCtx.MultiStore()
// TODO: https://github.com/cosmos/cosmos-sdk/issues/2824
msCache := ms.CacheMultiStore()
if msCache.TracingEnabled() {
msCache = msCache.SetTracingContext(
sdk.TraceContext(
map[string]interface{}{
"txHash": fmt.Sprintf("%X", tmhash.Sum(txBytes)),
},
),
).(sdk.CacheMultiStore)
}
return sdkCtx.WithMultiStore(msCache), msCache
}
+2 -4
View File
@@ -5,8 +5,6 @@ import (
"sync"
"testing"
"github.com/cosmos/cosmos-sdk/testutil/testdata_pulsar"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
@@ -15,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
"github.com/cosmos/cosmos-sdk/testutil/testdata_pulsar"
sdk "github.com/cosmos/cosmos-sdk/types"
)
@@ -56,8 +55,7 @@ func TestRegisterQueryServiceTwice(t *testing.T) {
// Setup baseapp.
db := dbm.NewMemDB()
encCfg := simapp.MakeTestEncodingConfig()
logger, _ := log.NewDefaultLogger("plain", "info", false)
app := baseapp.NewBaseApp("test", logger, db)
app := baseapp.NewBaseApp("test", log.MustNewDefaultLogger("plain", "info", false), db, encCfg.TxConfig.TxDecoder())
app.SetInterfaceRegistry(encCfg.InterfaceRegistry)
testdata.RegisterInterfaces(encCfg.InterfaceRegistry)
+140
View File
@@ -0,0 +1,140 @@
package baseapp
import (
"context"
"fmt"
gogogrpc "github.com/gogo/protobuf/grpc"
"github.com/gogo/protobuf/proto"
"google.golang.org/grpc"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// MsgServiceRouter routes fully-qualified Msg service methods to their handler.
type MsgServiceRouter struct {
interfaceRegistry codectypes.InterfaceRegistry
routes map[string]MsgServiceHandler
}
var _ gogogrpc.Server = &MsgServiceRouter{}
// NewMsgServiceRouter creates a new MsgServiceRouter.
func NewMsgServiceRouter() *MsgServiceRouter {
return &MsgServiceRouter{
routes: map[string]MsgServiceHandler{},
}
}
// MsgServiceHandler defines a function type which handles Msg service message.
type MsgServiceHandler = func(ctx sdk.Context, req sdk.Msg) (*sdk.Result, error)
// Handler returns the MsgServiceHandler for a given msg or nil if not found.
func (msr *MsgServiceRouter) Handler(msg sdk.Msg) MsgServiceHandler {
return msr.routes[sdk.MsgTypeURL(msg)]
}
// HandlerByTypeURL returns the MsgServiceHandler for a given query route path or nil
// if not found.
func (msr *MsgServiceRouter) HandlerByTypeURL(typeURL string) MsgServiceHandler {
return msr.routes[typeURL]
}
// RegisterService implements the gRPC Server.RegisterService method. sd is a gRPC
// service description, handler is an object which implements that gRPC service.
//
// This function PANICs:
// - if it is called before the service `Msg`s have been registered using
// RegisterInterfaces,
// - or if a service is being registered twice.
func (msr *MsgServiceRouter) RegisterService(sd *grpc.ServiceDesc, handler interface{}) {
// Adds a top-level query handler based on the gRPC service name.
for _, method := range sd.Methods {
fqMethod := fmt.Sprintf("/%s/%s", sd.ServiceName, method.MethodName)
methodHandler := method.Handler
var requestTypeName string
// NOTE: This is how we pull the concrete request type for each handler for registering in the InterfaceRegistry.
// This approach is maybe a bit hacky, but less hacky than reflecting on the handler object itself.
// We use a no-op interceptor to avoid actually calling into the handler itself.
_, _ = methodHandler(nil, context.Background(), func(i interface{}) error {
msg, ok := i.(sdk.Msg)
if !ok {
// We panic here because there is no other alternative and the app cannot be initialized correctly
// this should only happen if there is a problem with code generation in which case the app won't
// work correctly anyway.
panic(fmt.Errorf("unable to register service method %s: %T does not implement sdk.Msg", fqMethod, i))
}
requestTypeName = sdk.MsgTypeURL(msg)
return nil
}, noopInterceptor)
// Check that the service Msg fully-qualified method name has already
// been registered (via RegisterInterfaces). If the user registers a
// service without registering according service Msg type, there might be
// some unexpected behavior down the road. Since we can't return an error
// (`Server.RegisterService` interface restriction) we panic (at startup).
reqType, err := msr.interfaceRegistry.Resolve(requestTypeName)
if err != nil || reqType == nil {
panic(
fmt.Errorf(
"type_url %s has not been registered yet. "+
"Before calling RegisterService, you must register all interfaces by calling the `RegisterInterfaces` "+
"method on module.BasicManager. Each module should call `msgservice.RegisterMsgServiceDesc` inside its "+
"`RegisterInterfaces` method with the `_Msg_serviceDesc` generated by proto-gen",
requestTypeName,
),
)
}
// Check that each service is only registered once. If a service is
// registered more than once, then we should error. Since we can't
// return an error (`Server.RegisterService` interface restriction) we
// panic (at startup).
_, found := msr.routes[requestTypeName]
if found {
panic(
fmt.Errorf(
"msg service %s has already been registered. Please make sure to only register each service once. "+
"This usually means that there are conflicting modules registering the same msg service",
fqMethod,
),
)
}
msr.routes[requestTypeName] = func(ctx sdk.Context, req sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
interceptor := func(goCtx context.Context, _ interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
goCtx = context.WithValue(goCtx, sdk.SdkContextKey, ctx)
return handler(goCtx, req)
}
// Call the method handler from the service description with the handler object.
// We don't do any decoding here because the decoding was already done.
res, err := methodHandler(handler, sdk.WrapSDKContext(ctx), noopDecoder, interceptor)
if err != nil {
return nil, err
}
resMsg, ok := res.(proto.Message)
if !ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "Expecting proto.Message, got %T", resMsg)
}
return sdk.WrapServiceResult(ctx, resMsg, err)
}
}
}
// SetInterfaceRegistry sets the interface registry for the router.
func (msr *MsgServiceRouter) SetInterfaceRegistry(interfaceRegistry codectypes.InterfaceRegistry) {
msr.interfaceRegistry = interfaceRegistry
}
func noopDecoder(_ interface{}) error { return nil }
func noopInterceptor(_ context.Context, _ interface{}, _ *grpc.UnaryServerInfo, _ grpc.UnaryHandler) (interface{}, error) {
return nil, nil
}
+121
View File
@@ -0,0 +1,121 @@
package baseapp_test
import (
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
)
func TestRegisterMsgService(t *testing.T) {
db := dbm.NewMemDB()
// Create an encoding config that doesn't register testdata Msg services.
encCfg := simapp.MakeTestEncodingConfig()
app := baseapp.NewBaseApp("test", log.MustNewDefaultLogger("plain", "info", false), db, encCfg.TxConfig.TxDecoder())
app.SetInterfaceRegistry(encCfg.InterfaceRegistry)
require.Panics(t, func() {
testdata.RegisterMsgServer(
app.MsgServiceRouter(),
testdata.MsgServerImpl{},
)
})
// Register testdata Msg services, and rerun `RegisterService`.
testdata.RegisterInterfaces(encCfg.InterfaceRegistry)
require.NotPanics(t, func() {
testdata.RegisterMsgServer(
app.MsgServiceRouter(),
testdata.MsgServerImpl{},
)
})
}
func TestRegisterMsgServiceTwice(t *testing.T) {
// Setup baseapp.
db := dbm.NewMemDB()
encCfg := simapp.MakeTestEncodingConfig()
app := baseapp.NewBaseApp("test", log.MustNewDefaultLogger("plain", "info", false), db, encCfg.TxConfig.TxDecoder())
app.SetInterfaceRegistry(encCfg.InterfaceRegistry)
testdata.RegisterInterfaces(encCfg.InterfaceRegistry)
// First time registering service shouldn't panic.
require.NotPanics(t, func() {
testdata.RegisterMsgServer(
app.MsgServiceRouter(),
testdata.MsgServerImpl{},
)
})
// Second time should panic.
require.Panics(t, func() {
testdata.RegisterMsgServer(
app.MsgServiceRouter(),
testdata.MsgServerImpl{},
)
})
}
func TestMsgService(t *testing.T) {
priv, _, _ := testdata.KeyTestPubAddr()
encCfg := simapp.MakeTestEncodingConfig()
testdata.RegisterInterfaces(encCfg.InterfaceRegistry)
db := dbm.NewMemDB()
app := baseapp.NewBaseApp("test", log.MustNewDefaultLogger("plain", "info", false), db, encCfg.TxConfig.TxDecoder())
app.SetInterfaceRegistry(encCfg.InterfaceRegistry)
testdata.RegisterMsgServer(
app.MsgServiceRouter(),
testdata.MsgServerImpl{},
)
_ = app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: 1}})
msg := testdata.MsgCreateDog{Dog: &testdata.Dog{Name: "Spot"}}
txBuilder := encCfg.TxConfig.NewTxBuilder()
txBuilder.SetFeeAmount(testdata.NewTestFeeAmount())
txBuilder.SetGasLimit(testdata.NewTestGasLimit())
err := txBuilder.SetMsgs(&msg)
require.NoError(t, err)
// First round: we gather all the signer infos. We use the "set empty
// signature" hack to do that.
sigV2 := signing.SignatureV2{
PubKey: priv.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: encCfg.TxConfig.SignModeHandler().DefaultMode(),
Signature: nil,
},
Sequence: 0,
}
err = txBuilder.SetSignatures(sigV2)
require.NoError(t, err)
// Second round: all signer infos are set, so each signer can sign.
signerData := authsigning.SignerData{
ChainID: "test",
AccountNumber: 0,
Sequence: 0,
}
sigV2, err = tx.SignWithPrivKey(
encCfg.TxConfig.SignModeHandler().DefaultMode(), signerData,
txBuilder, priv, encCfg.TxConfig, 0)
require.NoError(t, err)
err = txBuilder.SetSignatures(sigV2)
require.NoError(t, err)
// Send the tx to the app
txBytes, err := encCfg.TxConfig.TxEncoder()(txBuilder.GetTx())
require.NoError(t, err)
res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes})
require.Equal(t, abci.CodeTypeOK, res.Code, "res=%+v", res)
}
+12 -4
View File
@@ -12,7 +12,6 @@ import (
snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// File for storing in-package BaseApp optional functions,
@@ -146,12 +145,12 @@ func (app *BaseApp) SetEndBlocker(endBlocker sdk.EndBlocker) {
app.endBlocker = endBlocker
}
func (app *BaseApp) SetTxHandler(txHandler tx.Handler) {
func (app *BaseApp) SetAnteHandler(ah sdk.AnteHandler) {
if app.sealed {
panic("SetTxHandler() on sealed BaseApp")
panic("SetAnteHandler() on sealed BaseApp")
}
app.txHandler = txHandler
app.anteHandler = ah
}
func (app *BaseApp) SetAddrPeerFilter(pf sdk.PeerFilter) {
@@ -193,6 +192,14 @@ func (app *BaseApp) SetStoreLoader(loader StoreLoader) {
app.storeLoader = loader
}
// SetRouter allows us to customize the router.
func (app *BaseApp) SetRouter(router sdk.Router) {
if app.sealed {
panic("SetRouter() on sealed BaseApp")
}
app.router = router
}
// SetSnapshot sets the snapshot store and options.
func (app *BaseApp) SetSnapshot(snapshotStore *snapshots.Store, opts snapshottypes.SnapshotOptions) {
if app.sealed {
@@ -210,6 +217,7 @@ func (app *BaseApp) SetSnapshot(snapshotStore *snapshots.Store, opts snapshottyp
func (app *BaseApp) SetInterfaceRegistry(registry types.InterfaceRegistry) {
app.interfaceRegistry = registry
app.grpcQueryRouter.SetInterfaceRegistry(registry)
app.msgServiceRouter.SetInterfaceRegistry(registry)
}
// SetStreamingService is used to set a streaming service into the BaseApp hooks and load the listeners into the multistore
+2 -3
View File
@@ -1,4 +1,4 @@
package baseapp_test
package baseapp
import (
"testing"
@@ -7,7 +7,6 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
)
@@ -16,7 +15,7 @@ var testQuerier = func(_ sdk.Context, _ []string, _ abci.RequestQuery) ([]byte,
}
func TestQueryRouter(t *testing.T) {
qr := baseapp.NewQueryRouter()
qr := NewQueryRouter()
// require panic on invalid route
require.Panics(t, func() {
+77
View File
@@ -0,0 +1,77 @@
package baseapp
import (
"fmt"
"runtime/debug"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// RecoveryHandler handles recovery() object.
// Return a non-nil error if recoveryObj was processed.
// Return nil if recoveryObj was not processed.
type RecoveryHandler func(recoveryObj interface{}) error
// recoveryMiddleware is wrapper for RecoveryHandler to create chained recovery handling.
// returns (recoveryMiddleware, nil) if recoveryObj was not processed and should be passed to the next middleware in chain.
// returns (nil, error) if recoveryObj was processed and middleware chain processing should be stopped.
type recoveryMiddleware func(recoveryObj interface{}) (recoveryMiddleware, error)
// processRecovery processes recoveryMiddleware chain for recovery() object.
// Chain processing stops on non-nil error or when chain is processed.
func processRecovery(recoveryObj interface{}, middleware recoveryMiddleware) error {
if middleware == nil {
return nil
}
next, err := middleware(recoveryObj)
if err != nil {
return err
}
return processRecovery(recoveryObj, next)
}
// newRecoveryMiddleware creates a RecoveryHandler middleware.
func newRecoveryMiddleware(handler RecoveryHandler, next recoveryMiddleware) recoveryMiddleware {
return func(recoveryObj interface{}) (recoveryMiddleware, error) {
if err := handler(recoveryObj); err != nil {
return nil, err
}
return next, nil
}
}
// newOutOfGasRecoveryMiddleware creates a standard OutOfGas recovery middleware for app.runTx method.
func newOutOfGasRecoveryMiddleware(gasWanted uint64, ctx sdk.Context, next recoveryMiddleware) recoveryMiddleware {
handler := func(recoveryObj interface{}) error {
err, ok := recoveryObj.(sdk.ErrorOutOfGas)
if !ok {
return nil
}
return sdkerrors.Wrap(
sdkerrors.ErrOutOfGas, fmt.Sprintf(
"out of gas in location: %v; gasWanted: %d, gasUsed: %d",
err.Descriptor, gasWanted, ctx.GasMeter().GasConsumed(),
),
)
}
return newRecoveryMiddleware(handler, next)
}
// newDefaultRecoveryMiddleware creates a default (last in chain) recovery middleware for app.runTx method.
func newDefaultRecoveryMiddleware() recoveryMiddleware {
handler := func(recoveryObj interface{}) error {
return sdkerrors.Wrap(
sdkerrors.ErrPanic, fmt.Sprintf(
"recovered: %v\nstack:\n%v", recoveryObj, string(debug.Stack()),
),
)
}
return newRecoveryMiddleware(handler, nil)
}
+64
View File
@@ -0,0 +1,64 @@
package baseapp
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// Test that recovery chain produces expected error at specific middleware layer
func TestRecoveryChain(t *testing.T) {
createError := func(id int) error {
return fmt.Errorf("error from id: %d", id)
}
createHandler := func(id int, handle bool) RecoveryHandler {
return func(_ interface{}) error {
if handle {
return createError(id)
}
return nil
}
}
// check recovery chain [1] -> 2 -> 3
{
mw := newRecoveryMiddleware(createHandler(3, false), nil)
mw = newRecoveryMiddleware(createHandler(2, false), mw)
mw = newRecoveryMiddleware(createHandler(1, true), mw)
receivedErr := processRecovery(nil, mw)
require.Equal(t, createError(1), receivedErr)
}
// check recovery chain 1 -> [2] -> 3
{
mw := newRecoveryMiddleware(createHandler(3, false), nil)
mw = newRecoveryMiddleware(createHandler(2, true), mw)
mw = newRecoveryMiddleware(createHandler(1, false), mw)
receivedErr := processRecovery(nil, mw)
require.Equal(t, createError(2), receivedErr)
}
// check recovery chain 1 -> 2 -> [3]
{
mw := newRecoveryMiddleware(createHandler(3, true), nil)
mw = newRecoveryMiddleware(createHandler(2, false), mw)
mw = newRecoveryMiddleware(createHandler(1, false), mw)
receivedErr := processRecovery(nil, mw)
require.Equal(t, createError(3), receivedErr)
}
// check recovery chain 1 -> 2 -> 3
{
mw := newRecoveryMiddleware(createHandler(3, false), nil)
mw = newRecoveryMiddleware(createHandler(2, false), mw)
mw = newRecoveryMiddleware(createHandler(1, false), mw)
receivedErr := processRecovery(nil, mw)
require.Nil(t, receivedErr)
}
}
+41
View File
@@ -0,0 +1,41 @@
package baseapp
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type Router struct {
routes map[string]sdk.Handler
}
var _ sdk.Router = NewRouter()
// NewRouter returns a reference to a new router.
func NewRouter() *Router {
return &Router{
routes: make(map[string]sdk.Handler),
}
}
// AddRoute adds a route path to the router with a given handler. The route must
// be alphanumeric.
func (rtr *Router) AddRoute(route sdk.Route) sdk.Router {
if !sdk.IsAlphaNumeric(route.Path()) {
panic("route expressions can only contain alphanumeric characters")
}
if rtr.routes[route.Path()] != nil {
panic(fmt.Sprintf("route %s has already been initialized", route.Path()))
}
rtr.routes[route.Path()] = route.Handler()
return rtr
}
// Route returns a handler for a given route path.
//
// TODO: Handle expressive matches.
func (rtr *Router) Route(_ sdk.Context, path string) sdk.Handler {
return rtr.routes[path]
}
+31
View File
@@ -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, error) {
return &sdk.Result{}, nil
}
func TestRouter(t *testing.T) {
rtr := NewRouter()
// require panic on invalid route
require.Panics(t, func() {
rtr.AddRoute(sdk.NewRoute("*", testHandler))
})
rtr.AddRoute(sdk.NewRoute("testRoute", testHandler))
h := rtr.Route(sdk.Context{}, "testRoute")
require.NotNil(t, h)
// require panic on duplicate route
require.Panics(t, func() {
rtr.AddRoute(sdk.NewRoute("testRoute", testHandler))
})
}
+16 -54
View File
@@ -1,81 +1,39 @@
package baseapp
import (
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/tx"
)
// SimCheck defines a CheckTx helper function that used in tests and simulations.
func (app *BaseApp) SimCheck(txEncoder sdk.TxEncoder, sdkTx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
// CheckTx expects tx bytes as argument, so we encode the tx argument into
// bytes. Note that CheckTx will actually decode those bytes again. But since
func (app *BaseApp) SimCheck(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
// runTx expects tx bytes as argument, so we encode the tx argument into
// bytes. Note that runTx will actually decode those bytes again. But since
// this helper is only used in tests/simulation, it's fine.
bz, err := txEncoder(sdkTx)
bz, err := txEncoder(tx)
if err != nil {
return sdk.GasInfo{}, nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "%s", err)
}
ctx := app.getContextForTx(runTxModeDeliver, bz)
res, _, err := app.txHandler.CheckTx(ctx, tx.Request{Tx: sdkTx, TxBytes: bz}, tx.RequestCheckTx{Type: abci.CheckTxType_New})
gInfo := sdk.GasInfo{GasWanted: uint64(res.GasWanted), GasUsed: uint64(res.GasUsed)}
if err != nil {
return gInfo, nil, err
}
data, err := makeABCIData(res)
if err != nil {
return gInfo, nil, err
}
return gInfo, &sdk.Result{Data: data, Log: res.Log, Events: res.Events, MsgResponses: res.MsgResponses}, nil
gasInfo, result, _, err := app.runTx(runTxModeCheck, bz)
return gasInfo, result, err
}
// Simulate executes a tx in simulate mode to get result and gas info.
func (app *BaseApp) Simulate(txBytes []byte) (sdk.GasInfo, *sdk.Result, error) {
ctx := app.getContextForTx(runTxModeSimulate, txBytes)
res, err := app.txHandler.SimulateTx(ctx, tx.Request{TxBytes: txBytes})
gasInfo := sdk.GasInfo{
GasWanted: res.GasWanted,
GasUsed: res.GasUsed,
}
if err != nil {
return gasInfo, nil, err
}
data, err := makeABCIData(res)
if err != nil {
return gasInfo, nil, err
}
return gasInfo, &sdk.Result{Data: data, Log: res.Log, Events: res.Events, MsgResponses: res.MsgResponses}, nil
gasInfo, result, _, err := app.runTx(runTxModeSimulate, txBytes)
return gasInfo, result, err
}
// SimDeliver defines a DeliverTx helper function that used in tests and
// simulations.
func (app *BaseApp) SimDeliver(txEncoder sdk.TxEncoder, sdkTx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
func (app *BaseApp) SimDeliver(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, *sdk.Result, error) {
// See comment for Check().
bz, err := txEncoder(sdkTx)
bz, err := txEncoder(tx)
if err != nil {
return sdk.GasInfo{}, nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "%s", err)
}
ctx := app.getContextForTx(runTxModeDeliver, bz)
res, err := app.txHandler.DeliverTx(ctx, tx.Request{Tx: sdkTx, TxBytes: bz})
gInfo := sdk.GasInfo{GasWanted: uint64(res.GasWanted), GasUsed: uint64(res.GasUsed)}
if err != nil {
return gInfo, nil, err
}
data, err := makeABCIData(res)
if err != nil {
return gInfo, nil, err
}
return gInfo, &sdk.Result{Data: data, Log: res.Log, Events: res.Events, MsgResponses: res.MsgResponses}, nil
gasInfo, result, _, err := app.runTx(runTxModeDeliver, bz)
return gasInfo, result, err
}
// Context with current {check, deliver}State of the app used by tests.
@@ -91,3 +49,7 @@ func (app *BaseApp) NewContext(isCheckTx bool, header tmproto.Header) sdk.Contex
func (app *BaseApp) NewUncachedContext(isCheckTx bool, header tmproto.Header) sdk.Context {
return sdk.NewContext(app.cms, header, isCheckTx, app.logger)
}
func (app *BaseApp) GetContextForDeliverTx(txBytes []byte) sdk.Context {
return app.getContextForTx(runTxModeDeliver, txBytes)
}