变更记录

  • 2019 年 8 月 31 日:初始草案
  • 2021 年 9 月 14 日:已被 ADR-045 取代

状态

已被 ADR-045 取代

背景

当前的 AnteHandler 设计允许用户要么使用 x/auth 提供的默认 AnteHandler,要么从头构建自己的 AnteHandler。理想情况下,AnteHandler 功能应拆分为多个模块化函数,并能够与自定义 ante-function 链式组合,这样用户在希望实现自定义行为时,就不必重写通用的 antehandler 逻辑。 例如,假设某个用户想实现自定义的签名校验逻辑。在当前代码库中,用户必须从头编写自己的 Antehandler,大量重复实现相同代码,然后在 baseapp 中设置自己定制的单体 antehandler。相反,我们希望允许用户在必要时指定自定义行为,并以尽可能模块化且灵活的方式将其与默认的 ante-handler 功能组合起来。

提案

按模块划分的 AnteHandler

一种做法是使用 ModuleManager,并让每个模块在需要自定义 antehandler 逻辑时实现自己的 antehandler。随后,可以像为 BeginBlockers 和 EndBlockers 指定顺序一样,为 ModuleManager 传入 AnteHandler 的执行顺序。ModuleManager 返回一个单一的 AnteHandler 函数,该函数接收一个 tx,并按指定顺序运行各模块的 AnteHandle。模块管理器的 AnteHandler 会被设置为 baseapp 的 AnteHandler。 优点:
  1. 实现简单
  2. 复用了现有的 ModuleManager 架构
缺点:
  1. 虽然提升了粒度,但最细仍然只能到模块级别。例如,如果 auth 的 AnteHandle 函数负责校验 memo 和签名,用户仍然无法在保留 auth 其余 AnteHandle 功能的同时,仅替换签名校验功能。
  2. 模块 AnteHandler 是依次运行的。一个 AnteHandler 无法包装或“装饰”另一个 AnteHandler。

装饰器模式

weave project 通过装饰器模式实现 AnteHandler 的模块化。其接口设计如下:
// Decorator wraps a Handler to provide common functionality
// like authentication, or fee-handling, to many Handlers
type Decorator interface {
    Check(ctx Context, store KVStore, tx Tx, next Checker) (*CheckResult, error)

Deliver(ctx Context, store KVStore, tx Tx, next Deliverer) (*DeliverResult, error)
}
每个 decorator 的作用都类似于模块化后的 Cosmos SDK antehandler 函数,但它可以接收一个 next 参数,该参数可以是另一个 decorator,也可以是一个 Handler(后者不接收 next 参数)。这些 decorator 可以链式连接,链中的前一个 decorator 将后一个 decorator 作为 next 参数传入。链路最终以一个 Router 结束,Router 接收 tx 并将其路由到相应的 msg handler。 这种方案的一个关键优势是,一个 Decorator 可以围绕下一个 Checker/Deliverer 包装自己的内部逻辑。一个 weave Decorator 可能会这样做:
// Example Decorator's Deliver function
func (example Decorator)

Deliver(ctx Context, store KVStore, tx Tx, next Deliverer) {
    // Do some pre-processing logic

    res, err := next.Deliver(ctx, store, tx)

    // Do some post-processing logic given the result and error
}
优点:
  1. Weave Decorator 可以包装链中的下一个 decorator/handler。既能做前处理又能做后处理,这种能力在某些场景下会很有用。
  2. 提供了上一个方案无法实现的嵌套式模块化结构,同时也支持像上一个方案那样线性、依次执行的结构。
缺点:
  1. 在第一眼阅读时,很难理解一个 Decorator 运行后 ctx、store 和 tx 会发生哪些状态更新。一个 Decorator 可以在其函数体内调用任意数量的嵌套 Decorator,每个都可能在调用链中的下一个 decorator 之前和之后执行一些处理。因此,要理解某个 Decorator 在做什么,也必须理解链路后续所有其他 decorator 在做什么。这会变得相当复杂。相比之下,线性、依次执行的方式虽然能力较弱,但可能更容易推理。

链式微函数

Weave 方案的优点在于 Decorator 可以写得非常简洁,而将它们链起来后可以实现最大的可定制性。不过,这种嵌套结构可能会变得相当复杂,因此难以推理。 另一种做法是将 AnteHandler 功能拆分为职责非常聚焦的“微函数”,同时保留 ModuleManager 方案中的那种依次执行顺序。 然后,我们可以提供一种方式把这些微函数串联起来,使其按顺序逐个运行。模块可以定义多个 ante 微函数,同时还可以提供一个默认的模块级 AnteHandler,为这些微函数给出一个默认、推荐的执行顺序。 用户可以通过简单使用 ModuleManager 来轻松安排 AnteHandler 的顺序。ModuleManager 接收一个 AnteHandler 列表,并返回一个单一的 AnteHandler,按列表给定的顺序执行每个 AnteHandler。如果用户接受每个模块的默认顺序,那么只需要提供一个包含各模块 antehandler 的列表即可(与 BeginBlocker 和 EndBlocker 完全相同)。 但如果用户希望调整顺序,或以任何方式新增、修改或删除 ante 微函数;他们始终可以定义自己的 ante 微函数,并显式将其加入传给 module manager 的列表中。

默认工作流

这是一个用户选择不编写任何自定义微函数时的 AnteHandler 示例。
Cosmos SDK 代码
// Chains together a list of AnteHandler micro-functions that get run one after the other.
// Returned AnteHandler will abort on first error.
func Chainer(order []AnteHandler)

AnteHandler {
    return func(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    for _, ante := range order {
    ctx, err := ante(ctx, tx, simulate)
    if err != nil {
    return ctx, err
}
 
}

return ctx, err
}
}
// AnteHandler micro-function to verify signatures
func VerifySignatures(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // verify signatures
    // Returns InvalidSignature Result and abort=true if sigs invalid
    // Return OK result and abort=false if sigs are valid
}

// AnteHandler micro-function to validate memo
func ValidateMemo(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // validate memo
}

// Auth defines its own default ante-handler by chaining its micro-functions in a recommended order
AuthModuleAnteHandler := Chainer([]AnteHandler{
    VerifySignatures, ValidateMemo
})
// Distribution micro-function to deduct fees from tx
func DeductFees(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // Deduct fees from tx
    // Abort if insufficient funds in account to pay for fees
}

// Distribution micro-function to check if fees > mempool parameter
func CheckMempoolFees(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // If CheckTx: Abort if the fees are less than the mempool's minFee parameter
}

// Distribution defines its own default ante-handler by chaining its micro-functions in a recommended order
DistrModuleAnteHandler := Chainer([]AnteHandler{
    CheckMempoolFees, DeductFees
})
type ModuleManager struct {
    // other fields
    AnteHandlerOrder []AnteHandler
}

func (mm ModuleManager)

GetAnteHandler()

AnteHandler {
    retun Chainer(mm.AnteHandlerOrder)
}
用户代码
// Note: Since user is not making any custom modifications, we can just SetAnteHandlerOrder with the default AnteHandlers provided by each module in our preferred order
moduleManager.SetAnteHandlerOrder([]AnteHandler(AuthModuleAnteHandler, DistrModuleAnteHandler))

app.SetAnteHandler(mm.GetAnteHandler())

自定义工作流

这是一个用户希望实现自定义 antehandler 逻辑时的工作流示例。在这个例子中,用户希望实现自定义签名校验,并调整 antehandler 的顺序,使 validate memo 在签名校验之前运行。
用户代码
// User can implement their own custom signature verification antehandler micro-function
func CustomSigVerify(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // do some custom signature verification logic
}
// Micro-functions allow users to change order of when they get executed, and swap out default ante-functionality with their own custom logic.
// Note that users can still chain the default distribution module handler, and auth micro-function along with their custom ante function
moduleManager.SetAnteHandlerOrder([]AnteHandler(ValidateMemo, CustomSigVerify, DistrModuleAnteHandler))
优点:
  1. 使 ante 功能尽可能模块化。
  2. 对于不需要自定义 ante-functionality 的用户来说,antehandler 的工作方式与 ModuleManager 中的 BeginBlock 和 EndBlock 几乎没有区别。
  3. 仍然易于理解
