变更记录
- 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 中:
tx.Handler 的引用:
{Check,Deliver}Tx() 和 Simulate() 方法只需使用相关参数调用 app.txHandler.{Check,Deliver,Simulate}Tx()。例如,对 DeliverTx 而言:
BaseApp.CheckTx 和 BaseApp.Simulate 的实现与此类似。
baseapp.txHandler 这三个方法的实现当然可以是单体函数,但为了模块化,我们提出一种中间件组合设计:中间件本质上只是一个函数,它接收一个 tx.Handler,并返回一个包装在前一个 tx.Handler 外层的新的 tx.Handler。
实现中间件
在实践中,中间件由 Go 函数创建,该函数接收中间件所需的一些参数,并返回一个tx.Middleware。
例如,为了创建一个任意的 MyMiddleware,我们可以这样实现:
组合中间件
虽然 BaseApp 只是持有一个对tx.Handler 的引用,但这个 tx.Handler 本身是通过中间件栈定义出来的。Cosmos SDK 暴露了一个基础的(也就是最内层的)tx.Handler,名为 RunMsgsTxHandler,用于执行消息。
随后,应用开发者可以在这个基础 tx.Handler 之上组合多个中间件。每个中间件都可以像上一节所述的那样,在其下一个中间件外围运行前置和后置处理逻辑。从概念上看,假设有中间件 A、B、C 和基础 tx.Handler H,那么栈结构如下:
ComposeMiddlewares 函数来组合中间件。它接收基础 handler 作为第一个参数,随后按“从外到内”的顺序接收中间件。对于上面的栈,最终的 tx.Handler 为:
SetTxHandler setter 设置到 BaseApp 中:
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 栈:
正面影响
- 允许在
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 被有意设计得较小,因此单元测试非常适合。参考资料
- 初始讨论:链接
- 实现:#9920 BaseApp 重构 和 #10028 Antehandler 迁移
Changelog
- 20.08.2021: Initial draft.
- 07.12.2021: Update
tx.Handlerinterface (#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 BaseApprunTx 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:
tx.Handler:
{Check,Deliver}Tx() and Simulate() methods simply call app.txHandler.{Check,Deliver,Simulate}Tx() with the relevant arguments. For example, for DeliverTx:
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 atx.Middleware.
For example, for creating an arbitrary MyMiddleware, we can implement:
Composing Middlewares
While BaseApp simply holds a reference to atx.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:
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:
SetTxHandler setter:
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:| Middleware | Description |
|---|---|
| RunMsgsTxHandler | This is the base tx.Handler. It replaces the old baseapp’s runMsgs, and executes a transaction’s Msgs. |
| TxDecoderMiddleware | This 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. |
| IndexEventsTxMiddleware | This 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) |
| RecoveryTxMiddleware | This index recovers from panics. It replaces baseapp.runTx’s panic recovery described in ADR-022. |
| GasTxMiddleware | This 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}Txand forSimulate. - Set up in
app.go, and easily customizable by app developers. - Order is important.
Differences with Antehandlers
- The Antehandlers are run before
Msgexecution, whereas middlewares can run before and after. - The middleware approach uses separate methods for
{Check,Deliver,Simulate}Tx, whereas the antehandlers pass asimulate boolflag and uses thesdkCtx.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
nextargument in theAnteHandlemethod. - The middleware design use Go’s standard
context.Context, whereas the antehandlers usesdk.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 inapp.go, app developers need to create a middleware stack:
Positive
- Allow custom logic to be run before an after
Msgexecution. 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}Txwith different returns types. This allows for improved readability (replaceif sdkCtx.IsRecheckTx() && !simulate {...}with separate methods) and more flexibility (e.g. returning apriorityinResponseCheckTx).
Negative
- It is hard to understand at first glance the state updates that would occur after a middleware runs given the
sdk.Contextandtx. 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.Txinterface with the concrete protobuf Tx type in thetx.Handlermethods 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
- Initial discussion: Link
- Implementation: #9920 BaseApp refactor and #10028 Antehandlers migration