App
一条基于cosmosSDK的链,就是一个App,App是最顶层单元,负责实现ABCI接口。App以module方式管理。
Tendermint通过ABCI和App通讯,核心的接口如下:
- CheckTx。验证tx,成功则加入交易池。
- BeginBlock。执行Block之前调用。
- DeliverTx。执行Block时调用,对每个tx遍历调用。
- EndBlock。执行Block结束后调用。
- Commit。存储落盘。
二者具体关系如图:
-------App-------- a b c i ----Tendermint-----
也就是说,Tendermint调用App,App实现ABCI对应的接口,ABCI接口的执行流程,形成了App的核心逻辑。
其中CosmosSDK提供了一个baseApp,编写了基础的核心逻辑,用户的自定义App往往继承自baseApp。
App以module方式管理,包括以下核心组件:
- codec。编码。
- store。存储。
- keeper。各个模块的具体的功能。
- anteHandler。验证交易的有效性,被CheckTx和DeliverTx调用。
- router。各个交易执行的具体路由。
- module manager。模块管理者。
Module
App中,通过module来管理各个功能模块,比如evm是一个module,ibc是另一个module。代码一般存放在x目录下。
一个module的组成有以下核心组件:
- keeper。keeper操作store,对外提供模块的具体功能。
- store。本模块相关的k,v数据库。
- handler。提供msg(tx)的执行入口。
- module。提供module级别的对外接口,如module名字,beginBlock, endBlock等函数,module级别的借口往往会调用具体的keeper接口。
需要注意的时,module中可能会使用到别的module的keeper。cosmos采用的方式是在app中创建和管理所有keeper,然后在创建module时,把对应的keeper分配出去。
因此module和keeper并不是严格的上下层级关系,而是一种引用关系,即module中包含keeper,但该keeper是由app创建,而不是module内部创建的。
Module manager
所有module会注册在App的moduleManager中。module manager是一个module的集合。作用是统一调用module级别的接口,如beginBlock, endBlock等函数。
app在执行业务流程,比如执行beginBlock的时候,会调用app的beginBlocker。
app.beginBlocker会调用module manager中的BeginBlock函数,然后遍历所有的module,调用对应的BeginBlock函数,如下所示:
// BeginBlock performs begin block functionality for all modules. It creates a // child context with an event manager to aggregate events emitted from all // modules. func (m *Manager) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { ctx = ctx.WithEventManager(sdk.NewEventManager()) for _, moduleName := range m.OrderBeginBlockers { module, ok := m.Modules[moduleName].(BeginBlockAppModule) if ok { module.BeginBlock(ctx, req) } } return abci.ResponseBeginBlock{ Events: ctx.EventManager().ABCIEvents(), } }
Keeper
keeper是module的下属结构。但由于module中可能会使用到别的module的keeper。cosmos采用的方式是在app中创建和管理所有keeper,然后在创建module时,把对应的keeper分配出去。
因此module和keeper并不是严格的上下层级关系,而是一种引用关系,即module中包含keeper,但该keeper是由app创建,而不是module内部创建的。
anteHandler
anterhandler并不是由module管理的,代码一般不在x目录下,而是单独的ante目录。负责在交易被执行之前,验证交易的有效性,被CheckTx和DeliverTx调用。
anteHandler虽然不是module管理,个人认为是历史原因。一开始的设计认为anteHandle是公共功能,不需要按照module管理。但随着cosmosSDK的发展,anteHandler其实也一定程度按照module划分。
比如在NewAnteHandler代码中,会区分EVMAnteHandler, CosmosAnteHandler。
switch typeURL := opts[0].GetTypeUrl(); typeURL { case "/ethermint.evm.v1.ExtensionOptionsEthereumTx": // handle as *evmtypes.MsgEthereumTx anteHandler = newEVMAnteHandler(options) case "/ethermint.types.v1.ExtensionOptionsWeb3Tx": // handle as normal Cosmos SDK tx, except signature is checked for EIP712 representation anteHandler = newLegacyCosmosAnteHandlerEip712(options) case "/ethermint.types.v1.ExtensionOptionDynamicFeeTx": // cosmos-sdk tx with dynamic fee extension anteHandler = newCosmosAnteHandler(options) default: return ctx, errorsmod.Wrapf( errortypes.ErrUnknownExtensionOptions, "rejecting tx with unsupported extension option: %s", typeURL, ) }
router和handler
交易的具体执行在各个module中,在module中,会有一个handler文件,提供一个NewHandler接口,用于处理具体的tx(msg),如下:
// NewHandler returns a handler for Ethermint type messages.func NewHandler(server types.MsgServer) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) (result *sdk.Result, err error) { ctx = ctx.WithEventManager(sdk.NewEventManager()) switch msg := msg.(type) { case *types.MsgEthereumTx: res, err := server.EthereumTx(sdk.WrapSDKContext(ctx), msg) return sdk.WrapServiceResult(ctx, res, err) case *types.MsgUpdateParams: res, err := server.UpdateParams(sdk.WrapSDKContext(ctx), msg) return sdk.WrapServiceResult(ctx, res, err) default: err := errorsmod.Wrapf(errortypes.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg) return nil, err } } }
这些handler会通过moduleManager注册在App的router中,在baseApp执行交易时(runMsgs)时,会取出具体的handler,处理交易,如下:
for i, msg := range msgs { // match message route msgRoute := msg.Route() handler := app.router.Route(msgRoute) if handler == nil { return sdk.ErrUnknownRequest("unrecognized message type: " + msgRoute).Result() } var msgResult sdk.Result ... }
在cosmos把编码从amino升级到proto的时候,router的处理原理一样,但是注册方式发生了变化,通过module的函数注册:
func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterMsgServer(cfg.MsgServer(), am.keeper) ... }
module的入口也不是handler文件,而是msg_server文件,通过在tx.proto中定义handler和消息类型,最终会在router中保存到msg_server的route。
Store
App中的store采用一种父子关系,有一个总的store,可以理解为数据库,然后基于该store可以创建子store,可以立即为一个个的表,keeper操作具体的子store,并且在最后abci commit的时候,提交总store。
在baseApp初始化时,会创建一个总store,这是store的总入口:
func NewCommitMultiStore(db dbm.DB) types.CommitMultiStore { return rootmulti.NewStore(db, log.NewNopLogger()) }
根据总入口,可以创建一个个的子store,每个store对应一个key,在app中会有一个key的列表,根据key列表初始化子store:
keys := sdk.NewKVStoreKeys( // SDK keys authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, paramstypes.StoreKey, upgradetypes.StoreKey, evidencetypes.StoreKey, capabilitytypes.StoreKey, consensusparamtypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey, crisistypes.StoreKey, // ibc keys ibcexported.StoreKey, ibctransfertypes.StoreKey, // ica keys icahosttypes.StoreKey, // ethermint keys evmtypes.StoreKey, feemarkettypes.StoreKey, // evmos keys inflationtypes.StoreKey, erc20types.StoreKey, incentivestypes.StoreKey, epochstypes.StoreKey, claimstypes.StoreKey, vestingtypes.StoreKey, revenuetypes.StoreKey, recoverytypes.StoreKey, ) // initialize stores app.MountKVStores(keys) app.MountTransientStores(tkeys) app.MountMemoryStores(memKeys)
然后在keeper中,根据对应的key,就可以使用子store:
store := prefix.NewStore(ctx.TransientStore(k.transientKey), types.KeyPrefixTransientBloom) heightBz := sdk.Uint64ToBigEndian(uint64(ctx.BlockHeight())) store.Set(heightBz, bloom.Bytes())
最后在abci的commit接口中,保存所有的store:
func (app *BaseApp) Commit() abci.ResponseCommit { .... // Write the DeliverTx state into branched storage and commit the MultiStore. // The write to the DeliverTx state writes all state transitions to the root // MultiStore (app.cms) so when Commit() is called is persists those values. app.deliverState.ms.Write() commitID := app.cms.Commit() }
Codec
codec负责结构体的编解码。
目前有两种codec, amino和protobuf,amino是比较旧的方式,protobuf是比较新的方式,这里讨论amino。
codec保存结构体的反射信息:
type Codec struct { mtx sync.RWMutex sealed bool typeInfos map[reflect.Type]*TypeInfo interfaceInfos []*TypeInfo concreteInfos []*TypeInfo disfixToTypeInfo map[DisfixBytes]*TypeInfo nameToTypeInfo map[string]*TypeInfo }
在app创建时,会新建一个codec实例:
cdc := amino.NewLegacyAmino()
然后通过module manager,便利所有module,注册codec:
func (bm BasicManager) RegisterCodec(cdc *codec.Codec) { for _, b := range bm { b.RegisterCodec(cdc) } }
一般会在module的codec.go文件中,实现具体的注册:
init() { // Register all Amino interfaces and concrete types //on the authz and gov Amino codec so that this can later be // used to properly serialize MsgGrant, MsgExec and MsgSubmitProposal instances RegisterLegacyAminoCodec(authzcodec.Amino) RegisterLegacyAminoCodec(govcodec.Amino) RegisterLegacyAminoCodec(groupcodec.Amino) }
回复 can you buy cytotec prices 取消回复