缺点:
  1. 不能像 Weave 那样使用 decorator 包装 antehandler。

简化装饰器

这种做法借鉴了 Weave 的装饰器设计,同时试图尽量减少对 Cosmos SDK 的破坏性变更,并最大化保持简单性。与 Weave decorator 一样,这种方案允许一个 AnteDecorator 包装下一个 AnteHandler,并对结果进行前处理和后处理。这很有用,因为 decorator 可以在 AnteHandler 返回后执行 defer/清理逻辑,也可以在此之前执行一些初始化工作。与 Weave decorator 不同,这些 AnteDecorator 函数只能包装 AnteHandler,而不能包装整个 handler 执行路径。这是有意为之,因为我们希望不同模块的 decorator 只对 tx 执行认证/校验。然而,我们不希望 decorator 能够包装并修改 MsgHandler 的结果。 此外,这种方案不会破坏任何 Cosmos SDK 核心 API。由于我们保留了 AnteHandler 的概念,并且仍然只在 baseapp 中设置一个单一的 AnteHandler,因此 decorator 只是为那些希望获得更高定制能力的用户额外提供的一种方式。采用这种方案时,模块的 API(尤其是 x/auth)可能会发生破坏性变更,但核心 API 保持不变。 允许可链式组合的 Decorator 接口,用于创建一个 Cosmos SDK AnteHandler。 这使用户可以在两种方式之间选择:要么自行实现 AnteHandler 并将其设置到 baseapp 中;要么使用装饰器模式,按自己希望的顺序,将自定义 decorator 与 Cosmos SDK 提供的 decorator 链接起来。
// An AnteDecorator wraps an AnteHandler, and can do pre- and post-processing on the next AnteHandler
type AnteDecorator interface {
    AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error)
}
// ChainAnteDecorators will recursively link all of the AnteDecorators in the chain and return a final AnteHandler function
// This is done to preserve the ability to set a single AnteHandler function in the baseapp.
func ChainAnteDecorators(chain ...AnteDecorator)

AnteHandler {
    if len(chain) == 1 {
    return func(ctx Context, tx Tx, simulate bool) {
    chain[0].AnteHandle(ctx, tx, simulate, nil)
}
 
}

return func(ctx Context, tx Tx, simulate bool) {
    chain[0].AnteHandle(ctx, tx, simulate, ChainAnteDecorators(chain[1:]))
}
}

示例代码

定义 AnteDecorator 函数
// Setup GasMeter, catch OutOfGasPanic and handle appropriately
type SetUpContextDecorator struct{
}

func (sud SetUpContextDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    ctx.GasMeter = NewGasMeter(tx.Gas)

defer func() {
        // recover from OutOfGas panic and handle appropriately
}

return next(ctx, tx, simulate)
}

// Signature Verification decorator. Verify Signatures and move on
type SigVerifyDecorator struct{
}

func (svd SigVerifyDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    // verify sigs. Return error if invalid

    // call next antehandler if sigs ok
    return next(ctx, tx, simulate)
}

// User-defined Decorator. Can choose to pre- and post-process on AnteHandler
type UserDefinedDecorator struct{
    // custom fields
}

func (udd UserDefinedDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    // pre-processing logic

    ctx, err = next(ctx, tx, simulate)

    // post-processing logic
}
链接 AnteDecorators 以创建最终的 AnteHandler。在 baseapp 中设置这个 AnteHandler。
// Create final antehandler by chaining the decorators together
    antehandler := ChainAnteDecorators(NewSetUpContextDecorator(), NewSigVerifyDecorator(), NewUserDefinedDecorator())

// Set chained Antehandler in the baseapp
bapp.SetAnteHandler(antehandler)
优点:
  1. 允许某个 decorator 对下一个 AnteHandler 进行前置和后置处理,类似于 Weave 的设计。
  2. 不需要破坏 baseapp API。用户如果愿意,仍然可以设置单个 AnteHandler。
