10 分钟构建自己的区块链|乌托邦周报 #21

无环图
2023-06-08 13:02
发布于 Mirror

从早期的 ICO,到最近的 BRC-20、BRC-721,“人人发币”早已算不上什么新鲜事。

与其琢磨给自己的币种起名,设置流通量和规则,幻想有朝一日币价翻天,倒不如花 10 分钟,发一条完完整整的 Layer 1 区块链来得实在和酷。

作为《Tendermint 导读》的续篇,废话不多说,这次我们就来探探,Cosmos SDK 是如何带领大家走进“人人发链”时代的。

本文要点:

  • Cosmos SDK 架构概览

  • 手把手写一个类 ENS 区块链


Cosmos SDK 架构概览

Cosmos SDK 是基于 Tendermint(现 CometBFT)共识和网络层构建的区块链模块化开发框架。

来源:https://v1.cosmos.network/intro

回到《Tendermint 导读》中的第一张图,我们可以看到 Cosmos SDK 封装了很多模块,你最可能关心的包括:

  • Staking:负责管理质押和验证者,理解为 ETH 2.0 的 Staking 就好了。

  • Slashing:负责判别、处理验证者的违规行为,包括离线和 Double Sign 等。

  • Mint:负责定义区块奖励(Block Reward)逻辑,包括隔多久出块,每出一个区块奖励验证者多少 Token。

  • Bank:发币,可理解为 ERC-20 那套逻辑。

  • NFT:发币,但发的是 NFT,可理解为 ERC-721 那套逻辑。

  • Cosmwasm:智能合约能力,可以用 Rust 写。

  • Gov:负责定义治理逻辑,包括如何提交 Proposal、投票等。

  • IBC:跨链通信,在《Into the Cosmoverse》中有简单介绍。

因为这些模块都是开箱即用的,所以如果你对其中一些逻辑没有特别的要求,完全可以不作调整,只使用想要自定义逻辑的模块即可。

虽然 Cosmos SDK 大大降低了 Layer 1 的开发门槛,但笔者还是不鼓励生啃 Cosmos SDK 文档(勇士除外)。虽然文档内容相比几年前已大为进步,但行文依然十分艰涩难懂。事无巨细一一交代,结果就是读者很容易迷失。

要快速入门,其实看文档中的这张图就好了。

来源:https://docs.cosmos.network/v0.47/intro/sdk-design

整个 Cosmos SDK 有 2 层分发(这种模式在 Tendermint 中也有出现):

  • 第 1 层:Cosmos SDK 负责处理从 CometBFT 获取的 Tx,将其解析为 Message,然后将它分发到对应的模块处理。

  • 第 2 层:模块收到 Message 后,根据 Message 类型,再分发到对应的方法处理。

其中,到了第 2 层,对应的处理方法里做的主要就是根据 Message 内容,来执行 State Transition,最终将更新后的 State 储存下来。

Cosmos SDK 的 State 储存也很简单,本质上就是一个 KV 存储。给定一个 Key,你就能读取和储存相应的信息。至于你是想存一个数组,一个映射,还是单纯一个值,全靠在 Key 上做文章。

上面说的是 Tx,即涉及 State Transition 的操作,需要广播,消耗 Gas,所有节点执行;还有一类操作叫 Query,Query 是纯粹的查询,不会修改 State,不消耗 Gas,只要一个全节点执行就好了。Query 类似于在 Etherscan 上你可以根据合约提供的 view 方法,提供一个输入,直接得到一个结果输出。

一个 Cosmos SDK 链的全节点会通过 RPC 的形式,暴露 Tx 和 Query 的接口。事实上,写好 Tx 和 Query 处理方法后,你可以遵循 Cosmos SDK 中提供的脚手架,来生成对应的 CLI,调用你的区块链提供的能力。


手把手写一个类 ENS 区块链

如果上面说的还有点抽象,没关系,我们举一个实际的例子。

这里我们打算写一个幼儿园版的 ENS(Ethereum Name Service),叫做 CNS(Cosmos Name Service)。为了说明 Cosmos SDK 的使用方法,我们会简化业务逻辑,不会照搬 ENS 的功能,也不会像 ENS 那样用 NFT。

具体功能如下:

  • 注册域名(RegisterDomain),域名指向所有者的地址,在注册期限内有效;

  • 出售域名(ListForSale),采用挂牌价的方式;

  • 停止出售域名(UnlistForSale);

  • 购买域名(BuyDomain)。

这里我们不支持域名续期和更全面的解析类型,因而整个逻辑骨架非常清晰。

下面我们就一步步实现这些功能需求。

安装 Cosmos SDK(1/6)

直接从 GitHub 上克隆下来就好。

本身目录非常多,我们只需关心以下几个就好:

  • /proto:各种结构体定义放这里。

  • /simapp:各种模块初始化,看 app.go。

  • /x:所有模块都在这里,我们要写的 CNS 模块也会放在这里。

定义结构体(2/6)

要定义的结构体包含 3 类:

  • State 结构体

  • Tx Message 涉及的请求和返回值结构

  • Query 涉及的请求和返回值结构

首先是 State 结构体,这里我们直接定义一个 Domain 就够用了。

// /proto/cns/cns/domain.proto
syntax = "proto3";
package cns.cns;

option go_package = "cns/x/cns/types";

message Domain {
  string name = 1; // 域名本名,如 ag.cns。
  string owner = 2; // 所有者地址
  uint64 validThru = 3; // 有效期至哪个区块高度
  bool isForSale = 4; // 是否出售
  uint64 price = 5; // 挂牌价
}

然后是 Tx Message 涉及的请求和返回值结构(简单起见,返回值都为空)。

// /proto/cns/cns/tx.proto
syntax = "proto3";

package cns.cns;

import "cns/cns/domain.proto";

option go_package = "cns/x/cns/types";

service Msg {
  rpc RegisterDomain (MsgRegisterDomain) returns (MsgRegisterDomainResponse);
  rpc ListForSale    (MsgListForSale   ) returns (MsgListForSaleResponse   );
  rpc UnlistForSale  (MsgUnlistForSale ) returns (MsgUnlistForSaleResponse );
  rpc BuyDomain      (MsgBuyDomain     ) returns (MsgBuyDomainResponse     );
}

message MsgRegisterDomain {
  string creator = 1; // Tx 的创建方地址,与 Domain 实体本身无关。
  string name    = 2; // 域名本名,如 ag.cns。
  uint64 years   = 3; // 买多少年
}

message MsgRegisterDomainResponse {}

message MsgListForSale {
  string creator = 1; // Tx 的创建方地址,与 Domain 实体本身无关。
  string name    = 2; // 域名本名,如 ag.cns。
  uint64 price   = 3; // 挂牌价
}

message MsgListForSaleResponse {}

message MsgUnlistForSale {
  string creator = 1; // Tx 的创建方地址,与 Domain 实体本身无关。
  string name    = 2; // 域名本名,如 ag.cns。
}

message MsgUnlistForSaleResponse {}

message MsgBuyDomain {
  string creator = 1; // Tx 的创建方地址,与 Domain 实体本身无关。
  string name    = 2; // 域名本名,如 ag.cns。
}

message MsgBuyDomainResponse {}

最后是 Query 涉及的请求和返回值结构。这里支持查单个域名和列举全部域名。

// /proto/cns/cns/query.proto
syntax = "proto3";

package cns.cns;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "cns/cns/params.proto";
import "cns/cns/domain.proto";

option go_package = "cns/x/cns/types";

service Query {
  rpc Params (QueryParamsRequest) returns (QueryParamsResponse) {
    option (google.api.http).get = "/cns/cns/params";
  
  }

  rpc Domain (QueryGetDomainRequest) returns (QueryGetDomainResponse) {
    option (google.api.http).get = "/cns/cns/domain/{name}";
  
  }
  rpc DomainAll (QueryAllDomainRequest) returns (QueryAllDomainResponse) {
    option (google.api.http).get = "/cns/cns/domain";
  
  }
}

message QueryParamsRequest {}

message QueryParamsResponse {
  Params params = 1 [(gogoproto.nullable) = false];
}

message QueryGetDomainRequest {
  string name = 1;
}

message QueryGetDomainResponse {
  Domain domain = 1 [(gogoproto.nullable) = false];
}

message QueryAllDomainRequest {
  cosmos.base.query.v1beta1.PageRequest pagination = 1;
}

message QueryAllDomainResponse {
  repeated Domain domain = 1 [(gogoproto.nullable) = false];
           cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

代码特别多,但多是口水代码。

定义域名的 CRUD 方法(3/6)

区块链本质是 Replicated State Machine,所以无非是对 State 中的一些实体的 CRUD。

这里我们分别定义 Domain 的增删改查方法,为后面打下基础。

按照惯例,这些方法放在 Keeper 下。遵循的范式是:

  1. 获取 KV 存储对象;

  2. 对对象作操作。

以“查”为例,获取 KV 存储对象用的函数为 func NewStore(parent types.KVStore, prefix []byte) Store。其中,参数parent 为父存储对象,这里我们为 CNS 模块开辟了一个独立的存储空间(特别的storeKey),用于隔离 Cosmos SDK 自带模块的存储;prefix 为 KV 中 Key 的前缀,这里我们定义为/Domain/value/

获取存储对象后,根据域名(name)获取 Domain 对象。这里 DomainKey 做的是将namestring转为[]byte,并加一个/ 后缀,以匹配Get 函数的参数类型要求。

拿到对应的 Domain 后,这个 Domain 还只是又一串[]byte,需要用 codec 反序列化(MustUnmarshal)再返回。

// /x/cns/keeper/domain.go
package keeper

import (
    "cns/x/cns/types"
    "github.com/cosmos/cosmos-sdk/store/prefix"
    sdk "github.com/cosmos/cosmos-sdk/types"
)

// 查一个
func (k Keeper) GetDomain(
    ctx sdk.Context,
    name string,

) (val types.Domain, found bool) {
    store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.DomainKeyPrefix))

    b := store.Get(types.DomainKey(
        name,
    ))
    if b == nil {
        return val, false
    }

    k.cdc.MustUnmarshal(b, &val)
    return val, true
}

增、删、改同理,就不赘述了。

// 增、改
func (k Keeper) SetDomain(ctx sdk.Context, domain types.Domain) {
    store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.DomainKeyPrefix))
    b := k.cdc.MustMarshal(&domain)
    store.Set(types.DomainKey(
        domain.Name,
    ), b)
}

// 删
func (k Keeper) RemoveDomain(
    ctx sdk.Context,
    name string,

) {
    store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.DomainKeyPrefix))
    store.Delete(types.DomainKey(
        name,
    ))
}

定义 Tx Message 的处理方法(4/6)

CRUD 方法都有了,Tx Message 的处理方法就顺理成章了。

以注册域名(RegisterDomain)为例,我们先GetDomain 看看有没有注册过,有注册过的话看看注册有没有过期,如果有注册过且没过期就不让注册,否则就按照约定的价格(这里是 100 token 一年),为 Tx 的创建方注册上这个域名。

// /x/cns/keeper/msg_server_domain_sales.go
package keeper

import (
    "context"

    "cns/x/cns/types"

    sdk "github.com/cosmos/cosmos-sdk/types"
    sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

const (
    blocksPerYear = 60 * 60 * 8766 / 5 // mintGenesis.Params.BlocksPerYear
    pricePerYear  = 100
)

func (k msgServer) RegisterDomain(goCtx context.Context, msg *types.MsgRegisterDomain) (*types.MsgRegisterDomainResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    valFound, isFound := k.GetDomain(
        ctx,
        msg.Name,
    )

    if isFound && valFound.ValidThru <= uint64(ctx.BlockHeight()) {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain already taken")
    }

    price := pricePerYear * msg.Years
    from, _ := sdk.AccAddressFromBech32(msg.Creator)
    if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, from, types.ModuleName, sdk.NewCoins(sdk.NewCoin("token", sdk.NewInt(int64(price))))); err != nil {
        return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Insufficient funds.")
    }

    newDomain := types.Domain{
        Owner:     msg.Creator,
        Name:      msg.Name,
        ValidThru: uint64(ctx.BlockHeight()) + blocksPerYear*msg.Years,
        IsForSale: false,
        Price:     0,
    }

    k.SetDomain(ctx, newDomain)

    return &types.MsgRegisterDomainResponse{}, nil
}

出售域名、停止出售域名、购买域名也是类似。只不过:

  • 检查的条件稍有不同;

  • 购买域名时,直接将price对应的 token 从购买者账号转移到所有者账号中。

func (k msgServer) ListForSale(goCtx context.Context, msg *types.MsgListForSale) (*types.MsgListForSaleResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    valFound, isFound := k.GetDomain(
        ctx,
        msg.Name,
    )
    if !isFound {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain not found")
    }

    if msg.Creator != valFound.Owner {
        return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot list a domain you do not own")
    }

    valFound.IsForSale = true
    valFound.Price = msg.Price

    k.SetDomain(ctx, valFound)

    return &types.MsgListForSaleResponse{}, nil
}

func (k msgServer) UnlistForSale(goCtx context.Context, msg *types.MsgUnlistForSale) (*types.MsgUnlistForSaleResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    valFound, isFound := k.GetDomain(
        ctx,
        msg.Name,
    )
    if !isFound {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain not found")
    }

    if msg.Creator != valFound.Owner {
        return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot unlist a domain you do not own")
    }

    valFound.IsForSale = false

    k.SetDomain(ctx, valFound)

    return &types.MsgUnlistForSaleResponse{}, nil
}

func (k msgServer) BuyDomain(goCtx context.Context, msg *types.MsgBuyDomain) (*types.MsgBuyDomainResponse, error) {
    ctx := sdk.UnwrapSDKContext(goCtx)

    valFound, isFound := k.GetDomain(
        ctx,
        msg.Name,
    )

    if !isFound {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain not found")
    }

    if valFound.ValidThru < uint64(ctx.BlockHeight()) {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain exists but expired. Try RegisterDomain.")
    }

    if !valFound.IsForSale {
        return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, "Domain not for sale")
    }

    if msg.Creator == valFound.Owner {
        return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot buy your own domain")
    }
    from, _ := sdk.AccAddressFromBech32(msg.Creator)
    to, _ := sdk.AccAddressFromBech32(valFound.Owner)
    if err := k.bankKeeper.SendCoins(ctx, from, to, sdk.NewCoins(sdk.NewCoin("token", sdk.NewInt(int64(valFound.Price))))); err != nil {
        return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Insufficient funds")
    }

    valFound.Owner = msg.Creator
    valFound.IsForSale = false
    k.SetDomain(ctx, valFound)

    return &types.MsgBuyDomainResponse{}, nil
}

定义 Query 的处理方法(5/6)

Query 包括查一个域名和列举全部域名。知识点上面都有涉及,这里就不赘述了。

// /x/cns/keeper/query_domain.go
package keeper

