变更记录

  • 20.08.2021:初始草案。
  • 07.12.2021:更新 tx.Handler 接口(#10693)。
  • 17.05.2022:该 ADR 已废弃,因为中间件被认为过于难以推理。

状态

已废弃。替代方案正在 #11955 中讨论。

摘要

该 ADR 用基于中间件的设计替换当前 BaseApp 的 runTx 和 antehandler 设计。

背景

BaseApp 对 ABCI {Check,Deliver}Tx() 以及其自身 Simulate() 方法的实现,底层都会调用 runTx 方法;该方法会先运行 antehandler,然后执行 Msg。但是,交易小费 和 退还未使用的 gas 这两个用例要求在 Msg 执行之后运行自定义逻辑。目前还没有办法做到这一点。 一种朴素的解决方案是在 BaseApp 中添加 post-Msg hook。然而,Cosmos SDK 团队也在并行思考一个更大的方向,即让应用装配更简单(#9181),其中包括让 BaseApp 更轻量、更模块化。

决策

我们决定将 Baseapp 对 ABCI {Check,Deliver}Tx 以及其自身 Simulate 方法的实现改造为基于中间件的设计。 下面两个接口是该中间件设计的基础,定义在 types/tx 中:
type Handler interface {
    CheckTx(ctx context.Context, req Request, checkReq RequestCheckTx) (Response, ResponseCheckTx, error)

DeliverTx(ctx context.Context, req Request) (Response, error)

SimulateTx(ctx context.Context, req Request (Response, error)
}

type Middleware func(Handler)

Handler
其中定义了如下参数和返回类型:
type Request struct {
    Tx      sdk.Tx
	TxBytes []byte
}

type Response struct {
    GasWanted uint64
	GasUsed   uint64
	// MsgResponses is an array containing each Msg service handler's response
	// type, packed in an Any. This will get proto-serialized into the `Data` field
	// in the ABCI Check/DeliverTx responses.
	MsgResponses []*codectypes.Any
	Log          string
	Events       []abci.Event
}

type RequestCheckTx struct {
    Type abci.CheckTxType
}

type ResponseCheckTx struct {
    Priority int64
}
请注意,由于 CheckTx 需要处理与 mempool 优先级相关的独立逻辑,因此它的签名与 DeliverTx 和 SimulateTx 不同。 BaseApp 持有一个对 tx.Handler 的引用:
type BaseApp  struct {
    // other fields
    txHandler tx.Handler
}
Baseapp 的 ABCI {Check,Deliver}Tx() 和 Simulate() 方法只需使用相关参数调用 app.txHandler.{Check,Deliver,Simulate}Tx()。例如,对 DeliverTx 而言:
func (app *BaseApp)

DeliverTx(req abci.RequestDeliverTx)

abci.ResponseDeliverTx {
    var abciRes abci.ResponseDeliverTx
    ctx := app.getContextForTx(runTxModeDeliver, req.Tx)

res, err := app.txHandler.DeliverTx(ctx, tx.Request{
    TxBytes: req.Tx
})
    if err != nil {
    abciRes = sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)

return abciRes
}

abciRes, err = convertTxResponseToDeliverTx(res)
    if err != nil {
    return sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
}

return abciRes
}

// 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
}

// 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
})
}
BaseApp.CheckTx 和 BaseApp.Simulate 的实现与此类似。 baseapp.txHandler 这三个方法的实现当然可以是单体函数,但为了模块化,我们提出一种中间件组合设计:中间件本质上只是一个函数,它接收一个 tx.Handler,并返回一个包装在前一个 tx.Handler 外层的新的 tx.Handler。

实现中间件

在实践中,中间件由 Go 函数创建,该函数接收中间件所需的一些参数,并返回一个 tx.Middleware。 例如,为了创建一个任意的 MyMiddleware,我们可以这样实现:
// myTxHandler is the tx.Handler of this middleware. Note that it holds a
// reference to the next tx.Handler in the stack.
type myTxHandler struct {
    // next is the next tx.Handler in the middleware stack.
    next tx.Handler
    // some other fields that are relevant to the middleware can be added here
}

// NewMyMiddleware returns a middleware that does this and that.
func NewMyMiddleware(arg1, arg2)