缺点:
  1. decorator 模式可能会形成难以理解的深层嵌套结构,不过可以通过在 ChainAnteDecorators 函数中显式列出 decorator 顺序来缓解这一问题。
  2. 没有利用 ModuleManager 的设计。由于 BeginBlocker/EndBlocker 已经在使用这一设计,这个提案看起来与该设计模式并不一致。

后果

由于每种方案都已经分别写明了优缺点,因此本节省略。

参考资料


Changelog

  • 2019 Aug 31: Initial draft
  • 2021 Sep 14: Superseded by ADR-045

Status

SUPERSEDED by ADR-045

Context

The current AnteHandler design allows users to either use the default AnteHandler provided in x/auth or to build their own AnteHandler from scratch. Ideally AnteHandler functionality is split into multiple, modular functions that can be chained together along with custom ante-functions so that users do not have to rewrite common antehandler logic when they want to implement custom behavior. For example, let’s say a user wants to implement some custom signature verification logic. In the current codebase, the user would have to write their own Antehandler from scratch largely reimplementing much of the same code and then set their own custom, monolithic antehandler in the baseapp. Instead, we would like to allow users to specify custom behavior when necessary and combine them with default ante-handler functionality in a way that is as modular and flexible as possible.

Proposals

Per-Module AnteHandler

One approach is to use the ModuleManager and have each module implement its own antehandler if it requires custom antehandler logic. The ModuleManager can then be passed in an AnteHandler order in the same way it has an order for BeginBlockers and EndBlockers. The ModuleManager returns a single AnteHandler function that will take in a tx and run each module’s AnteHandle in the specified order. The module manager’s AnteHandler is set as the baseapp’s AnteHandler. Pros:
  1. Simple to implement
  2. Utilizes the existing ModuleManager architecture
Cons:
  1. Improves granularity but still cannot get more granular than a per-module basis. e.g. If auth’s AnteHandle function is in charge of validating memo and signatures, users cannot swap the signature-checking functionality while keeping the rest of auth’s AnteHandle functionality.
  2. Module AnteHandler are run one after the other. There is no way for one AnteHandler to wrap or “decorate” another.

Decorator Pattern

The weave project achieves AnteHandler modularity through the use of a decorator pattern. The interface is designed as follows:
// Decorator wraps a Handler to provide common functionality
// like authentication, or fee-handling, to many Handlers
type Decorator interface {
    Check(ctx Context, store KVStore, tx Tx, next Checker) (*CheckResult, error)

Deliver(ctx Context, store KVStore, tx Tx, next Deliverer) (*DeliverResult, error)
}
Each decorator works like a modularized Cosmos SDK antehandler function, but it can take in a next argument that may be another decorator or a Handler (which does not take in a next argument). These decorators can be chained together, one decorator being passed in as the next argument of the previous decorator in the chain. The chain ends in a Router which can take a tx and route to the appropriate msg handler. A key benefit of this approach is that one Decorator can wrap its internal logic around the next Checker/Deliverer. A weave Decorator may do the following:
// Example Decorator's Deliver function
func (example Decorator)

Deliver(ctx Context, store KVStore, tx Tx, next Deliverer) {
    // Do some pre-processing logic

    res, err := next.Deliver(ctx, store, tx)

    // Do some post-processing logic given the result and error
}
Pros:
  1. Weave Decorators can wrap over the next decorator/handler in the chain. The ability to both pre-process and post-process may be useful in certain settings.
  2. Provides a nested modular structure that isn’t possible in the solution above, while also allowing for a linear one-after-the-other structure like the solution above.
Cons:
  1. It is hard to understand at first glance the state updates that would occur after a Decorator runs given the ctx, store, and tx. A Decorator can have an arbitrary number of nested Decorators being called within its function body, each possibly doing some pre- and post-processing before calling the next decorator on the chain. Thus to understand what a Decorator is doing, one must also understand what every other decorator further along the chain is also doing. This can get quite complicated to understand. A linear, one-after-the-other approach while less powerful, may be much easier to reason about.