import (
    "context"

    "cns/x/cns/types"
    "github.com/cosmos/cosmos-sdk/store/prefix"
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/types/query"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// 查一个
func (k Keeper) Domain(goCtx context.Context, req *types.QueryGetDomainRequest) (*types.QueryGetDomainResponse, error) {
    if req == nil {
        return nil, status.Error(codes.InvalidArgument, "Invalid request")
    }
    ctx := sdk.UnwrapSDKContext(goCtx)

    val, found := k.GetDomain(
        ctx,
        req.Name,
    )
    if !found {
        return nil, status.Error(codes.NotFound, "Domain not found")
    }

    return &types.QueryGetDomainResponse{Domain: val}, nil
}

// 列举全部
func (k Keeper) DomainAll(goCtx context.Context, req *types.QueryAllDomainRequest) (*types.QueryAllDomainResponse, error) {
    if req == nil {
        return nil, status.Error(codes.InvalidArgument, "Invalid request")
    }

    var domains []types.Domain
    ctx := sdk.UnwrapSDKContext(goCtx)

    store := ctx.KVStore(k.storeKey)
    domainStore := prefix.NewStore(store, types.KeyPrefix(types.DomainKeyPrefix))

    pageRes, err := query.Paginate(domainStore, req.Pagination, func(key []byte, value []byte) error {
        var domain types.Domain
        if err := k.cdc.Unmarshal(value, &domain); err != nil {
            return err
        }

        domains = append(domains, domain)
        return nil
    })

    if err != nil {
        return nil, status.Error(codes.Internal, err.Error())
    }

    return &types.QueryAllDomainResponse{Domain: domains, Pagination: pageRes}, nil
}

更新 CLI

万事俱备,只差告诉 CLI 如何调用我们上面的 Tx 和 Query 能力了。

同样是口水代码,照抄一下自带模块的,改改就好了。

首先是 Tx 的 CLI 代码,这里以注册域名(RegisterDomain)为例:

// /x/cns/client/cli/tx_register_domain.go
package cli

import (
    "cns/x/cns/types"
    "github.com/cosmos/cosmos-sdk/client"
    "github.com/cosmos/cosmos-sdk/client/flags"
    "github.com/cosmos/cosmos-sdk/client/tx"
    "github.com/spf13/cast"
    "github.com/spf13/cobra"
)

func CmdRegisterDomain() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "register-domain [name] [years]",
        Short: "Broadcast message RegisterDomain",
        Args:  cobra.ExactArgs(2),
        RunE: func(cmd *cobra.Command, args []string) (err error) {
            argName := args[0]
            argYears, err := cast.ToUint64E(args[1])
            if err != nil {
                return err
            }

            clientCtx, err := client.GetClientTxContext(cmd)
            if err != nil {
                return err
            }

            msg := types.NewMsgRegisterDomain(
                clientCtx.GetFromAddress().String(),
                argName,
                argYears,
            )
            if err := msg.ValidateBasic(); err != nil {
                return err
            }
            return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
        },
    }

    flags.AddTxFlagsToCmd(cmd)

    return cmd
}

搞定后注册到 Tx 子命令下:

// /x/cns/client/cli/tx.go
package cli

import (
    "fmt"
    "time"

    "github.com/spf13/cobra"

    "github.com/cosmos/cosmos-sdk/client"
    "cns/x/cns/types"
)

var (
    DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds())
)

const (
    flagPacketTimeoutTimestamp = "packet-timeout-timestamp"
    listSeparator              = ","
)

func GetTxCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:                        types.ModuleName,
        Short:                      fmt.Sprintf("%s transactions subcommands", types.ModuleName),
        DisableFlagParsing:         true,
        SuggestionsMinimumDistance: 2,
        RunE:                       client.ValidateCmd,
    }

    cmd.AddCommand(CmdRegisterDomain())
    cmd.AddCommand(CmdListForSale())
    cmd.AddCommand(CmdUnlistForSale())
    cmd.AddCommand(CmdBuyDomain())

    return cmd
}

然后是 Query 对应的 CLI 代码:

// /x/cns/client/cli/query_domain.go
package cli

import (
    "context"

    "cns/x/cns/types"
    "github.com/cosmos/cosmos-sdk/client"
    "github.com/cosmos/cosmos-sdk/client/flags"
    "github.com/spf13/cobra"
)

func CmdShowDomain() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "show-domain [name]",
        Short: "shows a domain",
        Args:  cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) (err error) {
            clientCtx := client.GetClientContextFromCmd(cmd)

            queryClient := types.NewQueryClient(clientCtx)

            argName := args[0]

            params := &types.QueryGetDomainRequest{
                Name: argName,
            }

            res, err := queryClient.Domain(context.Background(), params)
            if err != nil {
                return err
            }

            return clientCtx.PrintProto(res)
        },
    }

    flags.AddQueryFlagsToCmd(cmd)

    return cmd
}

