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:
+27
-60
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user