Chained Micro-Functions

The benefit of Weave’s approach is that the Decorators can be very concise, which when chained together allows for maximum customizability. However, the nested structure can get quite complex and thus hard to reason about. Another approach is to split the AnteHandler functionality into tightly scoped “micro-functions”, while preserving the one-after-the-other ordering that would come from the ModuleManager approach. We can then have a way to chain these micro-functions so that they run one after the other. Modules may define multiple ante micro-functions and then also provide a default per-module AnteHandler that implements a default, suggested order for these micro-functions. Users can order the AnteHandlers easily by simply using the ModuleManager. The ModuleManager will take in a list of AnteHandlers and return a single AnteHandler that runs each AnteHandler in the order of the list provided. If the user is comfortable with the default ordering of each module, this is as simple as providing a list with each module’s antehandler (exactly the same as BeginBlocker and EndBlocker). If however, users wish to change the order or add, modify, or delete ante micro-functions in anyway; they can always define their own ante micro-functions and add them explicitly to the list that gets passed into module manager.

Default Workflow

This is an example of a user’s AnteHandler if they choose not to make any custom micro-functions.
Cosmos SDK code
// Chains together a list of AnteHandler micro-functions that get run one after the other.
// Returned AnteHandler will abort on first error.
func Chainer(order []AnteHandler)

AnteHandler {
    return func(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    for _, ante := range order {
    ctx, err := ante(ctx, tx, simulate)
    if err != nil {
    return ctx, err
}
 
}

return ctx, err
}
}
// AnteHandler micro-function to verify signatures
func VerifySignatures(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // verify signatures
    // Returns InvalidSignature Result and abort=true if sigs invalid
    // Return OK result and abort=false if sigs are valid
}

// AnteHandler micro-function to validate memo
func ValidateMemo(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // validate memo
}

// Auth defines its own default ante-handler by chaining its micro-functions in a recommended order
AuthModuleAnteHandler := Chainer([]AnteHandler{
    VerifySignatures, ValidateMemo
})
// Distribution micro-function to deduct fees from tx
func DeductFees(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // Deduct fees from tx
    // Abort if insufficient funds in account to pay for fees
}

// Distribution micro-function to check if fees > mempool parameter
func CheckMempoolFees(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // If CheckTx: Abort if the fees are less than the mempool's minFee parameter
}

// Distribution defines its own default ante-handler by chaining its micro-functions in a recommended order
DistrModuleAnteHandler := Chainer([]AnteHandler{
    CheckMempoolFees, DeductFees
})
type ModuleManager struct {
    // other fields
    AnteHandlerOrder []AnteHandler
}

func (mm ModuleManager)

GetAnteHandler()

AnteHandler {
    retun Chainer(mm.AnteHandlerOrder)
}
User Code
// Note: Since user is not making any custom modifications, we can just SetAnteHandlerOrder with the default AnteHandlers provided by each module in our preferred order
moduleManager.SetAnteHandlerOrder([]AnteHandler(AuthModuleAnteHandler, DistrModuleAnteHandler))

app.SetAnteHandler(mm.GetAnteHandler())

Custom Workflow

This is an example workflow for a user that wants to implement custom antehandler logic. In this example, the user wants to implement custom signature verification and change the order of antehandler so that validate memo runs before signature verification.
User Code
// User can implement their own custom signature verification antehandler micro-function
func CustomSigVerify(ctx Context, tx Tx, simulate bool) (newCtx Context, err error) {
    // do some custom signature verification logic
}
// Micro-functions allow users to change order of when they get executed, and swap out default ante-functionality with their own custom logic.
// Note that users can still chain the default distribution module handler, and auth micro-function along with their custom ante function
moduleManager.SetAnteHandlerOrder([]AnteHandler(ValidateMemo, CustomSigVerify, DistrModuleAnteHandler))
Pros:
  1. Allows for ante functionality to be as modular as possible.
  2. For users that do not need custom ante-functionality, there is little difference between how antehandlers work and how BeginBlock and EndBlock work in ModuleManager.
  3. Still easy to understand
