22 KiB
SDK应用程序剖析
Node Client
全节点的核心进程是基于SDK包的。 网络中的参与者运行此过程以初始化其状态机,与其他全节点连接并在新块进入时更新其状态机。
^ +-------------------------------+ ^
| | | |
| | State-machine = Application | |
| | | | Built with Cosmos SDK
| | ^ + | |
| +----------- | ABCI | ----------+ v
| | + v | ^
| | | |
Blockchain Node | | Consensus | |
| | | |
| +-------------------------------+ | Tendermint Core
| | | |
| | Networking | |
| | | |
v +-------------------------------+ v
区块链全节点以二进制形式表示,通常以-d
后缀表示守护程序
(例如,appd
表示 app
或 gaiad
表示gaia
)。 这个二进制文件是通过编译一个简单的代码文件 main.go构建的,main.go
通常位于./cmd/appd/
中。 此操作通常通过用Makefile编译。
编译了二进制文件,就可以通过运行start
命令 来启动节点。 此命令功能主要执行三件事:
1.[app.go
] 创建了一个状态机实例。
2.用最新的已知状态初始化状态机,该状态机是从存储在~/ .appd / data
文件夹中的db中提取的。 此时,状态机的高度为:appBlockHeight
。
3.创建并启动一个新的Tendermint实例。 该节点将与对等节点进行连接交换信息。 它将从他们那里获取最新的blockHeight
,如果它大于本地的appBlockHeight
,则重播块以同步到该高度。 如果appBlockHeight
为0
,则该节点从创世开始,并且Tendermint通过ABCI接口向app
发送InitChain
初始化链命令,从而触发InitChainer
。
Core Application File
通常,状态机的核心是在名为app.go
的文件中定义的。 它主要包含“应用程序的类型定义”和“创建和初始化它”的功能。
Type Definition of the Application
在app.go中重要的一个是应用程序的type。 它通常由以下部分组成:
-
在
app.go
中定义的自定义应用程序是baseapp
的扩展。 当事务由Tendermint发送到应用程序时,app
使用baseapp
的方法将它们转送到对应的模块。 baseapp为应用程序实现了大多数核心逻辑,包括所有的ABCI方法和转送消息逻辑。 -
一条key链包含整个状态,他是基于 Cosmos SDK 的multistore实现的。 每个模块使用multistore的一个或多个存储来存储其状态。 可以使用在“ app”类型中声明的特定键来访问这些存储。 这些密钥以及`keepers'是Cosmos SDK的 对象功能模型 的核心。
-
模块
keeper
的列表。 每个模块 都会抽象定义一个keeper,该keeper 实现模块存储的读写。 一个模块的“ keeper”方法可以从其他模块(如果已授权)中调用,这就是为什么它们在应用程序的类型中声明并作为接口导出到其他模块的原因,以便后者只能访问授权的功能。 -
应用程序的
codec
用于序列化和反序列化数据结构以便存储它们,因为存储只能持久化[]bytes
。 “编解码器”必须是确定性的。 默认编解码器为amino -
模块管理器是一个对象,其中包含应用程序模块的列表。 它简化了与这些模块相关的操作,例如注册routes操作,query route操作或设置各种功能的模块之间顺序执行情况,例如InitChainer操作,BeginBlocke操作和EndBlocker操作
-
请参阅gaia中的应用程序类型定义示例
+++ 5bc422e686/app/app.go (L87-L115)
Constructor Function
此函数构造了以上部分中定义的类型的新应用程序。 在应用程的start命令中使用,它必须具有AppCreator签名。
+++ 7d7821b9af/server/constructors.go (L20)
以下是此功能执行的主要操作:
- 创建初始化一个新的codec实例,并使用 基础模块管理器初始化每个应用程序模块的
codec
。 - 使用baseapp实例,编解码器和所有适当的存储键的引用实例化一个新应用程序。
- 使用每个应用程序模块的
NewKeeper
功能实例化在应用程序的类型
中定义的所有keeper。 注意,所有keeper必须以正确的顺序实例化,因为一个模块的NewKeeper可能需要引用另一个模块的keeper
。 - 使用每个应用模块的AppModule 来实例化应用程序的模块管理器
- 使用模块管理器,初始化应用程序的routes和query route。 当事务由Tendermint通过ABCI中继到应用程序时,它使用此处定义的路由被路由到相应模块的 回调handler。 同样,当应用程序收到查询时,使用此处定义的查询路由将其路由到适当的模块的querier。
- 使用模块管理器,注册应用程序的模块的invariants。 invariants是在每个块末尾评估的变量(例如token的总供应量)。 检查不变式的过程是通过InvariantsRegistry 的特殊模块完成的。 invariants应等于模块中定义的预测值。 如果该值与预测的值不同,则将触发不变注册表中定义的特殊逻辑(通常会中断链)。 这对于确保不会发现任何严重错误并产生难以修复的长期影响非常有用。
- 使用模块管理器,在每个应用程序的模块 的InitGenesis,BegingBlocker和EndBlocker函数之间设置执行顺序。 请注意,并非所有模块都实现这些功能。
- 模块实现这些功能。
- 设置其余的应用程序参数:
InitChainer
于在应用程序首次启动时对其进行初始化。BeginBlocker
,EndBlocker
:在每个块的开始和结尾处调用。anteHandler
:用于处理费用和签名验证。
- 挂载存储.
- 返回应用实例.
请注意,此函数仅创建该应用的一个实例,而如果重新启动节点,则状态将从〜/ .appd / data
文件夹中保留下来状态加载,如果节点是第一次启动,则从创世文件生成。See an example of application constructor from gaia
:
+++ f41a660cdd/app/app.go (L110-L222)
InitChainer
InitChainer用于根据创始文件(即创始账户的代币余额)初始化应用程序的状态。 当应用程序从Tendermint引擎收到InitChain
消息时调用该消息,该消息是在节点以appBlockHeight == 0
(即创世)启动。 应用程序必须通过SetInitChainer
方法设置其constructor中的Initchainer
。
通常,InitChainer
主要由每个应用程序模块的InitGenesis函数组成。 这是通过调用模块管理器的InitGenesis函数来完成的,而模块管理器的InitGenesis函数将依次调用其包含的每个模块的InitGenesis函数。 请注意,必须使用模块管理器的SetOrderInitGenesis方法设置模块的InitGenesis函数的顺序。 这是在 应用程序的构造函数 application-constructor 中完成的,必须在SetInitChainer之前调用SetOrderInitGenesis。
查看来自gaia的InitChainer的示例:
See an example of an InitChainer
from gaia
:
查看来自gaia
的InitChainer
的示例:
+++ f41a660cdd/app/app.go (L235-L239)
BeginBlocker and EndBlocker
该SDK为开发人员提供了在其应用程序中实现自定义代码可能性。 这是通过两个名为“ BeginBlocker”和“ EndBlocker”的函数实现的。 当应用程序分别从Tendermint引擎接收到BeginBlock
和EndBlock
消息时,将调用它们,它们分别在每个块的开始和结尾处发生。 应用程序必须通过 SetBeginBlocker和SetEndBlocker方法在其 constructor中设置BeginBlocker
和EndBlocker
。
通常,BeginBlocker
和EndBlocker
函数主要由每个应用程序模块的BeginBlock
和EndBlock
函数组成。 这是通过调用模块管理器的BeginBlock和EndBlock函数来完成的,而后者又会调用其包含的每个模块的BeginBLock和EndBlock函数。 请注意,必须分别在模块管理器中使用SetOrderBeginBlock和SetOrderEndBlock方法来设置模块的BegingBlock和EndBlock函数必须调用的顺序。 这是通过应用程序的构造函数中的模块管理器完成的,必须调用SetOrderBeginBlock和SetOrderEndBlock方法。 在SetBeginBlocker和SetEndBlocker函数之前。
附带说明,请记住特定于应用程序的区块链是确定性的,这一点很重要。 开发人员必须注意不要在BeginBlocker或EndBlocker中引入不确定性,还必须注意不要使它们在计算上过于昂贵,因为[gas]不会限制计算代价当调用 BeginBlocker和EndBlocker执行。
请参阅gaia中的BeginBlocker
和EndBlocker
函数的示例。
+++ f41a660cdd/app/app.go (L224-L232)
Register Codec
MakeCodec函数是app.go文件的最后一个重要功能。 此函数的目的是使用 RegisterCodec 函数实例化 codeccdc
,例如amino初始化SDK的编解码器以及每个应用程序的模块。
为了注册应用程序的模块,MakeCodec
函数在ModuleBasics
上调用RegisterCodec
。ModuleBasics
是一个基本管理器,其中列出了应用程序的所有模块。 它在init()
函数中得到实例化,仅用于注册应用程序模块的非依赖元素(例如编解码器)。 要了解有关基本模块管理器的更多信息,请点击这里。
请参阅gaia中的MakeCodec
示例:
+++ f41a660cdd/app/app.go (L64-L70)
Modules
Modules 是SDK应用程序的灵魂。 它们可以被视为状态机中的状态机。 当交易通过ABCI从底层的Tendermint引擎中继到应用程序时,它由 baseapp 找到对应的模块以便进行处理。 这种范例使开发人员可以轻松构建复杂的状态机,因为他们所需的大多数模块通常已经存在。 对于开发人员而言,构建SDK应用程序所涉及的大部分工作都围绕构建其应用程序尚不存在的自定义模块,并将它们与已经存在的模块集成到一个统一的应用程序中。 在应用程序目录中,标准做法是将模块存储在x/
文件夹中(不要与SDK的x/
文件夹混淆,该文件夹包含已构建的模块)。
Application Module Interface
模块必须实现Cosmos SDK AppModuleBasic中的interfaces 和 AppModule。 前者实现了模块的基本非依赖性元素,例如“编解码器”,而后者则处理了大部分模块方法(包括需要引用其他模块的keeper
的方法)。 AppModule
和AppModuleBasic
类型都在名为module.go
的文件中定义。
AppModule在模块上公开了一组有用的方法,这些方法有助于将模块组合成一个一致的应用程序。 这些方法是从模块管理器中调用的,该模块管理应用程序的模块集合。
Message Types
每个module
定义messages接口。 每个transaction
包含一个或多个messages
。
当全节点接收到有效的交易块时,Tendermint通过DeliverTx
将每个交易发到应用程序 。 然后,应用程序处理事务:
- 收到交易后,应用程序首先从
[] bytes
反序列化得到。 - 然后,在提取交易中包含的消息之前,它会验证有关交易的一些信息,例如费用支付和签名
- 使用message的Type()方法,baseapp可以将其发到对应模块的 回调 handler以便对其进行处理。
- 如果消息已成功处理,则状态将更新。
有关事务生命周期的更多详细信息,请看[这里](./ tx-lifecycle.md)。
模块开发人员在构建自己的模块时会创建自定义消息类型。 通常的做法是在消息的类型声明前加上Msg
。 例如,消息类型MsgSend
允许用户传输tokens:
+++ 7d7821b9af/x/bank/internal/types/msgs.go (L10-L15)
它由bank
模块的回调handler
处理,最终会调用auth
模块来写keeper
以更新状态。
Handler
回调handler
是指模块的一部分,负责处理baseapp
传递的message
消息。 仅当通过ABCI接口的DeliverTx 消息从Tendermint 收到事务时,才执行模块的“处理程序”功能。 如果通过CheckTx,仅执行无状态检查和与费用相关的有状态检查。 为了更好地理解DeliverTx
和CheckTx
之间的区别以及有状态和无状态检查之间的区别,请看[这里](./ tx-lifecycle.md)。
模块的“处理程序”通常在名为handler.go
的文件中定义,并包括:
- NewHandler 将消息发到对应的回调“ handler”。 该函数返回一个“ handler”函数,此前这个函数在[AppModule`]中注册,以在应用程序的模块管理器中用于初始化应用程序的路由器。 接下来是nameservice tutorial的一个例子。 +++ https://github.com/cosmos/sdk-tutorials/blob/master/nameservice/x/nameservice/handler.go#L12-L26
- 模块定义的每种消息类型的处理函数。 开发人员在这些函数中编写消息处理逻辑。 这通常包括进行状态检查以确保消息有效,并调用
keeper
的方法来更新状态。
处理程序函数返回结果类型为sdk.Result,该结果通知应用程序消息是否已成功处理:
+++ 7d7821b9af/types/result.go (L15-L40)
Querier
Queriers
与handlers
非常相似,除了它们向状态查询用户而不是处理事务。 最终用户从interface 发起query,最终用户会提供queryRoute
和一些 data
。 然后使用queryRoute
通过baseapp的
handleQueryCustom方法查询到正确的应用程序的querier
函数
+++ 7d7821b9af/baseapp/abci.go (L395-L453)
模块的Querier是在名为querier.go的文件中定义的,包括:
-
NewQuerier将查找到对应query函数。 此函数返回一个“
querier
”函数,此前它在 AppModule中注册,以在应用程序的模块管理器中用于初始化应用程序的查询路由器。 请参阅[nameservice demo](https://github.com/cosmos/sdk-tutorials/tree/master/nameservice)中的此类切换示例: +++86a27321cf/nameservice/x/nameservice/internal/keeper/querier.go (L19-L32)
-
对于模块定义的每种需要查询的数据类型,都具有一个查询器功能。 开发人员在这些函数中编写查询处理逻辑。 这通常涉及调用[
keeper
] 的方法来查询状态并将其 序列化为JSON。
Keeper
Keepers
是其模块存储器件。 要在模块的存储区中进行读取或写入,必须使用其keeper
方法之一。 这是由Cosmos SDK的 object-capabilities 模型确保的。 只有持有商店密钥的对象才能访问它,只有模块的keeper
才应持有该模块商店的密钥。
Keepers
通常在名为keeper.go
的文件中定义。 它包含keeper
的类型定义和方法。
keeper
类型定义通常包括:
- 多重存储中模块存储的“密钥”。
- 参考其他模块的
keepers
。 仅当keeper
需要访问其他模块的存储(从它们读取或写入)时才需要。
- 参考其他模块的
- 对应用程序的“编解码器”的引用。 “ keeper”需要它在存储结构之前序列化处理,或在检索它们时将反序列化处理,因为存储仅接受“ [] bytes”作为值。
与类型定义一起,keeper.go文件的一个重要组成部分是Keeper的构造函数NewKeeper。 该函数实例化上面定义的类型的新keeper
,并带有codec
,存储keys
以及可能引用其他模块的keeper
作为参数。 从应用程序的构造函数中调用NewKeeper
函数。 文件的其余部分定义了keeper
的方法,主要是getter和setter。
Command-Line and REST Interfaces
每个模块都定义了application-interfaces 向用户公开的命令行命令和REST routes。 用户可以创建模块中定义的类型的消息,或查询模块管理的状态的子集。
CLI
通常,与模块有关的命令在模块文件夹中名为client / cli
的文件夹中定义。 CLI将命令分为交易和查询两类,分别在client / cli / tx.go
和client / cli / query.go
中定义。 这两个命令基于Cobra Library之上:
- Transactions命令使用户可以生成新的事务,以便可以将它们包含在块中并更新状态。 应该为模块中定义的每个消息类型message-types创建一个命令。 该命令使用户提供的参数调用消息的构造函数,并将其包装到事务中。 SDK处理签名和其他事务元数据的添加。
- 用户可以查询模块定义的状态子集。 查询命令将查询转发到应用程序的查询路由器,然后将查询路由到提供的
queryRoute
参数的相应 querier。
REST
模块的REST接口允许用户生成事务并通过对应用程序的 light client daemon(LCD) 查询状态。 REST路由在client / rest / rest.go
文件中定义,该文件包括:
-
RegisterRoutes
函数,用于注册路由。 从主应用程序的接口 application-interfaces 中为应用程序内使用的每个模块调用此函数。 SDK中使用的路由器是 Gorilla's mux。 -
需要公开的每个查询或事务创建功能的自定义请求类型定义。 这些自定义请求类型基于Cosmos SDK的基本“请求”类型构建: +++
7d7821b9af/types/rest/rest.go (L47-L60)
-
每个请求的一个处理函数可以找到给定的模块。 这些功能实现了服务请求所需的核心逻辑。
Application Interface
Interfaces允许用户与全节点客户端进行交互。 这意味着从全节点查询数据,或者接受全节点中包含在块中的新事务。
通过汇总在应用程序使用的每个模块中定义的 CLI命令构建SDK应用程序的CLI。 应用程序的CLI通常具有后缀-cli(例如appcli),并在名为cmd / appcli / main.go
的文件中定义。 该文件包含:
- main()函数,用于构建appcli接口客户端。这个函数准备每个命令,并在构建它们之前将它们添加到
rootCmd
中。在appCli的根部,该函数添加了通用命令,例如status,keys和config,查询命令,tx命令和rest-server。 - 查询命令是通过调用
queryCmd
函数添加的,该函数也在appcli / main.go中定义。此函数返回一个Cobra命令,其中包含在每个应用程序模块中定义的查询命令(从main()
函数作为sdk.ModuleClients
数组传递),以及一些其他较低级别的查询命令,例如阻止或验证器查询。查询命令通过使用CLI的命令“ appcli query [query]”来调用。 - 通过调用
txCmd
函数来添加交易命令。与queryCmd
类似,该函数返回一个Cobra命令,其中包含在每个应用程序模块中定义的tx命令,以及较低级别的tx命令,例如事务签名或广播。使用CLI的命令appcli tx [tx]
调用Tx命令。 - registerRoutes函数,在初始化 轻客户端(LCD)时从main()函数调用。 “ registerRoutes”调用应用程序每个模块的“ RegisterRoutes”功能,从而注册该模块routes 到LCD的查询路由。可以通过运行以下命令“ appcli rest-server”来启动LCD。
从nameservice demo中查看应用程序的主要命令行文件的示例。
+++ 86a27321cf/nameservice/cmd/nscli/main.go
Dependencies and Makefile
因为开发人员可以自由选择其依赖项管理器和项目构建方法。 也就是说,当前最常用的版本控制框架是go.mod
。 它确保在整个应用程序中使用的每个库都以正确的版本导入。 请参阅demo中的示例:
+++ c6754a1e31/go.mod (L1-L18)
为了构建应用程序,通常使用Makefile。 Makefile主要确保在构建应用程序的两个入口点appd
和appcli
之前运行go.mod
。 请参阅 nameservice demo 中的Makefile示例
+++ 86a27321cf/nameservice/Makefile
Next
了解有关[交易生命周期](./ tx-lifecycle.md)的更多信息