tx.Middleware {
    return func (txh tx.Handler)

tx.Handler {
    return myTxHandler{
    next: txh,
            // optionally, set arg1, arg2... if they are needed in the middleware
}
 
}
}

// Assert myTxHandler is a tx.Handler.
var _ tx.Handler = myTxHandler{
}

func (h myTxHandler)

CheckTx(ctx context.Context, req Request, checkReq RequestcheckTx) (Response, ResponseCheckTx, error) {
    // CheckTx specific pre-processing logic

    // run the next middleware
    res, checkRes, err := txh.next.CheckTx(ctx, req, checkReq)

    // CheckTx specific post-processing logic

    return res, checkRes, err
}

func (h myTxHandler)

DeliverTx(ctx context.Context, req Request) (Response, error) {
    // DeliverTx specific pre-processing logic

    // run the next middleware
    res, err := txh.next.DeliverTx(ctx, tx, req)

    // DeliverTx specific post-processing logic

    return res, err
}

func (h myTxHandler)

SimulateTx(ctx context.Context, req Request) (Response, error) {
    // SimulateTx specific pre-processing logic

    // run the next middleware
    res, err := txh.next.SimulateTx(ctx, tx, req)

    // SimulateTx specific post-processing logic

    return res, err
}

组合中间件

虽然 BaseApp 只是持有一个对 tx.Handler 的引用,但这个 tx.Handler 本身是通过中间件栈定义出来的。Cosmos SDK 暴露了一个基础的(也就是最内层的)tx.Handler,名为 RunMsgsTxHandler,用于执行消息。 随后,应用开发者可以在这个基础 tx.Handler 之上组合多个中间件。每个中间件都可以像上一节所述的那样,在其下一个中间件外围运行前置和后置处理逻辑。从概念上看,假设有中间件 A、B、C 和基础 tx.Handler H,那么栈结构如下:
A.pre
    B.pre
        C.pre
            H # 基础 tx.handler,例如 `RunMsgsTxHandler`
        C.post
    B.post
A.post
我们定义了一个 ComposeMiddlewares 函数来组合中间件。它接收基础 handler 作为第一个参数,随后按“从外到内”的顺序接收中间件。对于上面的栈,最终的 tx.Handler 为:
txHandler := middleware.ComposeMiddlewares(H, A, B, C)
该中间件通过 SetTxHandler setter 设置到 BaseApp 中:
// simapp/app.go
    txHandler := middleware.ComposeMiddlewares(...)

app.SetTxHandler(txHandler)
应用开发者可以定义自己的中间件,也可以使用 Cosmos SDK 在 middleware.NewDefaultTxHandler() 中提供的预定义中间件。

由 Cosmos SDK 维护的中间件

虽然应用开发者可以定义并组合自己选择的中间件,但 Cosmos SDK 提供了一组中间件,用于满足生态中最常见的用例。这些中间件包括:
中间件说明
RunMsgsTxHandler这是基础 tx.Handler。它替代了旧版 baseapp 的 runMsgs,并执行交易中的 Msg。
TxDecoderMiddleware该中间件接收交易原始字节,并将其解码为 sdk.Tx。它替代了 baseapp.txDecoder 字段,从而让 BaseApp 尽可能保持精简。由于大多数中间件都会读取 sdk.Tx 的内容,因此 TxDecoderMiddleware 应该在中间件栈中最先运行。
{Antehandlers}每个 antehandler 都会被转换为各自独立的中间件。这些中间件会对传入交易执行签名校验、手续费扣除以及其他验证。
IndexEventsTxMiddleware这是一个简单的中间件,用于选择在 Tendermint 中索引哪些事件。它替代了 baseapp.indexEvents(遗憾的是,该字段在 baseapp 中依然存在,因为它还被用于索引 Begin/EndBlock 事件)。
RecoveryTxMiddleware该中间件用于从 panic 中恢复。它替代了 ADR-022 中描述的 baseapp.runTx panic 恢复机制。
GasTxMiddleware它替代了 Setup Antehandler。它会在 sdk.Context 上设置 GasMeter。请注意,过去 GasMeter 是在 antehandler 内部设置到 sdk.Context 上的,而 antehandler 还拥有自己的一套 panic 恢复系统,因此 baseapp 的恢复系统需要从中读取 GasMeter,这带来了一些混乱。现在这些混乱都被消除了:一个中间件负责设置 GasMeter,另一个中间件负责处理恢复。