Cons:
  1. Cannot wrap antehandlers with decorators like you can with Weave.

Simple Decorators

This approach takes inspiration from Weave’s decorator design while trying to minimize the number of breaking changes to the Cosmos SDK and maximizing simplicity. Like Weave decorators, this approach allows one AnteDecorator to wrap the next AnteHandler to do pre- and post-processing on the result. This is useful since decorators can do defer/cleanups after an AnteHandler returns as well as perform some setup beforehand. Unlike Weave decorators, these AnteDecorator functions can only wrap over the AnteHandler rather than the entire handler execution path. This is deliberate as we want decorators from different modules to perform authentication/validation on a tx. However, we do not want decorators being capable of wrapping and modifying the results of a MsgHandler. In addition, this approach will not break any core Cosmos SDK API’s. Since we preserve the notion of an AnteHandler and still set a single AnteHandler in baseapp, the decorator is simply an additional approach available for users that desire more customization. The API of modules (namely x/auth) may break with this approach, but the core API remains untouched. Allow Decorator interface that can be chained together to create a Cosmos SDK AnteHandler. This allows users to choose between implementing an AnteHandler by themselves and setting it in the baseapp, or use the decorator pattern to chain their custom decorators with the Cosmos SDK provided decorators in the order they wish.
// An AnteDecorator wraps an AnteHandler, and can do pre- and post-processing on the next AnteHandler
type AnteDecorator interface {
    AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error)
}
// ChainAnteDecorators will recursively link all of the AnteDecorators in the chain and return a final AnteHandler function
// This is done to preserve the ability to set a single AnteHandler function in the baseapp.
func ChainAnteDecorators(chain ...AnteDecorator)

AnteHandler {
    if len(chain) == 1 {
    return func(ctx Context, tx Tx, simulate bool) {
    chain[0].AnteHandle(ctx, tx, simulate, nil)
}
 
}

return func(ctx Context, tx Tx, simulate bool) {
    chain[0].AnteHandle(ctx, tx, simulate, ChainAnteDecorators(chain[1:]))
}
}

Example Code

Define AnteDecorator functions
// Setup GasMeter, catch OutOfGasPanic and handle appropriately
type SetUpContextDecorator struct{
}

func (sud SetUpContextDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    ctx.GasMeter = NewGasMeter(tx.Gas)

defer func() {
        // recover from OutOfGas panic and handle appropriately
}

return next(ctx, tx, simulate)
}

// Signature Verification decorator. Verify Signatures and move on
type SigVerifyDecorator struct{
}

func (svd SigVerifyDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    // verify sigs. Return error if invalid

    // call next antehandler if sigs ok
    return next(ctx, tx, simulate)
}

// User-defined Decorator. Can choose to pre- and post-process on AnteHandler
type UserDefinedDecorator struct{
    // custom fields
}

func (udd UserDefinedDecorator)

AnteHandle(ctx Context, tx Tx, simulate bool, next AnteHandler) (newCtx Context, err error) {
    // pre-processing logic

    ctx, err = next(ctx, tx, simulate)

    // post-processing logic
}
Link AnteDecorators to create a final AnteHandler. Set this AnteHandler in baseapp.
// Create final antehandler by chaining the decorators together
    antehandler := ChainAnteDecorators(NewSetUpContextDecorator(), NewSigVerifyDecorator(), NewUserDefinedDecorator())

// Set chained Antehandler in the baseapp
bapp.SetAnteHandler(antehandler)
Pros:
  1. Allows one decorator to pre- and post-process the next AnteHandler, similar to the Weave design.
  2. Do not need to break baseapp API. Users can still set a single AnteHandler if they choose.
Cons:
  1. Decorator pattern may have a deeply nested structure that is hard to understand, this is mitigated by having the decorator order explicitly listed in the ChainAnteDecorators function.
  2. Does not make use of the ModuleManager design. Since this is already being used for BeginBlocker/EndBlocker, this proposal seems unaligned with that design pattern.

Consequences

Since pros and cons are written for each approach, it is omitted from this section

References