Skip to content

Make middlewares less confusing #11955

Closed
Closed
@amaury1093

Description

Summary

Reconsider the middleware "decorator" approach as described in ADR-045 in favor of a less confusing model.

Problem Definition

Multiple feedback concur that middlewares are hard to reason about (cf Bez, Amaury, Yihuang).

Some problems:

1. Middlewares have dependencies between them, but those dependencies are not clearly defined.

For example, SigVerificationMiddleware always needs to be run after SetPubKeyMiddleware, but the only place where this dependency is defined is in the godoc comment. Or GasTxMiddleware: any other middlewares that access the GasMeter needs to run after it. Again, this is only enforced by go comments.

If any chain puts the middlewares in wrong order, then there are security risks.

2. It's hard to track pre- and post-runMsg logic for the whole middleware stack.

If middlewareA is below middlewareB in the stack, it's slightly unclear which which one's pre/post-hook runs after the other's pre/post-hook. But this gets impossible to track with 15+ middlewares. For example, store branching and when to write back to store is hard to get right with the current composition design: #11942.

In ConsumeBlockGasMiddleware, we also use a defer func(). Good luck tracking that ordering with other post-hook logic.

Middleware dependencies

Here's a quick overview of the dependencies between middlewares. \t⬑ means "depends on" i.e. "needs to run after", and items on the same level of indentation can be run parallely (have no dependencies).

Store Branching (all middlewares are in a store branch, store writes only happens if the whole branch succeeds)
⬑ Antehandler Branch (what corresponds to antehandlers in the old baseapp)
    ⬑ RunMsgs Branch (what corresponds to runMsgs in the old baseapp)

Where:
Antehandler Branch:
    ⬑TxDecoder (all middlewares need a decoded tx)
        ⬑ Gas (all middlewares that track gas need this one)
            ⬑ Recovery (not 100% required so high in the stack, but good to have, in case any middleware panics. needs Gas)
                ⬑ ExtensionOptions
                ⬑ ValidateBasic
                ⬑ TxTimeoutHeight
                ⬑ ValidateMemo
                ⬑ ConsumeTxSizeGas (needs gas)
                ⬑ IncrementSequence (needs gas)
                ⬑ IndexEvents (all middlewares that emit events needs this one)
                    ⬑ SetPubKey (needs event emitting)
                        ⬑ ValidateSigCount (needs pubkey)
                        ⬑ SigGasConsume (needs pubkey)
                        ⬑ SigVerification (needs pubkey)
                    ⬑ DeductFeeMiddleware (needs events)

And:
RunMsgs Branch:
    ⬑ RunMsgs
        ⬑ Tips
            ⬑ (Future: fee refund)
                ⬑ ConsumeBlockGas (add tx gas to the block GasMeter, needs to be run after ALL tx gas consumption, also runs on failed txs)

Proposals

In any of the 2 proposals below, we should consider removing the possibility of having both pre and post logic for middlewares. A middleware should either be a pre-middleware, or a post-middleware, not both at the same time.

Proposal 1: Build a dependency graph between middlewares

App developers specify relative dependencies of middlewares.

Then, an algorithm will propose a graph that satisfies all the defined dependencies. Finally, the SDK will figure out the sequential ordering of middlewares by traversing the dependency graph, in a way that this sequential order satisfies all the initial dependencies.

Example:

// Syntax:
// <middleware>: [<array of dependencies>]
SigVerification: [SetPubKey, IndexEvents, Gas]
SetPubKey: [IndexEvents]
IndexEvents: [TxDecoder]
Gas: [TxDecoder]

In this case the SDK will know that any of the 2 following orderings are valid:

  • TxDecoder, Gas, IndexEvents, SetPubKey, SigVerification
  • TxDecoder, IndexEvents, Gas, SetPubKey, SigVerification

Done in v0.46 Proposal 2: Fallback to something similar to the old baseapp

We could also define 2 middleware stacks:

  • one before runMsgs, exactly like that old antehandlers
  • one after runMsgs (for tips, and in the future for e.g. fee refunds)

This would make the code easier to reason about.

Further Notes

If we go with the dependency graph solution, we could use the same graph design for BeginBlockers, EndBlockers, InitChain etc (as proposed initially by @ValarDragon): instead of middleware dependencies, it would be module dependencies.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

S:proposedT: API BreakingBreaking changes that impact APIs and the SDK only (not state machine).

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions