diff --git a/CHANGELOG.md b/CHANGELOG.md index 8032588097..c2e9538919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (baseapp) [#24069](https://github.com/cosmos/cosmos-sdk/pull/24069) Create CheckTxHandler to allow extending the logic of CheckTx. * (types) [#24093](https://github.com/cosmos/cosmos-sdk/pull/24093) Added a new method, `IsGT`, for `types.Coin`. This method is used to check if a `types.Coin` is greater than another `types.Coin`. * (client/keys) [#24071](https://github.com/cosmos/cosmos-sdk/pull/24071) Add support for importing hex key using standard input. * (types) [#23780](https://github.com/cosmos/cosmos-sdk/pull/23780) Add a ValueCodec for the math.Uint type that can be used in collections maps. diff --git a/baseapp/abci.go b/baseapp/abci.go index 42f1d81e6b..30761b5081 100644 --- a/baseapp/abci.go +++ b/baseapp/abci.go @@ -351,18 +351,27 @@ func (app *BaseApp) CheckTx(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, er return nil, fmt.Errorf("unknown RequestCheckTx type: %s", req.Type) } - gInfo, result, anteEvents, err := app.runTx(mode, req.Tx) - if err != nil { - return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, app.trace), nil + if app.checkTxHandler == nil { + gInfo, result, anteEvents, err := app.runTx(mode, req.Tx, nil) + if err != nil { + return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, app.trace), nil + } + + 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), + }, nil } - 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), - }, nil + // Create wrapper to avoid users overriding the execution mode + runTx := func(txBytes []byte, tx sdk.Tx) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { + return app.runTx(mode, txBytes, tx) + } + + return app.checkTxHandler(runTx, req) } // PrepareProposal implements the PrepareProposal ABCI method and returns a diff --git a/baseapp/abci_test.go b/baseapp/abci_test.go index 0af38e4762..58c51a21a4 100644 --- a/baseapp/abci_test.go +++ b/baseapp/abci_test.go @@ -926,7 +926,7 @@ func TestABCI_InvalidTransaction(t *testing.T) { _, _, err := suite.baseApp.SimDeliver(suite.txConfig.TxEncoder(), tx) require.Error(t, err) space, code, _ := errorsmod.ABCIInfo(err, false) - require.EqualValues(t, sdkerrors.ErrTxDecode.ABCICode(), code) + require.EqualValues(t, sdkerrors.ErrUnknownRequest.ABCICode(), code) require.EqualValues(t, sdkerrors.ErrTxDecode.Codespace(), space) } } diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 1984f7b66d..677f65cd7f 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -79,6 +79,7 @@ type BaseApp struct { anteHandler sdk.AnteHandler // ante handler for fee and auth postHandler sdk.PostHandler // post handler, optional + checkTxHandler sdk.CheckTxHandler // ABCI CheckTx handler initChainer sdk.InitChainer // ABCI InitChain handler preBlocker sdk.PreBlocker // logic to run before BeginBlocker beginBlocker sdk.BeginBlocker // (legacy ABCI) BeginBlock handler @@ -693,7 +694,6 @@ func (app *BaseApp) getContextForTx(mode execMode, txBytes []byte) sdk.Context { // a branched multi-store. func (app *BaseApp) cacheTxContext(ctx sdk.Context, txBytes []byte) (sdk.Context, storetypes.CacheMultiStore) { ms := ctx.MultiStore() - // TODO: https://github.com/cosmos/cosmos-sdk/issues/2824 msCache := ms.CacheMultiStore() if msCache.TracingEnabled() { msCache = msCache.SetTracingContext( @@ -769,7 +769,7 @@ func (app *BaseApp) deliverTx(tx []byte) *abci.ExecTxResult { telemetry.SetGauge(float32(gInfo.GasWanted), "tx", "gas", "wanted") }() - gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx) + gInfo, result, anteEvents, err := app.runTx(execModeFinalize, tx, nil) if err != nil { resultStr = "failed" resp = sdkerrors.ResponseExecTxResultWithEvents( @@ -826,7 +826,9 @@ func (app *BaseApp) endBlock(_ context.Context) (sdk.EndBlock, error) { // 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 execMode, txBytes []byte) (gInfo sdk.GasInfo, result *sdk.Result, anteEvents []abci.Event, err error) { +// both txbytes and the decoded tx are passed to runTx to avoid the state machine encoding the tx and decoding the transaction twice +// passing the decoded tx to runTX is optional, it will be decoded if the tx is nil +func (app *BaseApp) runTx(mode execMode, txBytes []byte, tx sdk.Tx) (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. @@ -874,9 +876,12 @@ func (app *BaseApp) runTx(mode execMode, txBytes []byte) (gInfo sdk.GasInfo, res defer consumeBlockGas() } - tx, err := app.txDecoder(txBytes) - if err != nil { - return sdk.GasInfo{}, nil, nil, err + // if the transaction is not decoded, decode it here + if tx == nil { + tx, err = app.txDecoder(txBytes) + if err != nil { + return sdk.GasInfo{GasUsed: 0, GasWanted: 0}, nil, nil, sdkerrors.ErrTxDecode.Wrap(err.Error()) + } } msgs := tx.GetMsgs() @@ -1115,7 +1120,7 @@ func (app *BaseApp) PrepareProposalVerifyTx(tx sdk.Tx) ([]byte, error) { return nil, err } - _, _, _, err = app.runTx(execModePrepareProposal, bz) + _, _, _, err = app.runTx(execModePrepareProposal, bz, tx) if err != nil { return nil, err } @@ -1134,7 +1139,7 @@ func (app *BaseApp) ProcessProposalVerifyTx(txBz []byte) (sdk.Tx, error) { return nil, err } - _, _, _, err = app.runTx(execModeProcessProposal, txBz) + _, _, _, err = app.runTx(execModeProcessProposal, txBz, tx) if err != nil { return nil, err } diff --git a/baseapp/options.go b/baseapp/options.go index d46d95d4f4..1f809e498b 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -350,6 +350,15 @@ func (app *BaseApp) SetPrepareProposal(handler sdk.PrepareProposalHandler) { app.prepareProposal = handler } +// SetCheckTx sets the checkTx function for the BaseApp. +func (app *BaseApp) SetCheckTxHandler(handler sdk.CheckTxHandler) { + if app.sealed { + panic("SetCheckTxHandler() on sealed BaseApp") + } + + app.checkTxHandler = handler +} + func (app *BaseApp) SetExtendVoteHandler(handler sdk.ExtendVoteHandler) { if app.sealed { panic("SetExtendVoteHandler() on sealed BaseApp") diff --git a/baseapp/test_helpers.go b/baseapp/test_helpers.go index f6735dcee1..b3aa396a02 100644 --- a/baseapp/test_helpers.go +++ b/baseapp/test_helpers.go @@ -19,13 +19,13 @@ func (app *BaseApp) SimCheck(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, * return sdk.GasInfo{}, nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "%s", err) } - gasInfo, result, _, err := app.runTx(execModeCheck, bz) + gasInfo, result, _, err := app.runTx(execModeCheck, bz, tx) 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) { - gasInfo, result, _, err := app.runTx(execModeSimulate, txBytes) + gasInfo, result, _, err := app.runTx(execModeSimulate, txBytes, nil) return gasInfo, result, err } @@ -36,7 +36,7 @@ func (app *BaseApp) SimDeliver(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk.GasInfo, return sdk.GasInfo{}, nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "%s", err) } - gasInfo, result, _, err := app.runTx(execModeFinalize, bz) + gasInfo, result, _, err := app.runTx(execModeFinalize, bz, tx) return gasInfo, result, err } @@ -47,7 +47,7 @@ func (app *BaseApp) SimTxFinalizeBlock(txEncoder sdk.TxEncoder, tx sdk.Tx) (sdk. return sdk.GasInfo{}, nil, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, "%s", err) } - gasInfo, result, _, err := app.runTx(execModeFinalize, bz) + gasInfo, result, _, err := app.runTx(execModeFinalize, bz, tx) return gasInfo, result, err } diff --git a/docs/docs/build/abci/04-checktx.md b/docs/docs/build/abci/04-checktx.md new file mode 100644 index 0000000000..081d6fd241 --- /dev/null +++ b/docs/docs/build/abci/04-checktx.md @@ -0,0 +1,50 @@ +# CheckTx + +CheckTx is called by the `BaseApp` when comet receives a transaction from a client, over the p2p network or RPC. The CheckTx method is responsible for validating the transaction and returning an error if the transaction is invalid. + +```mermaid +graph TD + subgraph SDK[Cosmos SDK] + B[Baseapp] + A[AnteHandlers] + B <-->|Validate TX| A + end + C[CometBFT] <-->|CheckTx|SDK + U((User)) -->|Submit TX| C + N[P2P] -->|Receive TX| C +``` + +```go reference +https://github.com/cosmos/cosmos-sdk/blob/31c604762a434c7b676b6a89897ecbd7c4653a23/baseapp/abci.go#L350-L390 +``` + +## CheckTx Handler + +`CheckTxHandler` allows users to extend the logic of `CheckTx`. `CheckTxHandler` is called by passing context and the transaction bytes received through ABCI. It is required that the handler returns deterministic results given the same transaction bytes. + +:::note +we return the raw decoded transaction here to avoid decoding it twice. +::: + +```go +type CheckTxHandler func(ctx sdk.Context, tx []byte) (Tx, error) +``` + +Setting a custom `CheckTxHandler` is optional. It can be done from your app.go file: + +```go +func NewSimApp( + logger log.Logger, + db corestore.KVStoreWithBatch, + traceStore io.Writer, + loadLatest bool, + appOpts servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), +) *SimApp { + ... + // Create ChecktxHandler + checktxHandler := abci.NewCustomCheckTxHandler(...) + app.SetCheckTxHandler(checktxHandler) + ... +} +``` diff --git a/types/abci.go b/types/abci.go index 8325f5dadf..745ed4b24c 100644 --- a/types/abci.go +++ b/types/abci.go @@ -23,6 +23,11 @@ type ProcessProposalHandler func(Context, *abci.RequestProcessProposal) (*abci.R // PrepareProposalHandler defines a function type alias for preparing a proposal type PrepareProposalHandler func(Context, *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) +// CheckTxHandler defines a function type alias for executing logic before transactions are executed. +// `RunTx` is a function type alias for executing logic before transactions are executed. +// The passed in runtx does not override antehandlers, the execution mode is not passed into runtx to avoid overriding the execution mode. +type CheckTxHandler func(RunTx, *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) + // ExtendVoteHandler defines a function type alias for extending a pre-commit vote. type ExtendVoteHandler func(Context, *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) @@ -74,3 +79,5 @@ type ResponsePreBlock struct { func (r ResponsePreBlock) IsConsensusParamsChanged() bool { return r.ConsensusParamsChanged } + +type RunTx = func(txBytes []byte, tx Tx) (gInfo GasInfo, result *Result, anteEvents []abci.Event, err error)