万字雄文讲解如何用cosmos构建自己的区块链
用cosmos构建自己的区块链——总体设计
你正在构建的应用程序的目标是让用户购买域名并为其设置解析的值。给定域名的所有者将是当前最高出价者。在本节中,你将了解如何将这些简单需求转化为程序的设计。
区块链应用程序只是一个具有确定性的复制状态机。作为开发人员,你只需定义状态机(即状态,启动状态和触发状态转变的消息),Tendermint 将为你处理通过网络进行复制。
Tendermint是一个与应用程序无关的引擎,负责处理区块链的网络层和共识层。实际上,这意味着Tendermint负责传播和排序交易字节。Tendermint Core依赖于拜占庭容错(BFT)算法来达成交易顺序的共识。点击这里了解更多Tendermint相关信息。
Cosmos SDK 旨在帮助你构建状态机。SDK是一个模块化框架,意味着应用程序是通过将一组可互操作的模块集成在一起构建而成的。每个模块都包含自己的消息/交易处理器,而SDK负责将每条消息路由到其对应模块。
以下是nameservice应用程序所需的模块:
auth
: 此模块定义了账户和手续费,并为你应用程序的其余部分提供了访问这些功能的权限。bank
: 此模块使得应用程序能够创建和管理token及余额。nameservice
: 此模块目前还不存在其将处理你所构建的nameservice
应用的核心逻辑。它是你构建应用程序时必须使用的主要部分。
你可能会好奇为什么没有模块来处理验证人集合的变更。实际上,Tendermint依靠一组验证人来对下一个要添加至区块链的有效交易区块达成共识。默认情况下,如果没有模块处理验证集合的变更,验证人集合将与创世文件
genesis.json
中定义的验证人集合保持一致。该应用程序就是这种情况。如果要允许更改应用程序的验证人集合,可以使用SDK的 staking 模块,或编写自己的模块
现在,看一下应用程序的两个主要部分:state(状态) 和 message(消息)类型。
State
state反映了特定时刻你的应用程序。它告诉了每个帐户拥有多少token,每个域名的所有者和价格,以及每个域名的解析值。
token 和帐户的 state 由auth
和bank
模块定义,这意味着你现在不必关心它。你需要做的是定义与你的nameservice
模块特定相关部分state。
在 SDK 中,所有内容都存储在一个名为multistore
的存储中。可以在此 multistore 中创建任意数量的键值对存储(在Cosmos SDK中称作KVStore
)。在本应用中,我们将使用一个 store 记录 name
与 whois
信息,name
的 value、owner 和 price 将存储在一个结构中。
Message
message 包含在 transaction 中。它们负责触发 state 的转变。每个模块定义了一个 message 列表及如何去处理它们。下面这些 message 是你需要为你的 nameservice 应用去实现的:
MsgSetName
: 此 message 允许域名的所有者为指定域名的nameStore
设置一个值。MsgBuyName
: 此 message 允许账户去购买一个域名并在ownerStore
中成为所有者。- 当有人购买一个域名时,他们需要支付币之前所有者购买价格更高的费用。如果域名还没有人购买,那么他们需要销毁最小价格(
MinPrice
)的代币。
- 当有人购买一个域名时,他们需要支付币之前所有者购买价格更高的费用。如果域名还没有人购买,那么他们需要销毁最小价格(
当一条交易(包含在区块中)到达一个Tendermint节点时,它将通过 ABCI 传递给应用程序并被解码以得到 message。然后将message路由至对应的模块,并根据定义在Handler
中的逻辑来进行处理。如果 state 需要更新,Handler
会调用Keeper
来执行更新。你将在后面的教程了解有关这些概念的更多信息。
现在你已经从高层视角完成了对应用程序的设计,是时候开始实现它了。
用cosmos构建自己的区块链——开始编写你的代码
首先创建一个新的文件./app.go
。这个文件是确定性状态机的核心。
在app.go
中,你定义了应用程序在接收交易时执行的操作。但首先,它要能够以正确的顺序接收交易。这是 Tendermint共识引擎的职责。
引入必要的依赖:
package appimport ( "github.com/tendermint/tendermint/libs/log" "github.com/cosmos/cosmos-sdk/x/auth" bam "github.com/cosmos/cosmos-sdk/baseapp" dbm "github.com/tendermint/tm-db")
下面是各引入模块和包的文档:
log
: Tendermint 的日志auth
: Cosmos SDK 的auth
模块dbm
: Tendermint 的数据库代码baseapp
: 如下
这里有几个包是tendermint
包。Tendermint 通过名为 ABCI 的接口将交易从网络传递给应用程序。如果你要查看正在构建的区块链节点的架构,如下所示:
+---------------------+ | | | Application | | | +--------+---+--------+ ^ | | | ABCI | v +--------+---+--------+ | | | | | Tendermint | | | | | +---------------------+
幸运的是,你不必实现ABCI接口。Cosmos SDK以baseapp
的形式提供了它的实现样板。
baseapp
做了以下几点:
- 解码从 Tendermint 共识引擎接收到的交易。
- 从交易中提取 messages 并做基本的合理性校验。
- 将这些 message 路由到合适的模块使其被正确处理。注意
baseapp
并不了解你想要使用的具体模块。你要做的就是在app.go
中声明这些模块,在接下来的教程中将会看到这些工作。baseapp
仅实现了适用于任意模块的核心路由逻辑。 - 如果 ABCI 消息是
DeliverTx
(CheckTx
)的话就Commit。 - 帮助设置
BeginBlock
和EndBlock
,这两种消息让你能定义在每个区块开始和结束时执行的逻辑。实际上,每个模块实现了各自的BeginBlock
和EndBlock
子逻辑,app的职责是它们都聚合起来。(注意:你不会在你的应用程序中使用这些消息) - 帮助初始化你的 state。
- 帮助设置 queries。
现在你需要为应用程序创建一个新的自定义类型nameServiceApp
。这个类型将嵌入baseapp
(在Go中的嵌入类似于其他语言中的继承),这意味着它可以访问baseapp
的所有方法。
const ( appName = "nameservice")type nameServiceApp struct { *bam.BaseApp}
为你的应用添加一个简单的构造函数:
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp { // First define the top level codec that will be shared by the different modules. Note: Codec will be explained later cdc := MakeCodec() // BaseApp handles interactions with Tendermint through the ABCI protocol bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc)) var app = &nameServiceApp{ BaseApp: bApp, cdc: cdc, } return app}
很好现在你有了应用程序的骨架;但是,仍然缺少具体功能。
baseapp
不了解你要在应用程序中使用的路由或用户交互。应用程序的主要作用是定义这些路由。另一个作用是定义初始状态。这两件事都要求你向应用程序添加模块。
正如你在应用程序设计章节中看到的,你的nameservice需要三个模块:auth
,bank
和nameservice
。前两个已经存在了,但最后一个还没有nameservice
模块将定义你的状态机的大部分内容。下一步是构建它。
为了完成应用程序,你需要引入一些模块。 继续开始构建你的域名服务模块 。 稍后会回到 app.go.
用cosmos构建自己的区块链——定义你的数据结构
我们要做的第一件事是定义一个结构,包含域名所有元数据。依据 ICANN DNS 术语,我们之后将此结构称为 Whois。
types.go
首先创建文件 ./x/nameservice/internal/types/types.go
在其内定义模块自有类型,在 Cosmos SDK 应用中,习惯上将模块相关的代码放在 ./x/
文件夹中。
Whois
每个域名有三个预期相关的数据:
- Value – 域名解析出的值。这是任意字符串,但将来您可以修改它以要求它适合特定格式,例如IP地址,DNS区域文件或区块链地址。
- Owner – 该域名当前所有者的地址。
- Price – 你需要为购买域名支付的费用。
要开始你的 SDK 模块,在 ./x/nameservice/internal/types/types.go
文件中定义 nameservice.Whois
结构。
package nameservice
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Whois is a struct that contains all the metadata of a name
type Whois struct {
Value string `json:"value"`
Owner sdk.AccAddress `json:"owner"`
价格sdk.Coins `json:"price"`
}
在设计文档中提到过,如果名称尚未有所有者,我们希望使用 MinPrice 对其进行初始化。
// Initial Starting 价格for a name that was never previously owned
var MinNamePrice = sdk.Coins{sdk.NewInt64Coin("nametoken", 1)}
// Returns a new Whois with the minprice as the price
func NewWhois() Whois {
return Whois{
Price: MinNamePrice,
}
}
现在我们继续去编写 Keeper 模块的代码。
用cosmos构建自己的区块链——编写Keeper
Cosmos SDK模块的主要核心是名为Keeper
的部分。它处理同存储的交互,引用其他的keeper进行跨模块的交互,并包含模块的大部分核心功能。
首先创建文件./x/nameservice/internal/keeper/keeper.go
来保存模块的keeper。在 Cosmos SDK 应用程序中,模块通常放在./x/
文件夹中。
Keeper结构
开始制作你的SDK模块,请在./x/nameservice/internal/keeper/keeper.go
文件中定义nameservice.Keeper
:
package nameservice
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/bank"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
coinKeeper bank.Keeper
storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context
cdc *codec.Codec // The wire codec for binary encoding/decoding.
}
关于上述代码的几点说明:
- 3个不同的
cosmos-sdk
包被引入:codec
– 提供负责Cosmos编码格式的工具——Amino。bank
–bank
模块控制账户和转账。types
–types
包含了整个SDK常用的类型。
Keeper
结构体。在 keeper 中有几个关键部分:bank.Keeper
: 这是bank
模块的Keeper
引用。包括它来允许该模块中的代码调用bank
模块的函数。SDK使用对象能力
来访问应用程序状态的各个部分。这是为了允许开发人员采用小权限准入原则,限制错误或恶意模块的去影响其不需要访问的状态的能力。*codec.Codec
: 这是被Amino用于编码及解码二进制机构的编码解码器的指针。sdk.StoreKey
: 通过它来访问一个持久化保存你的应用程序状态的sdk.KVStore
。
- 模块有1个StoreKey:
storeKey
– 这是 name 指向(如map[name]Whois
)Whois 结构的主存储空间,
Getter 和 Setter
现在要添加通过Keeper
来与存储交互的方法了。首先,添加一个函数来为指定域名设置解析字符串值:
// Sets the entire Whois metadata struct for a name
func (k Keeper) SetWhois(ctx sdk.Context, name string, whois Whois) {
if whois.Owner.Empty() {
return
}
store := ctx.KVStore(k.storeKey)
store.Set([]byte(name), k.cdc.MustMarshalBinaryBare(whois))
}
在此方法中,首先使用Keeper
中的namesStoreKey
获取map[name]value
的存储对象。
注意:这个函数使用
sdk.Context
。该对象持有访问像blockHeight
和chainID
这样重要部分状态的函数。
接下来,你可以使用方法.Set([]byte,[]byte)
向存储中插入键值对。由于存储只接受
[]byte
,想要把string
转化成[]byte
再把它们作为参数传给Set
方法。
接下来,添加一个函数来解析域名(即查找域名对应的解析值):
// Gets the entire Whois metadata struct for a name
func (k Keeper) GetWhois(ctx sdk.Context, name string) Whois {
store := ctx.KVStore(k.storeKey)
if !store.Has([]byte(name)) {
return NewWhois()
}
bz := store.Get([]byte(name))
var whois Whois
k.cdc.MustUnmarshalBinaryBare(bz, &whois)
return whois
}
这里,与SetName
方法一样,首先使用StoreKey
访问存储。接下来,不使用使用.Get([] byte) []byte
方法而不是Set
方法。向函数传参,传递key值,要把name
字符串转化成[]byte
,并以[]byte
的形式返回结果。将此转换成字符串再返回。
如果一个域名尚未在存储中,它返回一个新的 Whois 信息,包含最低价格 MinPrice。
现在,我们添加了根据名称从 store 获取特定参数的功能。我们重用了 GetWhois 和 SetWhois 函数,而不是重写 store 的 getter 和 setter。例如,要设置字段,首先我们获取整个 Whois 数据,更新我们的特定字段,然后将新版本放回 store。
// ResolveName - returns the string that the name resolves to
func (k Keeper) ResolveName(ctx sdk.Context, name string) string {
return k.GetWhois(ctx, name).Value
}
// SetName - sets the value string that a name resolves to
func (k Keeper) SetName(ctx sdk.Context, name string, value string) {
whois := k.GetWhois(ctx, name)
whois.Value = value
k.SetWhois(ctx, name, whois)
}
// HasOwner - returns whether or not the name already has an owner
func (k Keeper) HasOwner(ctx sdk.Context, name string) bool {
return !k.GetWhois(ctx, name).Owner.Empty()
}
// GetOwner - get the current owner of a name
func (k Keeper) GetOwner(ctx sdk.Context, name string) sdk.AccAddress {
return k.GetWhois(ctx, name).Owner
}
// SetOwner - sets the current owner of a name
func (k Keeper) SetOwner(ctx sdk.Context, name string, owner sdk.AccAddress) {
whois := k.GetWhois(ctx, name)
whois.Owner = owner
k.SetWhois(ctx, name, whois)
}
// GetPrice - gets the current price of a name. If price doesn't exist yet, set to 1nametoken.
func (k Keeper) GetPrice(ctx sdk.Context, name string) sdk.Coins {
return k.GetWhois(ctx, name).Price
}
// SetPrice - sets the current price of a name
func (k Keeper) SetPrice(ctx sdk.Context, name string, price sdk.Coins) {
whois := k.GetWhois(ctx, name)
whois.Price = price
k.SetWhois(ctx, name, whois)
}
SDK 还有一个特性叫 sdk.Iterator
,可以返回一个迭代器用于遍历指定 store 中的所有 对。
我们增加一个函数用于获取遍历 store 中所有已知域名的迭代器。
// Get an iterator over all names in which the keys are the names and the values are the whois
func (k Keeper) GetNamesIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, []byte{})
}
最后需要在./x/nameservice/keeper.go
文件中加上Keeper
的构造函数:
// NewKeeper creates new instances of the nameservice Keeper
func NewKeeper(coinKeeper bank.Keeper, storeKey sdk.StoreKey, cdc *codec.Codec) Keeper {
return Keeper{
coinKeeper: coinKeeper,
storeKey: storeKey,
cdc: cdc,
}
}
下一节我们会描述如何让用户通过 Msgs
and Handlers
与刚刚建立的 store 交互。
用cosmos构建自己的区块链——编写消息和消息处理器
前面我们已经设置了Keeper
,是时候构建允许用户购买域名和设置解析值的消息Msg
和处理消息的Handler
了。
Msg
Msg
触发状态转变。Msgs
被点评在客户端提交至网络的Txs
中。Cosmos SDK从Txs
中打包和解包来自Msgs
,这就意味着,作为一个应用开发者,你只需要去定义Msgs
。Msgs
必须要满足下面的接口(我们会在下一小节实现):
// Transactions messages must fulfill the Msg
type Msg interface {
// Return the message type.
// Must be alphanumeric or empty.
Type() string
// Returns a human-readable string for the message, intended for utilization
// within tags
Route() 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.
GetSignBytes() []byte
// Signers 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
}
Handler
Handler
定义了在接收到一个特定Msg
时,需要采取的操作(哪些存储需要更新,怎样更新及要满足什么条件)。
在此模块中,你有两种类型的Msg
,用户可以发送这些Msg
来和应用程序状态进行交互:SetName
和BuyName
。它们各自同其Handler
关联。
现在你已经更好地理解了 Msgs 和 Handler,下一节我们开始构建第一条消息:SetName
。
用cosmos构建自己的区块链——SetName
Msg
SDK中Msg
的命令约束是 Msg{.Action}
。要实现的第一个操作是SetName
,因此命名为MsgSetName
。此Msg
允许域名的所有者设置该域名的解析返回值。首先在名为./x/nameservice/msgs.go
的新文件中定义MsgSetName
:
package nameservice
import (
"encoding/json"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// MsgSetName defines a SetName message
type MsgSetName struct {
Name string
Value string
Owner sdk.AccAddress
}
// NewMsgSetName is a constructor function for MsgSetName
func NewMsgSetName(name string, value string, owner sdk.AccAddress) MsgSetName {
return MsgSetName{
Name: name,
Value: value,
Owner: owner,
}
}
MsgSetName
具有设置域名解析值所需的三个属性:
name
– 所要设置的域名value
– 要设置的域名解析值owner
– 域名的所有者
接下来,实现Msg
接口:
// Route should return the name of the module
func (msg MsgSetName) Route() string { return "nameservice" }
// Type should return the action
func (msg MsgSetName) Type() string { return "set_name"}
SDK使用上述函数将Msg
路由至合适的模块进行处理。它们还为用于索引的数据库标签添加了可读性的名称。
// ValidateBasic runs stateless checks on the message
func (msg MsgSetName) ValidateBasic() sdk.Error {
if msg.Owner.Empty() {
return sdk.ErrInvalidAddress(msg.Owner.String())
}
if len(msg.Name) == 0 || len(msg.Value) == 0 {
return sdk.ErrUnknownRequest("Name and/or Value cannot be empty")
}
return nil
}
ValidateBasic
用于对Msg
的有效性进行一些基本的无状态检查。在此情形下,请检查没有属性为空。请注意这里使用sdk.Error
类型。SDK提供了一组应用开发人员经常遇到的错误类型。
// GetSignBytes encodes the message for signing
func (msg MsgSetName) GetSignBytes() []byte {
b, err := json.Marshal(msg)
if err != nil {
panic(err)
}
return sdk.MustSortJSON(b)
}
GetSignBytes
定义了如何编码Msg
以进行签名(这里并没有进行签名,只是先编码成排序好的json)。在大多数情形下,要编码成排好序的JSON。不应修改输出。
// GetSigners defines whose signature is required
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Owner}
}
GetSigners
定义一个Tx
上需要哪些人的签名才能使其有效。在这种情形下,MsgSetName
要求域名所有者在尝试重置域名解析值时要对该交易签名。
Handler
现在MsgSetName
已经定义好了,下一部来定义收到此Msg时需要采取的操作。也就是handler
所要做的。
在一个新文件(./x/nameservice/handler.go
)先写入如下代码:
package nameservice
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSetName:
return handleMsgSetName(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
NewHandler
本质上是一个子路由,它将进入该模块的msg路由到正确的handler做处理。目前,只有一个 Msg
/Handler
。
现在,你需要定义在handlerMsgSetName
中定义处理MsgSetName
消息的实际逻辑:
注意:SDK中handler的命名规范是
handlerMsg{.Action}
// Handle a message to set name
func handleMsgSetName(ctx sdk.Context, keeper Keeper, msg MsgSetName) sdk.Result {
if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the the msg sender is the same as the current owner
return sdk.ErrUnauthorized("Incorrect Owner").Result() // If not, throw an error
}
keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
return sdk.Result{} // return
}
在此函数中,要检查Msg
的发送者是否就是域名的所有者(keeper.GetOwner)。如果是,就能通过调用Keeper
里的函数来设置域名。如果不是,则抛出错误并返回给用户。
很好,现在所有者可以 SetName,但在域名还没有出售时,你的模块需要有一个途径让用户购买域名。下一节我们来一起定义 BuyName 消息。
用cosmos构建自己的区块链——BuyName
Msg
现在来定义购买域名的Msg
,并加在./x/nameservice/msgs.go
文件中。代码同SetName
非常相似:
// MsgBuyName defines the BuyName message
type MsgBuyName struct {
Name string
Bid sdk.Coins
Buyer sdk.AccAddress
}
// NewMsgBuyName is the constructor function for MsgBuyName
func NewMsgBuyName(name string, bid sdk.Coins, buyer sdk.AccAddress) MsgBuyName {
return MsgBuyName{
Name: name,
Bid: bid,
Buyer: buyer,
}
}
// Route should return the name of the module
func (msg MsgBuyName) Route() string { return "nameservice" }
// Type should return the action
func (msg MsgBuyName) Type() string { return "buy_name" }
// ValidateBasic runs stateless checks on the message
func (msg MsgBuyName) ValidateBasic() sdk.Error {
if msg.Buyer.Empty() {
return sdk.ErrInvalidAddress(msg.Buyer.String())
}
if len(msg.Name) == 0 {
return sdk.ErrUnknownRequest("Name cannot be empty")
}
if !msg.Bid.IsAllPositive() {
return sdk.ErrInsufficientCoins("Bids must be positive")
}
return nil
}
// GetSignBytes encodes the message for signing
func (msg MsgBuyName) GetSignBytes() []byte {
b, err := json.Marshal(msg)
if err != nil {
panic(err)
}
return sdk.MustSortJSON(b)
}
// GetSigners defines whose signature is required
func (msg MsgBuyName) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Buyer}
}
接着,在./x/nameservice/handler.go
文件中,把MsgBuyName
加入到模块路由器中:
// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSetName:
return handleMsgSetName(ctx, keeper, msg)
case MsgBuyName:
return handleMsgBuyName(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
最后,定义BuyName
的handler
,该函数执行由msg触发的状态转换。请记住,此时msg已运行其ValidateBasic
函数,因此已进行了一些输入验证。但是,ValidateBasic
无法查询应用程序状态。应在handler
中执行依赖于网络状态(例如帐户余额)的验证逻辑。
// Handle a message to buy name
func handleMsgBuyName(ctx sdk.Context, keeper Keeper, msg MsgBuyName) sdk.Result {
if keeper.GetPrice(ctx, msg.Name).IsAllGT(msg.Bid) { // Checks if the the bid price is greater than the price paid by the current owner
return sdk.ErrInsufficientCoins("Bid not high enough").Result() // If not, throw an error
}
if keeper.HasOwner(ctx, msg.Name) {
_, err := keeper.coinKeeper.SendCoins(ctx, msg.Buyer, keeper.GetOwner(ctx, msg.Name), msg.Bid)
if err != nil {
return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
}
} else {
_, _, err := keeper.coinKeeper.SubtractCoins(ctx, msg.Buyer, msg.Bid) // If so, deduct the Bid amount from the sender
if err != nil {
return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
}
}
keeper.SetOwner(ctx, msg.Name, msg.Buyer)
keeper.SetPrice(ctx, msg.Name, msg.Bid)
return sdk.Result{}
}
首先确保出价高于当前价格。然后,检查域名是否已有所有者。如果有,之前的所有者将会收到Buyer
的钱。
如果没有所有者,你的nameservice
模块会把Buyer
的资金“销毁”(即发送到不可恢复的地址)。
如果SubtractCoins
或SendCoins
返回一个非空错误,handler会抛出一个错误,回退状态转变。没有的话,使用之前在Keeper
上定义的 getter 和 setter,handler 将买方设置为新所有者,并将新价格设置为当前出价。
注意:此handler使用
coinKeeper
中的函数执行货币相关操作。如果你的应用程序正在执行货币相关操作,你可能需要查看此模块的文档,以查看它提供的功能。
现在已经有了 Msgs
and Handlers
定义,下一节我们来学习如何使交易中的数据能被查询到
用cosmos构建自己的区块链——查询域名
首先创建./x/nameservice/querier.go
文件。在这里定义应用程序用户可以对哪些状态进行查询。你的nameservice
模块会暴露两个querier:
resolve
: 传入一个域名
返回nameservice
给定的解析值
。类似于DNS查询。whois
: 传入一个域名
返回价格
,解析值
和域名的所有者
。用于确定你想要购买名称的成本。
首先定义NewQuerier
函数,该函数充当查询此模块的子路由器(类似于NewHandler
函数)。请注意,因为querier没有类似于Msg的接口,所以需要手动定义switch语句(它们无法从query.Route()函数中删除):
package nameservice
import (
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)
// query endpoints supported by the nameservice Querier
const (
QueryResolve = "resolve"
QueryWhois = "whois"
QueryNames = "names"
)
// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
switch path[0] {
case QueryResolve:
return queryResolve(ctx, path[1:], req, keeper)
case QueryWhois:
return queryWhois(ctx, path[1:], req, keeper)
case QueryNames:
return queryNames(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unknown nameservice query endpoint")
}
}
}
现在已定义路由器,为每个查询定义输入和响应:
// nolint: unparam
func queryResolve(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
name := path[0]
value := keeper.ResolveName(ctx, name)
if value == "" {
return []byte{}, sdk.ErrUnknownRequest("could not resolve name")
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, QueryResResolve{value})
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}
// Query Result Payload for a resolve query
type QueryResResolve struct {
Value string `json:"value"`
}
// implement fmt.Stringer
func (r QueryResResolve) String() string {
return r.Value
}
// nolint: unparam
func queryWhois(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
name := path[0]
whois := keeper.GetWhois(ctx, name)
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, whois)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}
// implement fmt.Stringer
func (w Whois) String() string {
return strings.TrimSpace(fmt.Sprintf(`Owner: %s
Value: %s
Price: %s`, w.Owner, w.Value, w.Price))
}
func queryNames(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
var namesList QueryResNames
iterator := keeper.GetNamesIterator(ctx)
for ; iterator.Valid(); iterator.Next() {
name := string(iterator.Key())
namesList = append(namesList, name)
}
bz, err2 := codec.MarshalJSONIndent(keeper.cdc, namesList)
if err2 != nil {
panic("could not marshal result to JSON")
}
return bz, nil
}
// Query Result Payload for a names query
type QueryResNames []string
// implement fmt.Stringer
func (n QueryResNames) String() string {
return strings.Join(n[:], "\n")
}
上述代码中要注意:
- 在这里你的
Keeper
的 getter 和 setter 方法被大量使用。在构建使用此模块的任何其他应用程序时,你可能需要返回并定义更多getter/setter以访问所需的状态。 - 按照惯例,每个输出类型都应该是 JSON marshallable 和 stringable(实现 Golang fmt.Stringer 接口)。返回的字节应该是输出结果的JSON编码。
- 因此,对于输出类型的解析,我们将解析字符串盘点在一个名为 QueryResResolve 的结构中,该结构既是JSON marshallable 的又有.String()方法。
- 对于 Whois 的输出,正常的 Whois 结构已经是 JSON marshallable 的,但我们需要在其上添加.String()方法。
- 名称查询的输出也一样,[]字符串本身已经可 marshallable ,但我们需要在其上添加.String()方法。
用cosmos构建自己的区块链——注册接口,编写交互命令
经过前面的学习,基本完成了我们域名链所有功能的编写了,现在对我们前面编写的接口进行注册,然后开始编写和程序交互的命令,这样我们在启动程序后就可以通过命令来对区块链进行各种操作了。
在Amino中注册你的数据类型使得它们能够被编码/解码,有一些代码需要放在./x/nameservice/codec.go
中。你创建的任何接口和实现接口的任何结构都需要在RegisterCodec
函数中声明。在此模块中,需要注册两个Msg
的实现(SetName
和BuyName
),但你的Whois
查询返回的类型不需要:
package nameservice
import (
"github.com/cosmos/cosmos-sdk/codec"
)
// RegisterCodec registers concrete types on wire codec
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgSetName{}, "nameservice/SetName", nil)
cdc.RegisterConcrete(MsgBuyName{}, "nameservice/BuyName", nil)
}
接下来需要为你的模块定义 CLI 交互了。
Cosmos SDK使用cobra
库进行CLI交互。该库使每个模块都可以轻松地显示自己的操作命令。要开始定义用户与模块的CLI交互,请创建以下文件:
./x/nameservice/client/cli/query.go
./x/nameservice/client/cli/tx.go
./x/nameservice/client/module_client.go
## Queries
在query.go
文件中为你模块的每个Query
(resolve
和whois
)定义cobra.Command
:
package cli
import (
"fmt"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/sdk-application-tutorial/x/nameservice"
"github.com/spf13/cobra"
)
// GetCmdResolveName queries information about a name
func GetCmdResolveName(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "resolve [name]",
Short: "resolve name",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
name := args[0]
res, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/resolve/%s", queryRoute, name), nil)
if err != nil {
fmt.Printf("could not resolve name - %s \n", string(name))
return nil
}
var out nameservice.QueryResResolve
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdWhois queries information about a domain
func GetCmdWhois(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "whois [name]",
Short: "Query whois info of name",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
name := args[0]
res, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/whois/%s", queryRoute, name), nil)
if err != nil {
fmt.Printf("could not resolve whois - %s \n", string(name))
return nil
}
var out nameservice.Whois
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdNames queries a list of all names
func GetCmdNames(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "names",
Short: "names",
// Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/names", queryRoute), nil)
if err != nil {
fmt.Printf("could not get query names\n")
return nil
}
var out nameservice.QueryResNames
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
注意上述代码中:
- CLI 引入了一个新的
context
:CLIContext
。它包含有关CLI交互所需的用户输入和应用程序配置的数据。 cliCtx.QueryWithData()
函数所需的path
直接从你的查询路径中映射。- 路径的第一部分用于区分 SDK 应用程序可能的querier类型:
custom
用于Querier
- 第二部分(
nameservice
)是将查询路由到的模块的名称。 - 最后是要调用模块中的特定的querier。
- 在这个例子中,第四部分是查询。这是因为查询参数是一个简单的字符串。要启用更复杂的查询输入,你需要使用
.QueryWithData()
函数的第二个参数来传入data
。有关此示例,请参阅 Staking 模块中的 queriers。
- 路径的第一部分用于区分 SDK 应用程序可能的querier类型:
Transaction
现在已经定义了查询交互,是时候继续在tx.go
中的交易生成了:
你的应用程序需要导入你刚编写的代码。这里导入路径设置为此存储库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果您是在自己的仓库中进行的前面的操作,则需要更改导入路径(github.com/{.Username}/{.Project.Repo}/x/nameservice)。
package cli
import (
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/utils"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/sdk-application-tutorial/x/nameservice"
sdk "github.com/cosmos/cosmos-sdk/types"
authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder"
)
// GetCmdBuyName is the CLI command for sending a BuyName transaction
func GetCmdBuyName(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "buy-name [name] [amount]",
Short: "bid for existing name or claim new name",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)
txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
if err := cliCtx.EnsureAccountExists(); err != nil {
return err
}
coins, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
msg := nameservice.NewMsgBuyName(args[0], coins, cliCtx.GetFromAddress())
err = msg.ValidateBasic()
if err != nil {
return err
}
cliCtx.PrintResponse = true
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false)
},
}
}
// GetCmdSetName is the CLI command for sending a SetName transaction
func GetCmdSetName(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "set-name [name] [value]",
Short: "set the value associated with a name that you own",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)
txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
if err := cliCtx.EnsureAccountExists(); err != nil {
return err
}
msg := nameservice.NewMsgSetName(args[0], args[1], cliCtx.GetFromAddress())
err := msg.ValidateBasic()
if err != nil {
return err
}
cliCtx.PrintResponse = true
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false)
},
}
}
注意在上述代码中:
- 使用了
authcmd
包。查看文档了解更多使用信息。它提供对CLI控制的帐户的访问权限,并便于签名。
Module Client
导出此功能的最后一部分称为ModuleClient
,在./x/nameservice/client/module_client.go
文件中实现。Module Client 为模块提供了导出客户端功能的标准方法。
注意:你的应用程序需要导入你刚编写的代码。这里导入路径设置为此仓库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果你是在自己项目中编写的,则需要更改导入路径成(github.com/{.Username}/ {.Project.Repo}/x/nameservice)。
package client
import (
"github.com/cosmos/cosmos-sdk/client"
nameservicecmd "github.com/cosmos/sdk-application-tutorial/x/nameservice/client/cli"
"github.com/spf13/cobra"
amino "github.com/tendermint/go-amino"
)
// ModuleClient exports all client functionality from this module
type ModuleClient struct {
storeKey string
cdc *amino.Codec
}
func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient {
return ModuleClient{storeKey, cdc}
}
// GetQueryCmd returns the cli query commands for this module
func (mc ModuleClient) GetQueryCmd() *cobra.Command {
// Group nameservice queries under a subcommand
namesvcQueryCmd := &cobra.Command{
Use: "nameservice",
Short: "Querying commands for the nameservice module",
}
namesvcQueryCmd.AddCommand(client.GetCommands(
nameservicecmd.GetCmdResolveName(mc.storeKey, mc.cdc),
nameservicecmd.GetCmdWhois(mc.storeKey, mc.cdc),
)...)
return namesvcQueryCmd
}
// GetTxCmd returns the transaction commands for this module
func (mc ModuleClient) GetTxCmd() *cobra.Command {
namesvcTxCmd := &cobra.Command{
Use: "nameservice",
Short: "Nameservice transactions subcommands",
}
namesvcTxCmd.AddCommand(client.PostCommands(
nameservicecmd.GetCmdBuyName(mc.cdc),
nameservicecmd.GetCmdSetName(mc.cdc),
)...)
return namesvcTxCmd
}
上述代码要注意:
- 此抽象允许客户端以标准方式从模块导入客户端功能。当我们构建入口时,你将看到这一点。
- 有一个未解决的问题是将其余功能(在本教程的下一部分中描述)添加到此接口。
用cosmos构建自己的区块链——引入你的模块并完成程序
经过前面的编写我们的模块已就绪,它可以和其它两个模块auth
和bank
被合并到./app.go
文件中:
你的应用程序需要导入你刚编写的代码。这里导入路径设置为此存储库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果您是在自己的仓库中进行的前面的操作,则需要更改导入路径(github.com/{.Username}/{.Project.Repo}/x/nameservice)。
package app
import (
"encoding/json"
"github.com/tendermint/tendermint/libs/log"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/sdk-application-tutorial/x/nameservice"
bam "github.com/cosmos/cosmos-sdk/baseapp"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
cmn "github.com/tendermint/tendermint/libs/common"
dbm "github.com/tendermint/tm-db"
tmtypes "github.com/tendermint/tendermint/types"
)
接下来,你需要在nameServiceApp
结构体中添加存储的key和Keepers
,并更新构造函数:
const (
appName = "nameservice"
)
type nameServiceApp struct {
*bam.BaseApp
cdc *codec.Codec
keyMain *sdk.KVStoreKey
keyAccount *sdk.KVStoreKey
keyNS *sdk.KVStoreKey
keyFeeCollection *sdk.KVStoreKey
keyParams *sdk.KVStoreKey
tkeyParams *sdk.TransientStoreKey
accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
feeCollectionKeeper auth.FeeCollectionKeeper
paramsKeeper params.Keeper
nsKeeper nameservice.Keeper
}
// NewNameServiceApp is a constructor function for nameServiceApp
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {
// First define the top level codec that will be shared by the different modules
cdc := MakeCodec()
// BaseApp handles interactions with Tendermint through the ABCI protocol
bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))
// Here you initialize your application with the store keys it requires
var app = &nameServiceApp{
BaseApp: bApp,
cdc: cdc,
keyMain: sdk.NewKVStoreKey("main"),
keyAccount: sdk.NewKVStoreKey("acc"),
keyNS: sdk.NewKVStoreKey("ns"),
keyFeeCollection: sdk.NewKVStoreKey("fee_collection"),
keyParams: sdk.NewKVStoreKey("params"),
tkeyParams: sdk.NewTransientStoreKey("transient_params"),
}
return app
}
此时,构造函数仍然缺少重要的逻辑。它需要:
- 从每个所需模块中实例化所需的
Keeper
。 - 生成每个
Keeper
所需的storeKey
。 - 注册每个模块的
handler
。baseapp
的路由器
的AddRoute()
方法用来做这个。 - 注册每个模块的
querier
。baseapp
的queryRouter
中的AddRoute()
方法用来做这个。 - 将
KVStores
挂载到baseApp
的multistore提供的key值。 - 设置
initChainer
来定义初始应用程序状态。
你最终的构造函数应该如下所示:
// NewNameServiceApp is a constructor function for nameServiceApp
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {
// First define the top level codec that will be shared by the different modules
cdc := MakeCodec()
// BaseApp handles interactions with Tendermint through the ABCI protocol
bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))
// Here you initialize your application with the store keys it requires
var app = &nameServiceApp{
BaseApp: bApp,
cdc: cdc,
keyMain: sdk.NewKVStoreKey("main"),
keyAccount: sdk.NewKVStoreKey("acc"),
keyNS: sdk.NewKVStoreKey("ns"),
keyFeeCollection: sdk.NewKVStoreKey("fee_collection"),
keyParams: sdk.NewKVStoreKey("params"),
tkeyParams: sdk.NewTransientStoreKey("transient_params"),
}
// The ParamsKeeper handles parameter storage for the application
app.paramsKeeper = params.NewKeeper(app.cdc, app.keyParams, app.tkeyParams)
// The AccountKeeper handles address -> account lookups
app.accountKeeper = auth.NewAccountKeeper(
app.cdc,
app.keyAccount,
app.paramsKeeper.Subspace(auth.DefaultParamspace),
auth.ProtoBaseAccount,
)
// The BankKeeper allows you perform sdk.Coins interactions
app.bankKeeper = bank.NewBaseKeeper(
app.accountKeeper,
app.paramsKeeper.Subspace(bank.DefaultParamspace),
bank.DefaultCodespace,
)
// The FeeCollectionKeeper collects transaction fees and renders them to the fee distribution module
app.feeCollectionKeeper = auth.NewFeeCollectionKeeper(cdc, app.keyFeeCollection)
// The NameserviceKeeper is the Keeper from the module for this tutorial
// It handles interactions with the namestore
app.nsKeeper = nameservice.NewKeeper(
app.bankKeeper,
app.keyNS,
app.cdc,
)
// The AnteHandler handles signature verification and transaction pre-processing
app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.feeCollectionKeeper))
// The app.Router is the main transaction router where each module registers its routes
// Register the bank and nameservice routes here
app.Router().
AddRoute("bank", bank.NewHandler(app.bankKeeper)).
AddRoute("nameservice", nameservice.NewHandler(app.nsKeeper))
// The app.QueryRouter is the main query router where each module registers its routes
app.QueryRouter().
AddRoute("nameservice", nameservice.NewQuerier(app.nsKeeper)).
AddRoute("acc", auth.NewQuerier(app.accountKeeper))
// The initChainer handles translating the genesis.json file into initial state for the network
app.SetInitChainer(app.initChainer)
app.MountStores(
app.keyMain,
app.keyAccount,
app.keyNS,
app.keyFeeCollection,
app.keyParams,
app.tkeyParams,
)
err := app.LoadLatestVersion(app.keyMain)
if err != nil {
cmn.Exit(err.Error())
}
return app
}
注意:上面提到的 TransientStore 是 KVStore 的内存实现,用于未持久化的状态。
initChainer
定义了genesis.json
中的帐户如何在初始化区块链时被映射到应用程序状态。ExportAppStateAndValidators
函数可帮助引导初始化应用程序的状态。你现在不需要太关心它们。
构造函数注册了initChainer
函数,但尚未定义。继续创建它:
// GenesisState represents chain state at the start of the chain. Any initial state (account balances) are stored here.
type GenesisState struct {
AuthData auth.GenesisState `json:"auth"`
BankData bank.GenesisState `json:"bank"`
Accounts []*auth.BaseAccount `json:"accounts"`
}
func (app *nameServiceApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
stateJSON := req.AppStateBytes
genesisState := new(GenesisState)
err := app.cdc.UnmarshalJSON(stateJSON, genesisState)
if err != nil {
panic(err)
}
for _, acc := range genesisState.Accounts {
acc.AccountNumber = app.accountKeeper.GetNextAccountNumber(ctx)
app.accountKeeper.SetAccount(ctx, acc)
}
auth.InitGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper, genesisState.AuthData)
bank.InitGenesis(ctx, app.bankKeeper, genesisState.BankData)
return abci.ResponseInitChain{}
}
// ExportAppStateAndValidators does the things
func (app *nameServiceApp) ExportAppStateAndValidators() (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) {
ctx := app.NewContext(true, abci.Header{})
accounts := []*auth.BaseAccount{}
appendAccountsFn := func(acc auth.Account) bool {
account := &auth.BaseAccount{
Address: acc.GetAddress(),
Coins: acc.GetCoins(),
}
accounts = append(accounts, account)
return false
}
app.accountKeeper.IterateAccounts(ctx, appendAccountsFn)
genState := GenesisState{
Accounts: accounts,
AuthData: auth.DefaultGenesisState(),
BankData: bank.DefaultGenesisState(),
}
appState, err = codec.MarshalJSONIndent(app.cdc, genState)
if err != nil {
return nil, nil, err
}
return appState, validators, err
}
最后添加一个辅助函数来生成一个animo–*codec.Codec
,它可以正确地注册你应用程序中使用的所有模块:
// MakeCodec generates the necessary codecs for Amino
func MakeCodec() *codec.Codec {
var cdc = codec.New()
auth.RegisterCodec(cdc)
bank.RegisterCodec(cdc)
nameservice.RegisterCodec(cdc)
staking.RegisterCodec(cdc)
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
return cdc
}