Antehandler 与 Middleware 的相同点与不同点

基于 middleware 的设计建立在现有的 antehandler 设计之上,该设计见 ADR-010。尽管 ADR-010 的最终决定是采用“简单装饰器”方案,但 middleware 设计实际上与另一种 装饰器模式 提案非常相似,而 weave 也采用了这种模式。

与 Antehandler 的相同点

  • 都被设计为将小型模块化组件进行链式组合。
  • 都允许为 {Check,Deliver}Tx 和 Simulate 复用代码。
  • 都在 app.go 中进行设置,应用开发者也可以轻松自定义。
  • 顺序很重要。

与 Antehandler 的不同点

  • Antehandler 在 Msg 执行之前运行,而 middleware 可以在执行前和执行后运行。
  • Middleware 方案为 {Check,Deliver,Simulate}Tx 使用独立方法,而 antehandler 会传递一个 simulate bool 标志,并使用 sdkCtx.Is{Check,Recheck}Tx() 标志来判断当前处于哪种交易模式。
  • Middleware 设计允许每个 middleware 持有对下一个 middleware 的引用,而 antehandler 则是在 AnteHandle 方法中传递一个 next 参数。
  • Middleware 设计使用 Go 标准的 context.Context,而 antehandler 使用 sdk.Context。

影响

向后兼容性

由于这次重构将部分逻辑从 BaseApp 中移除并迁移到 middleware 中,因此会为应用开发者引入 API 级别的不兼容变更。最明显的一点是,应用开发者不再是在 app.go 中创建 antehandler 链,而是需要创建一个 middleware 栈:
- anteHandler, err := ante.NewAnteHandler(
-    ante.HandlerOptions{
-        AccountKeeper:   app.AccountKeeper,
-        BankKeeper:      app.BankKeeper,
-        SignModeHandler: encodingConfig.TxConfig.SignModeHandler(),
-        FeegrantKeeper:  app.FeeGrantKeeper,
-        SigGasConsumer:  ante.DefaultSigVerificationGasConsumer,
-    },
-)
+txHandler, err := authmiddleware.NewDefaultTxHandler(authmiddleware.TxHandlerOptions{
+    Debug:             app.Trace(),
+    IndexEvents:       indexEvents,
+    LegacyRouter:      app.legacyRouter,
+    MsgServiceRouter:  app.msgSvcRouter,
+    LegacyAnteHandler: anteHandler,
+    TxDecoder:         encodingConfig.TxConfig.TxDecoder,
+})
if err != nil {
    panic(err)
}
- app.SetAnteHandler(anteHandler)
+ app.SetTxHandler(txHandler)
其他较小的 API 不兼容变更也会在 CHANGELOG 中给出。与往常一样,Cosmos SDK 会为应用开发者提供版本迁移文档。 此 ADR 不会引入任何状态机、客户端或 CLI 层面的不兼容变更。

正面影响

  • 允许在 Msg 执行前后运行自定义逻辑。这使得 tips 和 gas refund 等用例成为可能,也可能支持其他用例。
  • 让 BaseApp 更轻量,并将复杂逻辑下放给小型模块化组件。
  • 为 {Check,Deliver,Simulate}Tx 分离不同路径,并使用不同的返回类型。这有助于提升可读性(例如用独立方法替代 if sdkCtx.IsRecheckTx() && !simulate {...}),并带来更高灵活性(例如在 ResponseCheckTx 中返回 priority)。

负面影响

  • 初看之下,很难理解在某个 middleware 运行后,基于 sdk.Context 和 tx 会发生哪些状态更新。一个 middleware 可以在其函数体内调用任意数量的嵌套 middleware,而这些 middleware 在调用链中的下一个 middleware 之前,都可能执行一些前置或后置处理。因此,要理解一个 middleware 在做什么,也必须理解链条中其后所有其他 middleware 在做什么,而且 middleware 的顺序也很重要。这会变得相当复杂,难以理解。
  • 会给应用开发者带来 API 不兼容变更。