func CmdListDomain() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "list-domain",
        Short: "list all domain",
        RunE: func(cmd *cobra.Command, args []string) error {
            clientCtx := client.GetClientContextFromCmd(cmd)

            pageReq, err := client.ReadPageRequest(cmd.Flags())
            if err != nil {
                return err
            }

            queryClient := types.NewQueryClient(clientCtx)

            params := &types.QueryAllDomainRequest{
                Pagination: pageReq,
            }

            res, err := queryClient.DomainAll(context.Background(), params)
            if err != nil {
                return err
            }

            return clientCtx.PrintProto(res)
        },
    }

    flags.AddPaginationFlagsToCmd(cmd, cmd.Use)
    flags.AddQueryFlagsToCmd(cmd)

    return cmd
}

同样,搞定后要注册到 Query 子命令下:

package cli

import (
    "fmt"

    "github.com/spf13/cobra"

    "github.com/cosmos/cosmos-sdk/client"

    "cns/x/cns/types"
)

func GetQueryCmd(queryRoute string) *cobra.Command {
    cmd := &cobra.Command{
        Use:                        types.ModuleName,
        Short:                      fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
        DisableFlagParsing:         true,
        SuggestionsMinimumDistance: 2,
        RunE:                       client.ValidateCmd,
    }

    cmd.AddCommand(CmdQueryParams())
    cmd.AddCommand(CmdListDomain())
    cmd.AddCommand(CmdShowDomain())

    return cmd
}

验收一下

编译后,我们得到一个我们 Cosmos Name Service 区块链专属的 CLI,叫做 cnsd。下面我们来测试下功能。

首先,我们看到 Alice 账号下一开始有 20,000 token(在 genesis 文件中配置):

cnsd query bank balances $(cnsd keys show alice -a)
balances:
- amount: "100000000"
  denom: stake
- amount: "20000"
  denom: token
pagination:
  next_key: null
  total: "0"

(大家可以留意一下 Query 和 Tx 命令的构成,比如后面都紧接着模块,再后面跟着 Query 的东西或 Tx Message 的类型,最后面是参数。)

然后,Alice 注册了 2 年的 ag.cns 域名:

cnsd tx cns register-domain ag.cns 2 --from alice
auth_info:
  fee:
    amount: []
    gas_limit: "200000"
    granter: ""
    payer: ""
  signer_infos: []
  tip: null
body:
  extension_options: []
  memo: ""
  messages:
  - '@type': /cns.cns.MsgRegisterDomain
    creator: cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5
    name: ag.cns
    years: "2"
  non_critical_extension_options: []
  timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
...
raw_log: '[{"msg_index":0,"events":[{"type":"coin_received","attributes":[{"key":"receiver","value":"cosmos1sfa9p0xmn9685r5r8mundaw96r0qhd9p2akk39"},{"key":"amount","value":"200token"}]},{"type":"coin_spent","attributes":[{"key":"spender","value":"cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5"},{"key":"amount","value":"200token"}]},{"type":"message","attributes":[{"key":"action","value":"/cns.cns.MsgRegisterDomain"},{"key":"sender","value":"cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5"}]},{"type":"transfer","attributes":[{"key":"recipient","value":"cosmos1sfa9p0xmn9685r5r8mundaw96r0qhd9p2akk39"},{"key":"sender","value":"cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5"},{"key":"amount","value":"200token"}]}]}]'
timestamp: ""
tx: null
txhash: 0A1C1AD27E3EED5610D957D904B886FF31B300FA0E674BD21F73ECC1833AFF22

不出所料,Alice 花了 200 Token:

cnsd query bank balances $(cnsd keys show alice -a)
balances:
- amount: "100000000"
  denom: stake
- amount: "19800"
  denom: token
pagination:
  next_key: null
  total: "0"

并且 Alice 确认自己抢注成功:

cnsd query cns show-domain ag.cns
domain:
  isForSale: false
  name: ag.cns
  owner: cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5
  price: "0"
  validThru: "12625268"

Alice 觉得这个域名会很火,打算挂 500 Token 出售:

cnsd tx cns list-for-sale ag.cns 500 --from alice
auth_info:
  fee:
    amount: []
    gas_limit: "200000"
    granter: ""
    payer: ""
  signer_infos: []
  tip: null
