赞
踩
Tendermint是一个应用程序软件,用于在多机器环境下进行安全且一致的复制应用程序。安全体现在即使多达1/3的机器在任意方式下发生故障,Tendermint仍然可以正常工作。一致体现在每个无故障的机器看到相同的事务日志并计算出相同的状态。安全一致的复制是分布式系统中的一个基本问题,它在应用的容错性方面发挥着关键作用,从货币、选举到基础设施协调等。
Tendermint设计的宗旨是易于使用、易于理解、高性能并且适用于各种分布式应用程序。
1.拜占庭容错
容忍机器以任意方式失效(包括变为恶意)的能力被称为拜占庭容错(BFT)。BFT理论已有几十年历史,但软件实现最近才开始流行,这主要归功于比特币和以太坊等区块链技术的成功。区块链技术是BFT的另一种表现形式,只是在当前的应用环境下,有了P2P网络和加密认证,区块链名称源自事务以块的形式进行批处理,其中每个块包含前一个块的加密哈希,形成链。实际上,区块链数据结构优化了BFT设计。
2.应用程序接口
Tendermint由两个主要技术组件组成:区块链共识引擎和通用应用程序接口。称为Tendermint Core的共识引擎确保在每台机器上以相同的顺序记录相同的事务。
应用程序接口称为应用程序区块链接口(ABCI),可以使用任何编程语言处理事务。与其他区块链和共识解决方案不同,开发人员可以在任何编程语言或开发环境中使用Tendermint进行BFT状态机复制。
3.Tendermint结构
充分了解应用程序和共识引擎的各自职责对于开发来说非常重要。
共识引擎的责任:
传播交易
有效交易顺序的确认
应用程序的责任:
生成交易
检查交易是否有效
处理交易(包括状态更改)
Consensus Engine知道每个块的验证节点集,但是验证节点集变更是由应用程序触发的。这就是为什么可以使用Cosmos-SDK和Tendermint 构建公共链和私有链的原因。链将是公共的还是私有的,具体取决于应用程序业务上定义的规则,该规则控制验证节点集的变更。
ABCI建立了共识引擎和应用程序之间的连接。从本质上讲,它归结为两条消息:
CheckTx:询问应用程序交易是否有效。当验证节点收到交易时,它将在CheckTx上运行,如果交易有效,则将其添加到mempool。
DeliverTx:要求应用程序处理交易并更新状态。
再次看看共识引擎和应用程序如何相互交互。
在任何时候,当验证器节点的共识引擎(Tendermint Core)收到交易时,它会将其传递给应用程序CheckTx以检查其有效性。如果有效,则将事务添加到mempool。
假定当前在块N,验证节点集为V。下一个块的提议者是由Consensus Engine从V中选择的。提议者从其mempool收集有效交易以形成新块。然后该块被广播到其他验证节点以进行确认、签名、提交。一旦2/3 +的V签署了预先提交,块就变为块N + 1 (有关一致性算法的更详细说明,请单击此处)。
当块N + 1由2/3 + V签名时,它被广播到全节点。当全节点接收到块时,它们确认其有效性。如果一个块包含超过V的2/3的有效签名并且块中的所有交易都有效,则该块有效。为了检查交易的有效性,Consensus Engine将该交易传递给应用程序DeliverTx。在每个事务之后,如果事务有效,DeliverTx则返回新状态。在块结束时,提交最终状态。当然,这意味着块内的交易顺序很重要。
4.应用程序框架
Tendermint使得开发人员只专注于区块链的应用程序层开发,这使得构建自己的区块链变得轻松。但构建应用程序层本身也是一项具有挑战性的任务。这就是Application Frameworks存在的原因。它们为开发人员提供了一个安全且功能丰富的环境来开发基于Tendermint的应用程序。
Cosmos SDK是构建多资产POS区块链的框架,目标是允许开发人员在Cosmos Network中轻松创建自定义的可互操作的区块链应用程序,而无需重新创建常见的区块链功能,从而消除了构建Tendermint ABCI应用程序的复杂性。我们可以将Cosmos SDK设想为类似于npm的框架,用于在Tendermint之上构建安全的区块链应用程序。
在设计方面,Cosmos SDK具有灵活性和安全性。该框架使用了模块化的程序栈设计,允许应用程序根据需要来选择使用模块。此外,所有模块都采用沙盒,以提高应用程序的安全性。
Cosmos SDK设计基于两个主要原则:
Composability:任何人都可以为Cosmos SDK创建模块,并集成已构建的模块,就像将它们导入区块链应用程序一样简单。
Capabilities:Cosmos SDK设计考虑了capabilities-based的安全性,参考了多年区块链状态机的经验。大多数开发人员在构建自己的模块时需要访问其他第三方模块。鉴于Cosmos SDK是一个开放框架,我们假设其中一些模块可能是恶意的,我们使用面向对象功能(ocaps)的原则设计了SDK。实际上,这意味着不是让每个模块保留其他模块的访问控制列表,而是每个模块都实现称为keeper的特殊对象,这些对象可以传递给其他模块以授予预定义的一组功能。例如,如果模块A的keeper的实例被传递给模块B,则后者将能够调用一组受限制的模块A的功能。每个keeper的能力由模块开发人员定义,模块开发人员的工作是根据第三方模块传递的功能来理解和审核来自第三方模块的外部代码的安全性。要深入了解功能,可以阅读本文文章。
有关对象功能的介绍,请参阅此文章
1.目录结构
Cosmos SDK包含以下目录:
baseapp:定义基本ABCI应用程序的模板,用于Cosmos-SDK应用程序与基础Tendermint节点通信。
client:用于与Cosmos SDK应用程序交互的CLI和REST服务器工具。
examples:如何构建应用程序的示例。
server:用于在Tendermint之上构建全节点Cosmos SDK应用程序。
store:Cosmos SDK的数据库。
types:Cosmos SDK应用程序中的常见类型。
x:核心扩展,定义了所有消息和处理程序。
2.应用程序体系结构
Cosmos SDK具有多个级别的“应用程序”:ABCI应用程序,BaseApp,BasecoinApp,开发者的应用程序。
ABIC应用程序
基本的ABCI接口,允许Tendermint使用事务块驱动应用程序状态机。
BaseApp
使用MultiStore实现ABCI应用程序以实现持久性,并使用路由器来处理事务。目标是在存储和可扩展状态机之间提供安全接口,同时尽可能少地定义该状态机(保持对ABCI的真实性)。
BaseApp要求存储提供一个key,处理程序只能访问为其提供key的存储。BaseApp可确保存储被正确加载、缓存和提交。一个默认的存储叫“main”,它包含最新的块头,我们可以从中找到并加载最新的状态。
BaseApp区分两种处理类型 - AnteHandler和MsgHandler。前者是全局有效性检查(检查nonces,sigs和足够的余额以支付费用,以及适用于所有模块的所有事务的检查),后者是完整的状态转换函数。在CheckTx期间,状态转换函数仅应用于checkTxState,并且应该在运行任何昂贵的状态转换之前返回(这取决于每个开发人员)。它还需要返回估计的费用成本。
在DeliverTx期间,状态转换功能应用于区块链状态,并且事务需要完全执行。
BaseApp负责管理传递给处理程序的上下文 - 它使块头可用,并为CheckTx和DeliverTx提供正确的存储。BaseApp与序列化格式完全无关。
Basecoin
Basecoin是该应用程序层中第一个完整的应用程序。完整的应用程序需要扩展SDK的核心模块才能实现处理程序功能。
SDK的原生扩展,适用于构建Cosmos Zones,可以使用x。Basecoin BaseApp使用x/auth和x/bank扩展来实现状态机,它定义了如何验证交易签名者以及如何转移代币。它也应该使用x/ibc,或者一个简单的质押扩展。
Basecoin和原生x扩展使用go-amino来满足所有序列化需求,包括交易和帐户。
开发者App
开发者Cosmos应用程序应该是Basecoin的fork,复制examples/basecoin目录并根据个人的需要进行修改。例如:
在帐户上添加新字段
修改handler逻辑
增加新交易类型并添加新的处理程序
增加新存储以便更好地隔离处理程序
Gaia采用Basecoin并添加许多存储和扩展来处理其他事务类型和逻辑,如高级质押逻辑和治理程序。
Ethermint
Ethermint是一个BaseApp但不依赖于Basecoin 的新实现,不使用cosmos-sdk/x/,它有自己的ethermint/x扩展go-ethereum。
Ethermint为其帐户使用Patricia store,为IBC使用IAVL store,它有x/ante,与Basecoin非常相似但使用RLP而不是go-amino。它没有使用x/bank,它有x/eth,定义了单个以太坊事务类型和以太坊状态机的所有语义。
在x/eth内部,发送到特定地址的事务可以以独特的方式处理,例如处理IBC和抵押。
以下将介绍如何使用Cosmos SDK构建Basecoin,它是一个完整的POS加密货币区块链系统。
我们将一步一步实现一个先进且完整的Basecoin应用程序,每一步实现都展示了Cosmos SDK的新组件使用方法:
App1 - 基础知识 - 消息、存储、处理程序、BaseApp
App2 - 交易 - Amino和AnteHandler
App3 - 模块 - x/auth和x/bank
App4 - 验证节点集变更 - 变更Tendermint验证节点集
App5 - Basecoin - 整合成一个完成的区块链
我们首先来构建App1,一个简单的bank,通过这个App1来介绍Cosmos SDK的基本组件。用户拥有帐户地址和帐户,他们可以发送代币。但没有身份验证,只使用JSON进行序列化。完整的代码可以在app1.go中找到。
Messages
消息是应用程序状态机的主要输入,它定义了交易的内容并且可以包含任意信息。开发人员可以通过实现Msg接口来创建消息:
- type Msg interface {
- // Return the message type.
- // Must be alphanumeric or empty.
- // Must correspond to name of message handler (XXX).
- Type() string
-
- // ValidateBasic does a simple validation check that
- // doesn't require access to any other information.
- ValidateBasic() error
-
- // Get the canonical byte representation of the Msg.
- // This is what is signed.
- GetSignBytes() []byte
-
- // GetSigners returns the addrs of signers that must sign.
- // CONTRACT: All signatures must be present to be valid.
- // CONTRACT: Returns addrs in some deterministic order.
- GetSigners() []AccAddress
- }
该Msg接口允许消息定义基本的有效性检查,以及需要签名的内容以及需要签名的人员。
例如,app1.go中定义了简单的代币发送消息类型:
- // MsgSend to send coins from Input to Output
- type MsgSend struct {
- From sdk.AccAddress `json:"from"`
- To sdk.AccAddress `json:"to"`
- Amount sdk.Coins `json:"amount"`
- }
-
- // Implements Msg.
- func (msg MsgSend) Type() string { return "send" }
消息应该是JSON编码并由发送人签名:
- // Implements Msg. JSON encode the message.
- func (msg MsgSend) GetSignBytes() []byte {
- bz, err := json.Marshal(msg)
- if err != nil {
- panic(err)
- }
- return bz
- }
-
- // Implements Msg. Return the signer.
- func (msg MsgSend) GetSigners() []sdk.AccAddress {
- return []sdk.AccAddress{msg.From}
- }
注意SDK中的地址是任意字节数组,在显示为字符串或以JSON格式呈现时是Bech32编码的。通常,地址是公钥的哈希值,因此我们可以使用它们来唯一地标识事务所需的签名者。
基本的有效性检查确保指定了From和To地址,且Amount为正数:
- // Implements Msg. Ensure the addresses are good and the
- // amount is positive.
- func (msg MsgSend) ValidateBasic() sdk.Error {
- if len(msg.From) == 0 {
- return sdk.ErrInvalidAddress("From address is empty")
- }
- if len(msg.To) == 0 {
- return sdk.ErrInvalidAddress("To address is empty")
- }
- if !msg.Amount.IsPositive() {
- return sdk.ErrInvalidCoins("Amount is not positive")
- }
- return nil
- }
请注意,该ValidateBasic方法由SDK自动调用!
KVStore
应用程序的基本存储层是KVStore:
- type KVStore interface {
- Store
-
- // Get returns nil iff key doesn't exist. Panics on nil key.
- Get(key []byte) []byte
-
- // Has checks if a key exists. Panics on nil key.
- Has(key []byte) bool
-
- // Set sets the key. Panics on nil key.
- Set(key, value []byte)
-
- // Delete deletes the key. Panics on nil key.
- Delete(key []byte)
-
- // Iterator over a domain of keys in ascending order. End is exclusive.
- // Start must be less than end, or the Iterator is invalid.
- // CONTRACT: No writes may happen within a domain while an iterator exists over it.
- Iterator(start, end []byte) Iterator
-
- // Iterator over a domain of keys in descending order. End is exclusive.
- // Start must be greater than end, or the Iterator is invalid.
- // CONTRACT: No writes may happen within a domain while an iterator exists over it.
- ReverseIterator(start, end []byte) Iterator
-
- }
KVStore的主要实现是目前的IAVL store。在后面的App中我们就会看到,应用程序有许多不同的KVStore,每个都有不同的名称和不同的存储内容。对store的访问由对象调用,在应用程序启动期间必须将其赋予处理程序。
处理程序
现在我们有了消息类型和存储接口,我们可以使用处理程序定义状态转换函数:
- // Handler defines the core of the state transition function of an application.
- type Handler func(ctx Context, msg Msg) Result
处理程序使用消息和上下文信息(a Context)作为输入,并返回一个Result,处理消息所需的所有信息都应包含在上下文中。
[todo]
所有这些中的KVStore在哪里?在消息处理程序中访问KVStore受Context通过对象功能键的限制。只有给予明确访问store密钥的处理程序才能在消息处理过程中访问该store。
Context
SDK使用Context来传递跨函数的公共信息,最重要的是,Context限制了基于对象功能键的KVStore访问,只有已明确访问key的处理程序才能访问相应的存储。
例如,FooHandler只能加载它给出key的store:
- // newFooHandler returns a Handler that can access a single store.
- func newFooHandler(key sdk.StoreKey) sdk.Handler {
- return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
- store := ctx.KVStore(key)
- // ...
- }
- }
Context已经在网络中间件和路由应用程序中无处不在,作为通过处理程序函数轻松传播请求上下文的一种手段。SDK对象上的许多方法都接收上下文作为第一个参数。
Context还包含块头,其中包含来自区块链的最新时间戳以及有关最新块的其他信息。
有关更多详细信息,请参阅Context API文档。
Result
Handler接受Context和Msg作为输入并返回Result。Result由相应的ABCI result驱动,它包含有关事务的返回值,错误信息,日志和元数据:
- // Result is the union of ResponseDeliverTx and ResponseCheckTx.
- type Result struct {
-
- // Code is the response code, is stored back on the chain.
- Code ABCICodeType
-
- // Data is any data returned from the app.
- Data []byte
-
- // Log is just debug information. NOTE: nondeterministic.
- Log string
-
- // GasWanted is the maximum units of work we allow this tx to perform.
- GasWanted int64
-
- // GasUsed is the amount of gas actually consumed. NOTE: unimplemented
- GasUsed int64
-
- // Tx fee amount and denom.
- FeeAmount int64
- FeeDenom string
-
- // Tags are used for transaction indexing and pubsub.
- Tags Tags
- }
我们将在本教程后面详细讨论这些字段。现在,请注意,Code值0被视为成功,而其他所有值都是失败的。Tags可包含事务相关的元数据,这将使我们能够很容易地查找涉及到特定的账户或交易行为的交易。
Handler
让我们为App1定义我们的处理程序:
- // Handle MsgSend.
- // NOTE: msg.From, msg.To, and msg.Amount were already validated
- // in ValidateBasic().
- func handleMsgSend(ctx sdk.Context, key *sdk.KVStoreKey, msg MsgSend) sdk.Result {
- // Load the store.
- store := ctx.KVStore(key)
-
- // Debit from the sender.
- if res := handleFrom(store, msg.From, msg.Amount); !res.IsOK() {
- return res
- }
-
- // Credit the receiver.
- if res := handleTo(store, msg.To, msg.Amount); !res.IsOK() {
- return res
- }
-
- // Return a success (Code 0).
- // Add list of key-value pair descriptors ("tags").
- return sdk.Result{
- Tags: msg.Tags(),
- }
- }
我们只有一个消息类型,因此只需定义一个特定于消息的函数,handleMsgSend。
请注意,此处理程序可以无限制地访问指定的存储,因此必须定义要存储的内容以及如何对其进行编码。稍后,我们将引入更高级别的抽象,以便处理程序在他们能做的事情上受到限制。对于第一个示例,我们使用一个JSON编码的简单帐户:
- type appAccount struct {
- Coins sdk.Coins `json:"coins"`
- }
Coins是SDK为多资产帐户提供的有用类型。我们可以在这里使用一个整数作为单一coin类型,但还是需要了解Coins。现在我们已经准备好处理MsgSend的两个部分:
- func handleFrom(store sdk.KVStore, from sdk.AccAddress, amt sdk.Coins) sdk.Result {
- // Get sender account from the store.
- accBytes := store.Get(from)
- if accBytes == nil {
- // Account was not added to store. Return the result of the error.
- return sdk.NewError(2, 101, "Account not added to store").Result()
- }
-
- // Unmarshal the JSON account bytes.
- var acc appAccount
- err := json.Unmarshal(accBytes, &acc)
- if err != nil {
- // InternalError
- return sdk.ErrInternal("Error when deserializing account").Result()
- }
-
- // Deduct msg amount from sender account.
- senderCoins := acc.Coins.Minus(amt)
-
- // If any coin has negative amount, return insufficient coins error.
- if !senderCoins.IsNotNegative() {
- return sdk.ErrInsufficientCoins("Insufficient coins in account").Result()
- }
-
- // Set acc coins to new amount.
- acc.Coins = senderCoins
-
- // Encode sender account.
- accBytes, err = json.Marshal(acc)
- if err != nil {
- return sdk.ErrInternal("Account encoding error").Result()
- }
-
- // Update store with updated sender account
- store.Set(from, accBytes)
- return sdk.Result{}
- }
-
- func handleTo(store sdk.KVStore, to sdk.AccAddress, amt sdk.Coins) sdk.Result {
- // Add msg amount to receiver account
- accBytes := store.Get(to)
- var acc appAccount
- if accBytes == nil {
- // Receiver account does not already exist, create a new one.
- acc = appAccount{}
- } else {
- // Receiver account already exists. Retrieve and decode it.
- err := json.Unmarshal(accBytes, &acc)
- if err != nil {
- return sdk.ErrInternal("Account decoding error").Result()
- }
- }
-
- // Add amount to receiver's old coins
- receiverCoins := acc.Coins.Plus(amt)
-
- // Update receiver account
- acc.Coins = receiverCoins
-
- // Encode receiver account
- accBytes, err := json.Marshal(acc)
- if err != nil {
- return sdk.ErrInternal("Account encoding error").Result()
- }
-
- // Update store with updated receiver account
- store.Set(to, accBytes)
- return sdk.Result{}
- }
处理程序很简单。我们首先使用授权的key从上下文加载KVStore。然后我们进行两个状态转换:一个用于发送者,一个用于接收者。每一个都涉及JSON解码来自store的帐户数据,修改账户Coins,将账户用JSON编码并存入store。
就是这样!
Tx
最后一步是Tx,虽然Msg包含应用程序中特定功能的内容,但用户提供的实际输入是序列化的Tx。应用程序可能有许多Msg接口实现,但它们应该只有一个实现Tx:
- // Transactions wrap messages.
- type Tx interface {
- // Gets the Msgs.
- GetMsgs() []Msg
- }
该Tx只是包装[]Msg,但可能包括额外的认证数据,如签名和帐户随机数。应用程序必须指定它们Tx的解码方式,因为这是应用程序的最终输入。我们稍后App中会详细讨论Tx类型,特别是在我们介绍StdTx时。在第一个应用程序中,我们根本不会有任何身份验证,这可能在私有网络中有意义,访问由客户端TLS证书等替代方法控制,但一般来说,我们希望将身份验证添加到我们的状态机中。我们将在下一个应用程序中使用它。现在,Tx只需嵌入MsgSend并使用JSON:
- type app1Tx struct {
- MsgSend
- }
-
- // This tx only has one Msg.
- func (tx app1Tx) GetMsgs() []sdk.Msg {
- return []sdk.Msg{tx.MsgSend}
- }
-
- // JSON decode MsgSend.
- func txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) {
- var tx app1Tx
- err := json.Unmarshal(txBytes, &tx)
- if err != nil {
- return nil, sdk.ErrTxDecode(err.Error())
- }
- return tx, nil
- }
BaseApp
最后,我们来构建完整的BaseApp。
该BaseApp是Tendermint ABCI的一个实现,也是基于Tendermint ABCI开发应用程序的简单框架。它充当SDK应用程序的两个关键组件之间的中介:store和消息处理程序。BaseApp实现了abci.Application接口。有关BaseApp API documentation更多详细信息,请参阅BaseApp API文档。以下是App1的完整实现:
- func NewApp1(logger log.Logger, db dbm.DB) *bapp.BaseApp {
-
- // Create the base application object.
- app := bapp.NewBaseApp(app1Name, logger, db, tx1Decoder)
-
- // Create a capability key for accessing the account store.
- keyAccount := sdk.NewKVStoreKey("acc")
-
- // Register message routes.
- // Note the handler receives the keyAccount and thus
- // gets access to the account store.
- app.Router().
- AddRoute("send", NewApp1Handler(keyAccount))
-
- // Mount stores and load the latest state.
- app.MountStoresIAVL(keyAccount)
- err := app.LoadLatestVersion(keyAccount)
- if err != nil {
- cmn.Exit(err.Error())
- }
- return app
- }
每个应用程序都将具有定义应用程序启动的功能。它通常包含在app.go文件中。我们将在本教程后面讨论如何将此app对象与CLI,REST API,记录器和文件系统相连接。现在,请注意,这里是我们为消息注册处理程序并授予它们访问store的地方。
在这里,我们只有一个Msg类型send,一个帐户存储和一个处理程序。通过为处理程序提供store key,可以授予处理程序访问store的权限。在未来的应用程序中,我们将拥有多个store和处理程序,并不是每个处理程序都可以访问每个store。
设置消息处理路由后,最后一步是装入存储并加载最新版本。由于我们只有一个store,我们只挂载一个。
执行
我们现在完成了应用程序的核心逻辑!从这里开始,我们可以在Go中编写测试,用帐户初始化store并通过调用app.DeliverTx方法执行事务。
在实际中,应用程序将在Tendermint共识引擎之上作为ABCI应用程序运行。它将由Genesis文件初始化,并且它将由基础Tendermint共识所commit的事务block驱动。我们将更多地讨论ABCI以及稍后如何工作,但请随时查看规范。我们还将了解如何将我们的应用程序连接到一整套组件,以便运行和使用实时区块链应用程序。
现在,我们注意到在收到交易时(通过app.DeliverTx)会发生以下事件序列:
序列化交易被传递给app.DeliverTx
使用TxDecoder将交易反序列化
对于事务block中的每条消息,运行 msg.ValidateBasic()
对于事务block中的每条消息,加载适当的处理程序并执行
结论
我们现在有一个简单的应用程序的完整实现!
在下一节中,我们将添加另一个Msg类型和另一个store。一旦我们有多种消息类型,我们就需要一种更好的解码消息的方法,因为我们需要解码到Msg接口中。这是我们介绍Amino的地方,这是一种优秀的编码方案,可以让我们解码成接口类型!
在之前的应用程序中,我们构建了一个简单的bank,其中有一种消息类型send用于发送代币,还有一种存储用于存储帐户,在这里我们来建立App2,通过扩展App1来介绍:
用于发行新代币的新消息类型
一个新的store用于存储代币元数据(比如谁可以发行代币)
一个新的需求 - 交易中包含有效签名
在此过程中,我们将介绍进行编码和解码交易的库Amino,并介绍用来编解码交易函数AnteHandler。完整的代码可以在app2.go中找到。
消息
首先是一种用于发行新代币的新消息类型:
- // MsgIssue to allow a registered issuer
- // to issue new coins.
- type MsgIssue struct {
- Issuer sdk.AccAddress
- Receiver sdk.AccAddress
- Coin sdk.Coin
- }
-
- // Implements Msg.
- func (msg MsgIssue) Type() string { return "issue" }
请注意该Type()方法返回"issue",因此此消息的类型不同于”send”,所以将由不同的处理程序执行,但方法类似于MsgSend。
Handler
我们需要一个新的Handler程序来支持新的消息类型,它只检查MsgIssue消息是否是正确的发行人发送的:
- // Handle MsgIssue
- func handleMsgIssue(keyIssue *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
- return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
- issueMsg, ok := msg.(MsgIssue)
- if !ok {
- return sdk.NewError(2, 1, "MsgIssue is malformed").Result()
- }
-
- // Retrieve stores
- issueStore := ctx.KVStore(keyIssue)
- accStore := ctx.KVStore(keyAcc)
-
- // Handle updating coin info
- if res := handleIssuer(issueStore, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
- return res
- }
-
- // Issue coins to receiver using previously defined handleTo function
- if res := handleTo(accStore, issueMsg.Receiver, []sdk.Coin{issueMsg.Coin}); !res.IsOK() {
- return res
- }
-
- return sdk.Result{
- // Return result with Issue msg tags
- Tags: issueMsg.Tags(),
- }
- }
- }
-
- func handleIssuer(store sdk.KVStore, issuer sdk.AccAddress, coin sdk.Coin) sdk.Result {
- // the issuer address is stored directly under the coin denomination
- denom := []byte(coin.Denom)
- infoBytes := store.Get(denom)
- if infoBytes == nil {
- return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
- }
-
- var coinInfo coinInfo
- err := json.Unmarshal(infoBytes, &coinInfo)
- if err != nil {
- return sdk.ErrInternal("Error when deserializing coinInfo").Result()
- }
-
- // Msg Issuer is not authorized to issue these coins
- if !bytes.Equal(coinInfo.Issuer, issuer) {
- return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
- }
-
- return sdk.Result{}
- }
-
- // coinInfo stores meta data about a coin
- type coinInfo struct {
- Issuer sdk.AccAddress `json:"issuer"`
- }
在这里我么使用coinInfo来存储每种代币的发行者地址,我们使用JSON序列化此类型并将其直接存储在issue store中。我们当然可以添加更多字段和逻辑,例如包括当前发行的代币的供应量,确保最大供应量,读者可以自行尝试来完成。
Amino
现在我们有两种Msg的实现,消息处理之前不知道哪个类型包含在序列化Tx中,理想情况下,我们会在Msg的接口中去实现,但JSON解码器无法解码为接口类型。实际上,没有标准的方法可以在Go中解码接口类型,这是我们使用Amino的主要原因之一 。
虽然SDK开发人员可以按照他们自己的方式编码交易和状态对象,但Amino是推荐的方式。Amino的目标是改进最新版本的Protocol Buffers , proto3. 为此,Amino与proto3(去除oneof关键字)子集兼容。
虽然oneof提供了联合类型,但Amino目标是在提供接口类型,主要区别在于联合类型,你必须预先知道所有类型,但任何人都可以随时随地实现接口类型,预先是不知道的。
为了实现接口类型,Amino允许接口的任何具体实现注册一个全局唯一的名称,只要序列化该接口类型,该名称就会被携带,这就允许Amino无缝地反序列化为对应的接口类型!
SDK中Amino的主要用途是用于实现Msg接口消息类型的编解码,通过为每个消息注册不同的名称,它们每个都被赋予了不同的Amino前缀,允许在交易中区分它们具体的消息类型。
Amino也可用于存储接口需要持久化时的编解码。
要使用Amino,只需创建一个编解码器,然后注册类型:
- func NewCodec() *wire.Codec {
- cdc := wire.NewCodec()
- cdc.RegisterInterface((*sdk.Msg)(nil), nil)
- cdc.RegisterConcrete(MsgSend{}, "example/MsgSend", nil)
- cdc.RegisterConcrete(MsgIssue{}, "example/MsgIssue", nil)
- crypto.RegisterAmino(cdc)
- return cdc
- }
Amino支持二进制和JSON格式的编码和解码。有关更多详细信息,请参阅编解码器API文档。
Tx
现在我们正在使用Amino,我们可以将Msg接口直接嵌入到 Tx中,我们还添加了公钥和签名数据来进行身份验证。
- // Simple tx to wrap the Msg.
- type app2Tx struct {
- sdk.Msg
-
- PubKey crypto.PubKey
- Signature []byte
- }
-
- // This tx only has one Msg.
- func (tx app2Tx) GetMsgs() []sdk.Msg {
- return []sdk.Msg{tx.Msg}
- }
-
- // Amino decode app2Tx. Capable of decoding both MsgSend and MsgIssue
- func tx2Decoder(cdc *wire.Codec) sdk.TxDecoder {
- return func(txBytes []byte) (sdk.Tx, sdk.Error) {
- var tx app2Tx
- err := cdc.UnmarshalBinary(txBytes, &tx)
- if err != nil {
- return nil, sdk.ErrTxDecode(err.Error())
- }
- return tx, nil
- }
- }
AnteHandler
既然我们实现的Tx包含的不仅仅是Msg,我们就需要指定如何验证和处理额外的信息,这就是AnteHandler的作用。ante的意思表示“之前”,因为 AnteHandler在所有的Handler之前运行。虽然应用程序可以有多个处理程序,每个消息类型一个,但它只能有一个AnteHandler。
AnteHandler类似于Handler程序:
type AnteHandler func(ctx Context, tx Tx) (newCtx Context, result Result, abort bool)
与Handler一样,AnteHandler带有一个Context,根据授权的keys来访问store。但它不是Msg的消息参数,而是一个Tx参数。
像Handler一样,AnteHandler返回一个Result类型,但它同时还返回一个新的 Context和一个abort结果。在App2中,我们只检查PubKey是否与地址匹配,并且是否使用PubKey进行了签名Signature :
- // Simple anteHandler that ensures msg signers have signed.
- // Provides no replay protection.
- func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort bool) {
- appTx, ok := tx.(app2Tx)
- if !ok {
- // set abort boolean to true so that we don't continue to process failed tx
- return ctx, sdk.ErrTxDecode("Tx must be of format app2Tx").Result(), true
- }
-
- // expect only one msg and one signer in app2Tx
- msg := tx.GetMsgs()[0]
- signerAddr := msg.GetSigners()[0]
-
- signBytes := msg.GetSignBytes()
-
- sig := appTx.GetSignature()
-
- // check that submitted pubkey belongs to required address
- if !bytes.Equal(appTx.PubKey.Address(), signerAddr) {
- return ctx, sdk.ErrUnauthorized("Provided Pubkey does not match required address").Result(), true
- }
-
- // check that signature is over expected signBytes
- if !appTx.PubKey.VerifyBytes(signBytes, sig) {
- return ctx, sdk.ErrUnauthorized("Signature verification failed").Result(), true
- }
-
- // authentication passed, app to continue processing by sending msg to handler
- return ctx, sdk.Result{}, false
- }
App2
接下来完成App2:
- func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
-
- cdc := NewCodec()
-
- // Create the base application object.
- app := bapp.NewBaseApp(app2Name, logger, db, txDecoder(cdc))
-
- // Create a key for accessing the account store.
- keyAccount := sdk.NewKVStoreKey("acc")
- // Create a key for accessing the issue store.
- keyIssue := sdk.NewKVStoreKey("issue")
-
- // set antehandler function
- app.SetAnteHandler(antehandler)
-
- // Register message routes.
- // Note the handler gets access to the account store.
- app.Router().
- AddRoute("send", handleMsgSend(keyAccount)).
- AddRoute("issue", handleMsgIssue(keyAccount, keyIssue))
-
- // Mount stores and load the latest state.
- app.MountStoresIAVL(keyAccount, keyIssue)
- err := app.LoadLatestVersion(keyAccount)
- if err != nil {
- cmn.Exit(err.Error())
- }
- return app
- }
相比App1,这里的主要区别在于,我们添加了第二个store,但该store的key仅传递给第二个处理程序,即 handleMsgIssue。第一个处理程序handleMsgSend无法访问这第二个store,也无法读取或写入,确保安全。
我们正在使用Amino,我们创建了一个编解码器,在编解码器上注册我们的类型,并将编解码器传递给我们的TxDecoder构造函数,tx2Decoder。SDK为我们完成剩下的工作!
结论
我们通过添加用于发行代币的新消息类型以及检查签名来扩展我们的第一个应用程序,我们学习了如何使用Amino解码到接口类型,允许我们支持多种Msg类型,并且我们学习了如何使用AnteHandler来验证交易。
但我们的应用程序仍然不安全,因为任何有效的交易都可以多次重放以消耗某人帐号!此外,验证签名和防止重放不是开发人员应该考虑的事情。
在下一节中,我们介绍内置SDK的模块auth和bank,分别为我们的交易提供认证接口和代币转账,同时保证安全的实现。
在之前的应用程序中,我们引入了一种新Msg类型,并使用Amino来编码交易。我们还介绍了Tx的其他使用,介绍了一个简单的验证方法AnteHandler。
在这里,App3我们将介绍两个内置的SDK模块来更换我们自定义的Msg,Tx,Handler,以及AnteHandler,x/auth和x/bank。
该x/auth模块实现Tx与AnteHandler-它有我们需要对交易进行验证一切。它还包括一种新Account类型,可简化store中帐户的操作。
该x/bank模块实现Msg和Handler- 它具有我们在帐户之间转移代币所需的一切。
在这里,我们将介绍来自x/auth和x/bank的重要数据类型,并使用它们来构建App3,这是基于模块构建的最短的应用程序。完整的代码可以在app3.go和本节末尾找到 。
有关更多详细信息,请参阅 x / auth和 x / bank API文档。
帐户
该x/auth模块定义了一个与以太坊非常相似的账户模型。在此模型中,帐户包含:
身份地址
PubKey用于身份验证
AccountNumber用于踢出空帐户
Sequence用于防止交易重放
代币余额
AccountNumber是在创建帐户时分配的唯一编号,并且Sequence每次从帐户发送交易时,Sequence都会增加1。
帐户
- // Account is a standard account using a sequence number for replay protection
- // and a pubkey for authentication.
- type Account interface {
- GetAddress() sdk.AccAddress
- SetAddress(sdk.AccAddress) error // errors if already set.
-
- GetPubKey() crypto.PubKey // can return nil.
- SetPubKey(crypto.PubKey) error
-
- GetAccountNumber() int64
- SetAccountNumber(int64) error
-
- GetSequence() int64
- SetSequence(int64) error
-
- GetCoins() sdk.Coins
- SetCoins(sdk.Coins) error
- }
这是一个低级接口 - 它允许覆盖任何字段。我们很快就会看到,使用Keeper 接口可以限制访问。
BaseAccount
默认实现Account的是BaseAccount:
- // BaseAccount - base account structure.
- // Extend this by embedding this in your AppAccount.
- // See the examples/basecoin/types/account.go for an example.
- type BaseAccount struct {
- Address sdk.AccAddress `json:"address"`
- Coins sdk.Coins `json:"coins"`
- PubKey crypto.PubKey `json:"public_key"`
- AccountNumber int64 `json:"account_number"`
- Sequence int64 `json:"sequence"`
- }
它只包含每个方法的字段。
AccountMapper
在之前使用我们的应用程序中appAccount,我们通过直接在KVStore上执行操作来处理来自store的编码/解码帐户。但是,不受限制地访问KVStore并不是我们想要在我们的应用程序中使用的接口。在SDK中,使用术语Mapper来指代KVStore上的一个抽象,它处理将特定数据类型编组到底层存储和从底层存储解组特定数据类型。
该x/auth模块提供了一个AccountMapper允许我们获取和设置Account store类型的模块。请注意Account 这里使用接口的好处- 开发人员可以实现自己的帐户类型,扩展BaseAccount存储其他数据,而无需从store进行另一次查询。
创建AccountMapper很简单 - 我们只需要指定编解码器,key和正在编码的对象的原型
accountMapper := auth.NewAccountMapper(cdc, keyAccount, auth.ProtoBaseAccount)
然后我们可以获取,修改和设置帐户。例如,我们可以将帐户中的代币数量加倍:
- acc := accountMapper.GetAccount(ctx, addr)
- acc.SetCoins(acc.Coins.Plus(acc.Coins))
- accountMapper.SetAccount(ctx, addr)
请注意,AccountMapper将一个 Context作为第一个参数,并使用创建时授予的key从那里加载KVStore。
另请注意,您必须SetAccount在变更帐户后明确调用以使更改保持不变!
有关更多信息,请参阅AccountMapper API文档。
StdTx
现在我们有了帐户的模型,现在是时候介绍Tx类型auth.StdTx:
- // StdTx is a standard way to wrap a Msg with Fee and Signatures.
- // NOTE: the first signature is the FeePayer (Signatures must not be nil).
- type StdTx struct {
- Msgs []sdk.Msg `json:"msg"`
- Fee StdFee `json:"fee"`
- Signatures []StdSignature `json:"signatures"`
- Memo string `json:"memo"`
- }
这是SDK中交易的标准形式。除了Msgs,它还包括:
第一位签名者需要支付的费用
签名
备注附加数据
有关如何验证Tx这些参数的详细信息,请参见下面的auth.AnteHandler。
签名的标准格式是StdSignature:
- // StdSignature wraps the Signature and includes counters for replay protection.
- // It also includes an optional public key, which must be provided at least in
- // the first transaction made by the account.
- type StdSignature struct {
- crypto.PubKey `json:"pub_key"` // optional
- []byte `json:"signature"`
- AccountNumber int64 `json:"account_number"`
- Sequence int64 `json:"sequence"`
- }
签名包括一个AccountNumber和Sequence。将Sequence与一个交易处理相应的帐户相匹配,并通过一个为每笔交易递增。这可以防止多次重放同一交易,从而解决App2中仍然存在的不安全问题。
该AccountNumber也是重放保护-它允许帐户用完后从store中被删除,如果帐户在删除后收到代币,则会重新创建帐户,并将Sequence重置为0,但会重新创建一个帐户编号。如果不是AccountNumber,则可以重播该帐户在删除之前所做的最后一系列交易!
最后,交易费的标准格式是StdFee:
- // StdFee includes the amount of coins paid in fees and the maximum
- // gas to be used by the transaction. The ratio yields an effective "gasprice",
- // which must be above some miminum to be accepted into the mempool.
- type StdFee struct {
- Amount sdk.Coins `json:"amount"`
- Gas int64 `json:"gas"`
- }
费用必须由第一位签名者支付。这使我们能够快速检查是否可以支付交易费用,如果没有,则拒绝交易。
签名
在StdTx支持多个消息和多个签名者。要签署交易,每位签名者必须收集以下信息:
ChainID
给定签名者帐户的AccountNumber和Sequence(来自区块链)
交易费
交易消息列表
一个可选的附加信息
然后他们可以使用auth.StdSignBytes函数计算要签名的交易字节 :
bytesToSign := StdSignBytes(chainID, accNum, accSequence, fee, msgs, memo)
请注意,这些字节对于每个签名者都是唯一的,因为它们取决于特定签名者AccountNumber,Sequence和附加信息。为了便于在签名之前进行检查,字节实际上只是所有相关信息的JSON编码形式。
AnteHandler
正如我们所看到的App2,我们可以在处理任何内部消息之前使用AnteHandler 来验证事务。虽然之前我们实现了自己的简单AnteHandler,但该x/auth模块提供了一个更高级的模块,它使用AccountMapper和使用StdTx:
app.SetAnteHandler(auth.NewAnteHandler(accountMapper, feeKeeper))
提供的AnteHandler强制执行以下规则:
附加数据一定不能太大
必须提供正确数量的签名(每个签名的唯一签名者msg.GetSigner一个msg)
任何首次签名的帐户都必须包含StdSignature中的公钥
以与消息指定的顺序相同的顺序进行身份验证时,签名必须有效
请注意,验证签名需要检查每个签名者是否使用了正确的帐号和序列,因为此信息是必需的StdSignBytes。
如果不满足上述任何条件,AnteHandelr将返回错误。
如果所有上述验证都通过,AnteHandler会对状态进行以下更改:
为所有签名者增加一个帐户序列
为任何首次签名者设置帐户中的pubkey
从第一个签名者的帐户中扣除费用
回想一下,增加Sequence阻止“重放攻击”可以一次又一次地执行相同的消息。
PubKey是签名验证所必需的,但只需在StdSignature中使用一次。从那时起,它将存储在帐户中。
该费用由返回的第一个地址,支付msg.GetSigners()了第一Msg,通过所提供的FeePayer(tx Tx) sdk.AccAddress功能。
CoinKeeper
现在我们已经看到了auth.AccountMapper以及如何构建一个完整的AnteHandler,现在是时候看看如何构建更高级别的抽象来形成帐户接口了。
早些时候,我们注意到对MappersKVStore的抽象是处理与底层存储之间的编码和解码数据类型。我们可以在Mappers调用Keepers的基础上构建另一个抽象,它只暴露由存储的底层类型的限制功能Mapper。
例如,该x/bank模块定义的规范的版本MsgSend 和MsgIssue用于SDK,以及一个Handler用于处理它们。但是,我们不是将一个KVStore或者甚至是AccountMapper直接传递给处理程序,而是引入一个bank.Keeper,它只能用于将代币转入和转出帐户。这允许我们Handler预先确定bank模块可以对store产生的唯一影响是更改帐户中的代币数量 - 它不能增加序列号,更改PubKeys或其他。
bank.Keeper很容易从以下实例中实例化AccountMapper:
coinKeeper = bank.NewKeeper(accountMapper)
然后我们可以在处理程序中使用它,而不是直接使用它 AccountMapper。例如,要向帐户添加代币:
- // Finds account with addr in AccountMapper.
- // Adds coins to account's coin array.
- // Sets updated account in AccountMapper
- app.coinKeeper.AddCoins(ctx, addr, coins)
有关完整的方法集,请参阅bank.Keeper API文档。
注意我们可以bank.Keeper通过限制它的方法集来改进它。例如, bank.ViewKeeper 是只读版本,而 bank.SendKeeper 仅执行从输入帐户到输出帐户的硬币转移。
我们Keeper在SDK中广泛使用此范例作为定义每个模块可以访问的功能类型的方式。特别是,我们试图遵循最小权威原则。而不是提供完全成熟的访问KVStore或者AccountMapper,我们只允许少数是做的非常具体的事情功能。
App3
随着auth.AccountMapper和bank.Keeper在手,我们现在已经准备好构建App3。而x/auth和x/bank模块做了所有的工作:
- func NewApp3(logger log.Logger, db dbm.DB) *bapp.BaseApp {
-
- // Create the codec with registered Msg types
- cdc := NewCodec()
-
- // Create the base application object.
- app := bapp.NewBaseApp(app3Name, logger, db, auth.DefaultTxDecoder(cdc))
-
- // Create a key for accessing the account store.
- keyAccount := sdk.NewKVStoreKey("acc")
- keyFees := sdk.NewKVStoreKey("fee") // TODO
-
- // Set various mappers/keepers to interact easily with underlying stores
- accountMapper := auth.NewAccountMapper(cdc, keyAccount, auth.ProtoBaseAccount)
- coinKeeper := bank.NewKeeper(accountMapper)
- feeKeeper := auth.NewFeeCollectionKeeper(cdc, keyFees)
-
- app.SetAnteHandler(auth.NewAnteHandler(accountMapper, feeKeeper))
-
- // Register message routes.
- // Note the handler gets access to
- app.Router().
- AddRoute("send", bank.NewHandler(coinKeeper))
-
- // Mount stores and load the latest state.
- app.MountStoresIAVL(keyAccount, keyFees)
- err := app.LoadLatestVersion(keyAccount)
- if err != nil {
- cmn.Exit(err.Error())
- }
- return app
- }
注意我们只使用bank.NewHandler,只处理bank.MsgSend,接收bank.Keeper。有关 更多详细信息,请参阅 x / bank API文档。
我们还使用默认的txDecoder x/auth,它解码amino编码的 auth.StdTx交易。
结论
拥有模块进行身份验证和代币转账,使用了mappers和keepers,已经建立了安全状态机,我们发现自己在这里进行了全面检查,多资产加密货币 - 这是Cosmos-SDK的核心。
应用区块链接口(ABCI),是在Cosmos SDK和Tendermint之间的一道清晰的界线。它将应用程序的状态机,与在多台机器之间进行安全拷贝的复制引擎这两部分隔离开来。
通过在应用程序和共识之间提供一个清晰的,无关语言的界线,ABCI提供了极大的开发者灵活性并支持多种语言。也就是说,它仍然是一个相当底层的协议,架构需要创建在底层组件之上。Cosmos SDK就是这样的一个框架。
尽管我们已经看到了DeliverTx这样的ABCI应用程序,这里我们将介绍Tendermint发出的其它ABCI请求,还有如何使用它们创建更先进的应用程序。想要获得关于ABCI完整的描述和使用方法,请查看说明。
InitChain
在之前的应用中,我们实现了所有的核心逻辑,但是我们还未指定要如何初始化存储。为此,我们使用了app.initChain方法,它会在应用程序首次启动时被Tendermint调用一次。
InitChain请求包含了多种Tendermint信息,比如共识层的参数和初始的验证人集合,还包含了一个不明确的应用专用字节流——通常为JSON编码格式。应用可以通过调用app.SetInitChainer方法决定用这些信息去做些什么。例如,我们引入一个GenesisAccount结构体,其能被JSON编码,而且是genesis文件的一部分。在InitChain期间,我们可以用一些账户信息来填充存储:
- // https://github.com/cosmos/cosmos-sdk/blob/v0.24.1/docs/sdk/core/examples/app4.go
-
- // GenesisAccount doesn't need pubkey or sequence
- type GenesisAccount struct {
- Address sdk.AccAddress `json:"address"`
- Coins sdk.Coins `json:"coins"`
- }
如果我们在Tendermint的genesis.json文件里包含了一个正确格式的GenesisAccount,存储会同这些账户一起被初始化,它们就能发送交易了!
BeginBlock
BeginBlock在由DeliverTx生成处理任何交易之前,每个区块开始时被调用。它包含了那些验证人签名过的信息。
EndBlock
EndBlock在每个区块最后,DeliverTx处理完所有的交易之后被调用。它允许应用程序返回对验证人集合的更新。
Commit
Commit在EndBlock之后调用。它对应用程序的状态做了持久化存储,并返回一个将被下一个Tendermint区块包含的默克尔树的根哈希值。根哈希可以在Query中用作状态的默克尔树证明。
Query
Query允许对应用程序的存储按照一个路径去查询。
CheckTx
CheckTx用于交易池。它只运行AnteHandler。消息处理直到交易已经被提交到区块时才开始处理的代价是非常之高的。AnteHandler对发送者授权,确保他们有足够的手续费去支付。如果之后交易失败,发送者仍然会支付这笔费用。
如我们所见,Cosmos SDK提供了一个足够灵活的综合框架来创建状态机,定义它们如何转变,还有对交易进行身份验证,执行消息,控制对存储的访问权限,以及更新验证人集合。
目前为止,我们关注的都是如何把ABCI应用与证明隔离开来,还有解释Cosmos SDK的多个特性和灵活性。这里,我们将会把ABCI应用程序连接到Tendermint,这样我们就能运行一个完整的区块链节点,然后引入命令行和HTTP接口来与之交互。
首先,我们讨论下源码要如何部署。
目录结构
TODO
Tendermint节点
因为Cosmos SDK由Go编写,Cosmos SDK应用程序能够被Tendermint编译成一个独立的二进制可执行文件。当然,如同所有的ABCI应用程序那样,它们可以独立运行,与Tendermint通过套接字来通信。
要了解如何启动一个Tendermint全节点,请查看github.com/tendermint/tendermint/node中的NewNode函数。Cosmos SDK中的server包简化了把一个应用程序同一个Tendermint节点连接的操作。例如下面的main.go文件给出了使用我们之前创建的Basecoin来创建一个完整的全节点:
- func main() {
- cdc := app.MakeCodec()
- ctx := server.NewDefaultContext()
-
- rootCmd := &cobra.Command{
- Use: "basecoind",
- Short: "Basecoin Daemon (server)",
- PersistentPreRunE: server.PersistentPreRunEFn(ctx),
- }
-
- server.AddCommands(ctx, cdc, rootCmd, server.DefaultAppInit,
- server.ConstructAppCreator(newApp, "basecoin"),
- server.ConstructAppExporter(exportAppStateAndTMValidators, "basecoin"))
-
- // prepare and add flags
- rootDir := os.ExpandEnv("$HOME/.basecoind")
- executor := cli.PrepareBaseCmd(rootCmd, "BC", rootDir)
-
- err := executor.Execute()
- if err != nil {
- // Note: Handle with #870
- panic(err)
- }
- }
-
- func newApp(logger log.Logger, db dbm.DB, storeTracer io.Writer) abci.Application {
- return app.NewBasecoinApp(logger, db, baseapp.SetPruning(viper.GetString("pruning")))
- }
注意,我们使用了通用的cobra library用于实现CLI,为了管理配置使用了viper library。查看我们的cli library来获取详情
TODO:编译和运行这个二进制文件
运行basecoind二进制文件的参数对与运行tendermint同样有效。查看使用Tendermint来获取详情。
客户端
TODO
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。