中性影响

没有中性影响。

进一步讨论

  • #9934 将 BaseApp 的其他 ABCI 方法拆分为 middleware。
  • 在 tx.Handler 方法签名中,用具体的 protobuf Tx 类型替换 sdk.Tx 接口。

测试用例

我们会更新现有的 baseapp 和 antehandler 测试以使用新的 middleware API,但保留相同的测试用例和逻辑,以避免引入回归。现有 CLI 测试也将保持不变。 对于新的 middleware,我们会引入单元测试。由于 middleware 被有意设计得较小,因此单元测试非常适合。

参考资料


Changelog

  • 20.08.2021: Initial draft.
  • 07.12.2021: Update tx.Handler interface (#10693).
  • 17.05.2022: ADR is abandoned, as middlewares are deemed too hard to reason about.

Status

ABANDONED. Replacement is being discussed in #11955.

Abstract

This ADR replaces the current BaseApp runTx and antehandlers design with a middleware-based design.

Context

BaseApp’s implementation of ABCI {Check,Deliver}Tx() and its own Simulate() method call the runTx method under the hood, which first runs antehandlers, then executes Msgs. However, the transaction Tips and refunding unused gas use cases require custom logic to be run after the Msgs execution. There is currently no way to achieve this. An naive solution would be to add post-Msg hooks to BaseApp. However, the Cosmos SDK team thinks in parallel about the bigger picture of making app wiring simpler (#9181), which includes making BaseApp more lightweight and modular.

Decision

We decide to transform Baseapp’s implementation of ABCI {Check,Deliver}Tx and its own Simulate methods to use a middleware-based design. The two following interfaces are the base of the middleware design, and are defined in types/tx:
type Handler interface {
    CheckTx(ctx context.Context, req Request, checkReq RequestCheckTx) (Response, ResponseCheckTx, error)

DeliverTx(ctx context.Context, req Request) (Response, error)

SimulateTx(ctx context.Context, req Request (Response, error)
}

type Middleware func(Handler)

Handler
where we define the following arguments and return types:
type Request struct {
    Tx      sdk.Tx
	TxBytes []byte
}

type Response struct {
    GasWanted uint64
	GasUsed   uint64
	// MsgResponses is an array containing each Msg service handler's response
	// type, packed in an Any. This will get proto-serialized into the `Data` field
	// in the ABCI Check/DeliverTx responses.
	MsgResponses []*codectypes.Any
	Log          string
	Events       []abci.Event
}

type RequestCheckTx struct {
    Type abci.CheckTxType
}

type ResponseCheckTx struct {
    Priority int64
}
Please note that because CheckTx handles separate logic related to mempool priotization, its signature is different than DeliverTx and SimulateTx. BaseApp holds a reference to a tx.Handler:
type BaseApp  struct {
    // other fields
    txHandler tx.Handler
}
Baseapp’s ABCI {Check,Deliver}Tx() and Simulate() methods simply call app.txHandler.{Check,Deliver,Simulate}Tx() with the relevant arguments. For example, for DeliverTx:
func (app *BaseApp)

DeliverTx(req abci.RequestDeliverTx)

abci.ResponseDeliverTx {
    var abciRes abci.ResponseDeliverTx
    ctx := app.getContextForTx(runTxModeDeliver, req.Tx)

res, err := app.txHandler.DeliverTx(ctx, tx.Request{
    TxBytes: req.Tx
})
    if err != nil {
    abciRes = sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)

return abciRes
}

abciRes, err = convertTxResponseToDeliverTx(res)
    if err != nil {
    return sdkerrors.ResponseDeliverTx(err, uint64(res.GasUsed), uint64(res.GasWanted), app.trace)
}

return abciRes
}

// 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
}

// 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
})
}
The implementations are similar for BaseApp.CheckTx and BaseApp.Simulate. baseapp.txHandler’s three methods’ implementations can obviously be monolithic functions, but for modularity we propose a middleware composition design, where a middleware is simply a function that takes a tx.Handler, and returns another tx.Handler wrapped around the previous one.

Implementing a Middleware