body:
  extension_options: []
  memo: ""
  messages:
  - '@type': /cns.cns.MsgListForSale
    creator: cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5
    name: ag.cns
    price: "500"
  non_critical_extension_options: []
  timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
...
raw_log: '[{"msg_index":0,"events":[{"type":"message","attributes":[{"key":"action","value":"/cns.cns.MsgListForSale"}]}]}]'
timestamp: ""
tx: null
txhash: F7AD84CD9E72AAD2819014E86F5FAB284B55FBE6D8B5C8BB4FC28087BB421702

Alice 确认 ag.cns 处于在售状态:

cnsd query cns show-domain ag.cns                
domain:
  isForSale: true
  name: ag.cns
  owner: cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5
  price: "500"
  validThru: "12625268"

Bob 看到了这个域名在售,兴高采烈来接盘:

cnsd tx cns buy-domain ag.cns --from bob   
auth_info:
  fee:
    amount: []
    gas_limit: "200000"
    granter: ""
    payer: ""
  signer_infos: []
  tip: null
body:
  extension_options: []
  memo: ""
  messages:
  - '@type': /cns.cns.MsgBuyDomain
    creator: cosmos1yrxya30r2yknyfdm29hg7d75dys85p6dz39m6f
    name: ag.cns
  non_critical_extension_options: []
  timeout_height: "0"
signatures: []
confirm transaction before signing and broadcasting [y/N]: y
...
raw_log: '[{"msg_index":0,"events":[{"type":"coin_received","attributes":[{"key":"receiver","value":"cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5"},{"key":"amount","value":"500token"}]},{"type":"coin_spent","attributes":[{"key":"spender","value":"cosmos1yrxya30r2yknyfdm29hg7d75dys85p6dz39m6f"},{"key":"amount","value":"500token"}]},{"type":"message","attributes":[{"key":"action","value":"/cns.cns.MsgBuyDomain"},{"key":"sender","value":"cosmos1yrxya30r2yknyfdm29hg7d75dys85p6dz39m6f"}]},{"type":"transfer","attributes":[{"key":"recipient","value":"cosmos1270k40m4rdd60huuf8uhyllql8wh3g6zppcwp5"},{"key":"sender","value":"cosmos1yrxya30r2yknyfdm29hg7d75dys85p6dz39m6f"},{"key":"amount","value":"500token"}]}]}]'
timestamp: ""
tx: null
txhash: F9ACC35F130B49EE6AE7A6CC505BA79D4B0F76398582AA4BE5D2242B0367F2AE

Bob 确认自己买到了这个域名:

cnsd query cns show-domain ag.cns       
domain:
  isForSale: false
  name: ag.cns
  owner: cosmos1yrxya30r2yknyfdm29hg7d75dys85p6dz39m6f
  price: "500"
  validThru: "12625268"

同样不出所料,Alice 进账 500 Token,Bob 花了 300 Token(一开始有 9,000 Token):

cnsd query bank balances $(cnsd keys show alice -a)
balances:
- amount: "100000000"
  denom: stake
- amount: "20300"
  denom: token
pagination:
  next_key: null
  total: "0"
cnsd query bank balances $(cnsd keys show bob -a) 
balances:
- amount: "100000000"
  denom: stake
- amount: "8700"
  denom: token
pagination:
  next_key: null
  total: "0"

以上便是用 Cosmos SDK 写 Layer 1 区块链的全过程。很多口水代码,实际关键代码并不多。为了提效,大家可以用 Ignite 来生成脚手架。

当然,演示用的例子可能还不是很过瘾,不过之后要扩展的话,无非是组合调用各个自带模块的能力罢了,思路大同小异。

后面我们将深入到智能合约等其他普遍应用的模块中去,了解这些模块的设计思路与应用方法,通过更多案例来一步步完善我们关于 Layer 1 的知识结构。

0
粉丝
0
获赞
27
精选
数据来源区块链,不构成投资建议!
网站只展示作者的精选文章
2022 Tagge. With ❤️ from Lambda