cosmos-sdk/docs/cn/basics/app-anatomy.md

22 KiB
Raw Blame History

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,则重播块以同步到该高度。 如果appBlockHeight0则该节点从创世开始并且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应等于模块中定义的预测值。 如果该值与预测的值不同,则将触发不变注册表中定义的特殊逻辑(通常会中断链)。 这对于确保不会发现任何严重错误并产生难以修复的长期影响非常有用。
  • 使用模块管理器,在每个应用程序的模块 的InitGenesisBegingBlocker和EndBlocker函数之间设置执行顺序。 请注意,并非所有模块都实现这些功能。
  • 模块实现这些功能。
  • 设置其余的应用程序参数:
    • InitChainer于在应用程序首次启动时对其进行初始化。
    • BeginBlockerEndBlocker:在每个块的开始和结尾处调用。
    • 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:

查看来自gaiaInitChainer的示例: +++ f41a660cdd/app/app.go (L235-L239)

BeginBlocker and EndBlocker

该SDK为开发人员提供了在其应用程序中实现自定义代码可能性。 这是通过两个名为“ BeginBlocker”和“ EndBlocker”的函数实现的。 当应用程序分别从Tendermint引擎接收到BeginBlockEndBlock消息时,将调用它们,它们分别在每个块的开始和结尾处发生。 应用程序必须通过 SetBeginBlockerSetEndBlocker方法在其 constructor中设置BeginBlockerEndBlocker

通常,BeginBlockerEndBlocker函数主要由每个应用程序模块的BeginBlockEndBlock函数组成。 这是通过调用模块管理器的BeginBlock和EndBlock函数来完成的而后者又会调用其包含的每个模块的BeginBLock和EndBlock函数。 请注意必须分别在模块管理器中使用SetOrderBeginBlock和SetOrderEndBlock方法来设置模块的BegingBlock和EndBlock函数必须调用的顺序。 这是通过应用程序的构造函数中的模块管理器完成的必须调用SetOrderBeginBlock和SetOrderEndBlock方法。 在SetBeginBlocker和SetEndBlocker函数之前。

附带说明,请记住特定于应用程序的区块链是确定性的,这一点很重要。 开发人员必须注意不要在BeginBlocker或EndBlocker中引入不确定性还必须注意不要使它们在计算上过于昂贵因为[gas]不会限制计算代价当调用 BeginBlocker和EndBlocker执行。

请参阅gaia中的BeginBlockerEndBlocker函数的示例。

+++ f41a660cdd/app/app.go (L224-L232)

Register Codec

MakeCodec函数是app.go文件的最后一个重要功能。 此函数的目的是使用 RegisterCodec 函数实例化 codeccdc例如amino初始化SDK的编解码器以及每个应用程序的模块。

为了注册应用程序的模块,MakeCodec函数在ModuleBasics上调用RegisterCodecModuleBasics是一个基本管理器,其中列出了应用程序的所有模块。 它在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的方法)。 AppModuleAppModuleBasic类型都在名为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仅执行无状态检查和与费用相关的有状态检查。 为了更好地理解DeliverTxCheckTx之间的区别以及有状态和无状态检查之间的区别,请看[这里](./ tx-lifecycle.md)。

模块的“处理程序”通常在名为handler.go的文件中定义,并包括:

处理程序函数返回结果类型为sdk.Result该结果通知应用程序消息是否已成功处理

+++ 7d7821b9af/types/result.go (L15-L40)

Querier

Queriershandlers非常相似,除了它们向状态查询用户而不是处理事务。 最终用户从interface 发起query最终用户会提供queryRoute和一些 data。 然后使用queryRoute通过baseapphandleQueryCustom方法查询到正确的应用程序的querier 函数

+++ 7d7821b9af/baseapp/abci.go (L395-L453)

模块的Querier是在名为querier.go的文件中定义的包括

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.goclient / cli / query.go中定义。 这两个命令基于Cobra Library之上:

  • Transactions命令使用户可以生成新的事务以便可以将它们包含在块中并更新状态。 应该为模块中定义的每个消息类型message-types创建一个命令。 该命令使用户提供的参数调用消息的构造函数,并将其包装到事务中。 SDK处理签名和其他事务元数据的添加。
  • 用户可以查询模块定义的状态子集。 查询命令将查询转发到应用程序的查询路由器,然后将查询路由到提供的queryRoute参数的相应 querier。

REST

模块的REST接口允许用户生成事务并通过对应用程序的 light client daemonLCD 查询状态。 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的根部该函数添加了通用命令例如statuskeys和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主要确保在构建应用程序的两个入口点appdappcli之前运行go.mod。 请参阅 nameservice demo 中的Makefile示例

+++ 86a27321cf/nameservice/Makefile

Next

了解有关[交易生命周期](./ tx-lifecycle.md)的更多信息