In practice, middlewares are created by Go function that takes as arguments some parameters needed for the middleware, and returns a tx.Middleware. For example, for creating an arbitrary MyMiddleware, we can implement:
// myTxHandler is the tx.Handler of this middleware. Note that it holds a
// reference to the next tx.Handler in the stack.
type myTxHandler struct {
    // next is the next tx.Handler in the middleware stack.
    next tx.Handler
    // some other fields that are relevant to the middleware can be added here
}

// NewMyMiddleware returns a middleware that does this and that.
func NewMyMiddleware(arg1, arg2)

tx.Middleware {
    return func (txh tx.Handler)

tx.Handler {
    return myTxHandler{
    next: txh,
            // optionally, set arg1, arg2... if they are needed in the middleware
}
 
}
}

// Assert myTxHandler is a tx.Handler.
var _ tx.Handler = myTxHandler{
}

func (h myTxHandler)

CheckTx(ctx context.Context, req Request, checkReq RequestcheckTx) (Response, ResponseCheckTx, error) {
    // CheckTx specific pre-processing logic

    // run the next middleware
    res, checkRes, err := txh.next.CheckTx(ctx, req, checkReq)

    // CheckTx specific post-processing logic

    return res, checkRes, err
}

func (h myTxHandler)

DeliverTx(ctx context.Context, req Request) (Response, error) {
    // DeliverTx specific pre-processing logic

    // run the next middleware
    res, err := txh.next.DeliverTx(ctx, tx, req)

    // DeliverTx specific post-processing logic

    return res, err
}

func (h myTxHandler)

SimulateTx(ctx context.Context, req Request) (Response, error) {
    // SimulateTx specific pre-processing logic

    // run the next middleware
    res, err := txh.next.SimulateTx(ctx, tx, req)

    // SimulateTx specific post-processing logic

    return res, err
}

Composing Middlewares

While BaseApp simply holds a reference to a tx.Handler, this tx.Handler itself is defined using a middleware stack. The Cosmos SDK exposes a base (i.e. innermost) tx.Handler called RunMsgsTxHandler, which executes messages. Then, the app developer can compose multiple middlewares on top on the base tx.Handler. Each middleware can run pre-and-post-processing logic around its next middleware, as described in the section above. Conceptually, as an example, given the middlewares A, B, and C and the base tx.Handler H the stack looks like:
A.pre
    B.pre
        C.pre
            H # The base tx.handler, for example `RunMsgsTxHandler`
        C.post
    B.post
A.post
We define a ComposeMiddlewares function for composing middlewares. It takes the base handler as first argument, and middlewares in the “outer to inner” order. For the above stack, the final tx.Handler is:
txHandler := middleware.ComposeMiddlewares(H, A, B, C)
The middleware is set in BaseApp via its SetTxHandler setter:
// simapp/app.go
    txHandler := middleware.ComposeMiddlewares(...)

app.SetTxHandler(txHandler)
The app developer can define their own middlewares, or use the Cosmos SDK’s pre-defined middlewares from middleware.NewDefaultTxHandler().

Middlewares Maintained by the Cosmos SDK

While the app developer can define and compose the middlewares of their choice, the Cosmos SDK provides a set of middlewares that caters for the ecosystem’s most common use cases. These middlewares are:
MiddlewareDescription
RunMsgsTxHandlerThis is the base tx.Handler. It replaces the old baseapp’s runMsgs, and executes a transaction’s Msgs.
TxDecoderMiddlewareThis middleware takes in transaction raw bytes, and decodes them into a sdk.Tx. It replaces the baseapp.txDecoder field, so that BaseApp stays as thin as possible. Since most middlewares read the contents of the sdk.Tx, the TxDecoderMiddleware should be run first in the middleware stack.
{Antehandlers}Each antehandler is converted to its own middleware. These middlewares perform signature verification, fee deductions and other validations on the incoming transaction.
IndexEventsTxMiddlewareThis is a simple middleware that chooses which events to index in Tendermint. Replaces baseapp.indexEvents (which unfortunately still exists in baseapp too, because it’s used to index Begin/EndBlock events)
RecoveryTxMiddlewareThis index recovers from panics. It replaces baseapp.runTx’s panic recovery described in ADR-022.
GasTxMiddlewareThis replaces the Setup Antehandler. It sets a GasMeter on sdk.Context. Note that before, GasMeter was set on sdk.Context inside the antehandlers, and there was some mess around the fact that antehandlers had their own panic recovery system so that the GasMeter could be read by baseapp’s recovery system. Now, this mess is all removed: one middleware sets GasMeter, another one handles recovery.

Similarities and Differences between Antehandlers and Middlewares

The middleware-based design builds upon the existing antehandlers design described in ADR-010. Even though the final decision of ADR-010 was to go with the “Simple Decorators” approach, the middleware design is actually very similar to the other Decorator Pattern proposal, also used in weave.

Similarities with Antehandlers

  • Designed as chaining/composing small modular pieces.
  • Allow code reuse for {Check,Deliver}Tx and for Simulate.
  • Set up in app.go, and easily customizable by app developers.
  • Order is important.

Differences with Antehandlers

  • The Antehandlers are run before Msg execution, whereas middlewares can run before and after.
  • The middleware approach uses separate methods for {Check,Deliver,Simulate}Tx, whereas the antehandlers pass a simulate bool flag and uses the sdkCtx.Is{Check,Recheck}Tx() flags to determine in which transaction mode we are.
  • The middleware design lets each middleware hold a reference to the next middleware, whereas the antehandlers pass a next argument in the AnteHandle method.
  • The middleware design use Go’s standard context.Context, whereas the antehandlers use sdk.Context.

Consequences

Backwards Compatibility

Since this refactor removes some logic away from BaseApp and into middlewares, it introduces API-breaking changes for app developers. Most notably, instead of creating an antehandler chain in app.go, app developers need to create a middleware stack:
- anteHandler, err := ante.NewAnteHandler(
-    ante.HandlerOptions{
-        AccountKeeper:   app.AccountKeeper,
-        BankKeeper:      app.BankKeeper,
-        SignModeHandler: encodingConfig.TxConfig.SignModeHandler(),
-        FeegrantKeeper:  app.FeeGrantKeeper,
-        SigGasConsumer:  ante.DefaultSigVerificationGasConsumer,
-    },
-)
+txHandler, err := authmiddleware.NewDefaultTxHandler(authmiddleware.TxHandlerOptions{
+    Debug:             app.Trace(),
+    IndexEvents:       indexEvents,
+    LegacyRouter:      app.legacyRouter,
+    MsgServiceRouter:  app.msgSvcRouter,
+    LegacyAnteHandler: anteHandler,
+    TxDecoder:         encodingConfig.TxConfig.TxDecoder,
+})
if err != nil {
    panic(err)
}
- app.SetAnteHandler(anteHandler)
+ app.SetTxHandler(txHandler)
Other more minor API breaking changes will also be provided in the CHANGELOG. As usual, the Cosmos SDK will provide a release migration document for app developers. This ADR does not introduce any state-machine-, client- or CLI-breaking changes.

Positive

  • Allow custom logic to be run before an after Msg execution. This enables the tips and gas refund uses cases, and possibly other ones.
  • Make BaseApp more lightweight, and defer complex logic to small modular components.
  • Separate paths for {Check,Deliver,Simulate}Tx with different returns types. This allows for improved readability (replace if sdkCtx.IsRecheckTx() && !simulate {...} with separate methods) and more flexibility (e.g. returning a priority in ResponseCheckTx).

Negative

  • It is hard to understand at first glance the state updates that would occur after a middleware runs given the sdk.Context and tx. A middleware can have an arbitrary number of nested middleware being called within its function body, each possibly doing some pre- and post-processing before calling the next middleware on the chain. Thus to understand what a middleware is doing, one must also understand what every other middleware further along the chain is also doing, and the order of middlewares matters. This can get quite complicated to understand.
  • API-breaking changes for app developers.

Neutral

No neutral consequences.

Further Discussions

  • #9934 Decomposing BaseApp’s other ABCI methods into middlewares.
  • Replace sdk.Tx interface with the concrete protobuf Tx type in the tx.Handler methods signature.

Test Cases

We update the existing baseapp and antehandlers tests to use the new middleware API, but keep the same test cases and logic, to avoid introducing regressions. Existing CLI tests will also be left untouched. For new middlewares, we introduce unit tests. Since middlewares are purposefully small, unit tests suit well.

References