毕设08 - Cosmos 构建自己的区块链

| 笔记 | 12447 | 32分钟 | 毕设Cosmos区块链Go

根据 https://docs.ignite.com/guide/getting-started 中的教程,实现一个 Cosmos 区块链的搭建。

使用 ignite 提供的自动化构建脚本,实现 Cosmos 区块链的搭建和模块设计。

内容不全面,详细的看链接网站。

但是因为 ignite 封装太多了,没完全明白如何继承模块。

本文为摸索过程中的记录,不适合学习。请看毕设10,比较适合参考学习。

前置准备

  • 安装 go (1.21+)

    https://ubuntuhandbook.org/index.php/2024/02/how-to-install-go-golang-1-22-in-ubuntu-22-04/#google_vignette

    wget -c https://go.dev/dl/go1.23.1.linux-amd64.tar.gz
    tar -C /usr/local/ -xzf go1.23.1.linux-amd64.tar.gz

    ~/.bashrc 中添加:

    if [ -d "/usr/local/go/bin" ] ; then
        PATH="/usr/local/go/bin:$PATH"
    fi

    重新载入 ~/.bashrc

    source ~/.bashrc

    设置中国代理:

    go env -w GO111MODULE=on
    go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
  • 安装 Ignite CLI

    sudo snap set system proxy.http="http://192.168.3.56:21882"
    sudo snap set system proxy.https="http://192.168.3.56:21882"
    sudo systemctl restart snapd
    snap install ignite --classic

1 创建一个新的区块链

要使用 Ignite 创建一个新的区块链项目,需要运行以下命令:

ignite scaffold chain example

在国内,安装过程中可能会有很多报错,都是代理的问题。安装成功之后输出如下:

[root@cishoon-virtual-machine:~/go]# ignite scaffold chain example

⭐️ Successfully created a new blockchain 'example'.
👉 Get started with the following commands:

 % cd example
 % ignite chain serve

Documentation: https://docs.ignite.com

新目录 example 中创建一个新的基于 Cosmos SDK 的区块链。默认导入了多个标准模块:

  • staking:用于实现 PoS 共识机制。
  • bank:用于实现代币的转账。
  • gov:用于链上治理。

1.1 目录结构

  • app/ 目录:包含连接区块链各部分的文件。该目录中最重要的文件是 app.go,它包含区块链的类型定义以及创建和初始化区块链的函数。此文件负责将区块链的各个组件连接在一起,并定义它们之间的交互方式。
  • cmd/ 目录:包含负责已编译二进制文件的命令行接口(CLI)的主程序包。此程序包定义了可以通过 CLI 运行的命令以及它们的执行方式。它是区块链项目的重要组成部分,为开发人员和用户提供了一种与区块链交互的方式,例如查询区块链状态或发送交易。
  • proto/ 目录:包含协议缓冲区(protocol buffer)文件,用于描述区块链的数据结构。协议缓冲区是一种语言和平台无关的机制,用于序列化结构化数据,通常用于分布式系统(如区块链网络)的开发。proto/ 目录中的协议缓冲区文件定义了区块链使用的数据结构和消息,并用于生成可用于与区块链交互的各种编程语言的代码。在 Cosmos SDK 中,协议缓冲区文件用于定义可以被区块链发送和接收的特定数据类型,以及可用于访问区块链功能的特定 RPC 端点。
  • testutil/ 目录:包含用于测试的辅助函数。这些函数提供了一种方便的方式来执行编写区块链测试时所需的常见任务,例如创建测试账户、生成交易以及检查区块链状态。使用 testutil/ 目录中的辅助函数,开发人员可以更快速、高效地编写测试,并确保测试全面而有效。
  • x/ 目录:包含添加到区块链的自定义 Cosmos SDK 模块。标准的 Cosmos SDK 模块是预构建的组件,提供了 Cosmos SDK 区块链的常见功能,例如质押和治理支持。另一方面,自定义模块是专门为区块链项目开发的模块,用于提供特定的项目功能。
  • config.yml 文件:是一个配置文件,用于在开发过程中自定义区块链。该文件包含控制区块链各方面的设置,例如网络 ID、账户余额和节点参数等。

2 启用项目

执行以下命令启用区块链:

ignite chain serve

ignite chain serve 命令用于在开发模式下启动区块链节点。该命令首先使用 ignite chain build 编译并安装二进制文件,然后使用 ignite chain init 初始化单个验证人的区块链数据目录。之后,它会在本地启动节点,并启用自动代码重载功能,使代码的更改可以直接反映在运行中的区块链上,而无需重启节点。这样可以加快区块链的开发和测试进程。

3 Hello World!

在本教程中,你将使用 Ignite CLI 构建一个简单的区块链,该区块链响应一个自定义查询并返回 “Hello %s!”,其中 “%s” 是查询中传入的名字。通过此教程,你将更深入地了解如何在 Cosmos SDK 区块链中创建自定义查询

3.1 创建一个新的链

ignite scaffold chain hello
cd hello

3.2 生成查询代码

# ignite scaffold query say-hello name --response name

modify proto/hello/hello/query.proto
create x/hello/keeper/query_say_hello.go
modify x/hello/module/autocli.go

🎉 Created a query `say-hello`.

此命令会为一个新的查询 say-hello 生成代码,该查询接受 name 作为输入,并将它返回在响应中。

生成的代码

  • proto/hello/hello/query.proto:定义请求和响应结构。
  • x/hello/module/autocli.go:包含查询的 CLI 命令。
  • x/hello/keeper/query_say_hello.go:存放查询响应的逻辑。

3.2.1 proto

增加了一个查询接口:

// Queries a list of SayHello items.
rpc SayHello (QuerySayHelloRequest) returns (QuerySayHelloResponse) {
	option (google.api.http).get = "/hello/hello/say_hello/{name}";
}

message QuerySayHelloRequest {
  string name = 1;
}

message QuerySayHelloResponse {
  string name = 1;
}

3.2.2 cli

新增了一个请求接口,但是没有包括具体实现代码,应该是封装进 Cosmos SDK 中的 autocliv1 了。

Query: &autocliv1.ServiceCommandDescriptor{
    Service: modulev1.Query_ServiceDesc.ServiceName,
    RpcCommandOptions: []*autocliv1.RpcCommandOptions{
        {
            RpcMethod: "Params",
            Use:       "params",
            Short:     "Shows the parameters of the module",
        },
        {
            RpcMethod:      "SayHello",
            Use:            "say-hello [name]",
            Short:          "Query say-hello",
            PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "name"}},
        },

        // this line is used by ignite scaffolding # autocli/query
    },
},

3.2.3 keeper

新增了 query_say_hello.go 文件:

package keeper

import (
    "context"
    "fmt"

    "hello/x/hello/types"

    sdk "github.com/cosmos/cosmos-sdk/types"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (q queryServer) SayHello(ctx context.Context, req *types.QuerySayHelloRequest) (*types.QuerySayHelloResponse, error) {
    if req == nil {
        return nil, status.Error(codes.InvalidArgument, "invalid request")
    }

    // 验证和上下文解包
    ctx := sdk.UnwrapSDKContext(ctx)

    _ = ctx
    // 自定义响应
    return &types.QuerySayHelloResponse{Name: fmt.Sprintf("Hello %s!", req.Name)}, nil
}

3.3 自定义查询响应

修改 query_say_hello.go

func (k Keeper) SayHello(goCtx context.Context, req *types.QuerySayHelloRequest) (*types.QuerySayHelloResponse, error) {
	if req == nil {
		return nil, status.Error(codes.InvalidArgument, "invalid request")
	}

	ctx := sdk.UnwrapSDKContext(goCtx)

	_ = ctx

	return &types.QuerySayHelloResponse{Name: fmt.Sprintf("Hello %s!", req.Name)}, nil
}

3.4 启动区块链并测试

启动区块链

# ignite chain serve

  Blockchain is running

  Blockchain is running

  Blockchain is running
  
 Added account alice with address cosmos1h2tqat8zme35fmkvk7yhp6r7r54edkg8eg0x43 and mnemonic:
  coffee coffee female clap cake kind pledge lamp give lens accuse reopen captain poverty program reward music trust transfer ski heavy husband 
  
 Added account bob with address cosmos1g7eamf74zkg629sd8wc0hdj88sqas8pfgrga7l and mnemonic:
  deputy fix marble merge toy rice traffic wait wife arrive laptop injury twelve doll shrimp guilt issue ripple position cereal width consider s
  
  🌍 Tendermint node: http://0.0.0.0:26657
  🌍 Blockchain API: http://0.0.0.0:1317
  🌍 Token faucet: http://0.0.0.0:4500
  
 Data directory: /root/.hello
 App binary: /root/go/bin/hellod
  
  Press the 'q' key to stop serve

运行测试

# cd /root/go/bin/hellod
# ./hellod q hello say-hello world
name: Hello world!

4 博客(增删改查)

https://docs.ignite.com/guide/blog

不细写了,直接看原文吧,底下记录一点关键代码。

4.1 Ignite 参数类型

Ignite CLI 支持在 Cosmos SDK 区块链中定义多种数据类型,以下是常用的数据类型:

基本数据类型

  • string:字符串类型,用于存储文本。
  • bool:布尔类型,值为 truefalse
  • int:有符号整数类型,用于存储整数值。
  • uint:无符号整数类型,只能存储非负整数。
  • int64 / uint64:64 位的有符号和无符号整数,适合存储较大的整数值。

数值类型

  • float:浮点数类型,用于存储小数。
  • float32 / float64:32 位和 64 位浮点数类型,分别提供不同的精度。

数组和列表类型

  • []<type>:泛型数组类型,用于存储指定类型的多个值。例如,[]string 表示字符串数组,[]uint64 表示无符号整数数组。

特殊类型

  • sdk.Coin:用于表示 Cosmos SDK 中的代币(coin)类型。代币包含数量和单位(例如 100atom),用于表示特定数量的原生代币。
  • sdk.Coins:表示多个代币,用于处理不同类型的资产组合。

自定义类型

  • 枚举类型(enums):通过定义特定的字符串或整数值集合,用于表示具有有限可能值的数据。
  • 时间类型(time.Time:用于存储日期和时间,通常在 Cosmos SDK 项目中用于处理区块时间戳或事件时间。

复杂结构类型

  • 结构体(structs):可以定义复杂的数据结构,包含多个字段,适用于具有多个属性的数据类型。

这些数据类型提供了灵活的数据建模能力,支持开发者根据需要定义区块链的存储和处理需求。Ignite CLI 会根据指定的数据类型自动生成相关的代码、查询和接口。

4.2 codec.BinaryCodec(cdc)

codec.BinaryCodec 是 Cosmos SDK 中用于数据编码和解码的接口。该接口包含一系列常用的函数,用于序列化(编码)和反序列化(解码)数据结构,以便进行存储、传输或模块间的数据交换。以下是 BinaryCodec 接口中常用的函数及其作用:

  1. MarshalBinaryBare

    MarshalBinaryBare(o interface{}) ([]byte, error)
    • 作用:将数据结构 o 编码为二进制格式。
    • 使用场景:当需要将结构化数据(如消息、状态)存储到数据库或进行网络传输时,可以调用该函数进行序列化。
    • 注意MarshalBinaryBare 通常用于内部存储,因为它不包含附加元数据(例如字段的类型信息),编码效率较高。
  2. UnmarshalBinaryBare

    UnmarshalBinaryBare(bz []byte, ptr interface{}) error
    • 作用:将二进制数据 bz 解码为结构化数据,并将结果存储在指针 ptr 指向的变量中。
    • 使用场景:当从数据库或网络中获取到二进制数据并需要还原为原始结构时使用该函数。
    • 注意:该函数必须知道数据的具体类型,通常在解码前已经知道数据结构的类型。
  3. MarshalBinaryLengthPrefixed

    MarshalBinaryLengthPrefixed(o interface{}) ([]byte, error)
    • 作用:将数据结构 o 编码为带有长度前缀的二进制格式。
    • 使用场景:带长度前缀的二进制格式在传输时可以明确表示数据的边界,常用于网络通信和日志记录。
    • 优势:长度前缀提供了更好的数据包分割管理,可用于防止数据被截断或破坏。
  4. UnmarshalBinaryLengthPrefixed

    UnmarshalBinaryLengthPrefixed(bz []byte, ptr interface{}) error
    • 作用:将带有长度前缀的二进制数据 bz 解码为结构化数据,并将结果存储在指针 ptr 指向的变量中。
    • 使用场景:从网络流或文件中读取带有长度前缀的二进制数据,并还原为结构化数据时使用。
  5. MarshalJSON

    MarshalJSON(o interface{}) ([]byte, error)
    • 作用:将数据结构 o 编码为 JSON 格式的字节切片。
    • 使用场景:当需要将数据以 JSON 格式与外部系统或前端交互时使用,便于人类阅读和调试。
    • 优势:JSON 是一种通用格式,易于调试,适用于对外提供的 API 接口。
  6. UnmarshalJSON

    UnmarshalJSON(bz []byte, ptr interface{}) error
    • 作用:将 JSON 格式的字节切片 bz 解码为结构化数据,并将结果存储在指针 ptr 指向的变量中。
    • 使用场景:从外部系统或用户接口接收到 JSON 数据并需要解析为结构化数据时使用。
  7. MustMarshalBinaryBare

    MustMarshalBinaryBare(o interface{}) []byte
    • 作用:将数据结构 o 编码为二进制格式,与 MarshalBinaryBare 类似,但若编码出错则直接触发 panic
    • 使用场景:在对编码错误容忍度较低的场景中使用,如编码错误几乎不可能发生时使用,简化代码结构。
  8. MustUnmarshalBinaryBare

    MustUnmarshalBinaryBare(bz []byte, ptr interface{})
    • 作用:将二进制数据 bz 解码为结构化数据,与 UnmarshalBinaryBare 类似,但若解码出错则直接触发 panic
    • 使用场景:在确定数据不会出错的情况下简化代码,减少错误处理代码的写法。
  9. MustMarshalJSON

    MustMarshalJSON(o interface{}) []byte
    • 作用:将数据结构 o 编码为 JSON 格式,若编码出错则直接触发 panic
    • 使用场景:在高置信度场景中快速编码 JSON 数据,而无需处理编码错误。
  10. MustUnmarshalJSON

MustUnmarshalJSON(bz []byte, ptr interface{})
  • 作用:将 JSON 字节切片 bz 解码为结构化数据,若解码出错则直接触发 panic
  • 使用场景:在确定 JSON 数据格式正确的场景下,减少错误处理逻辑。

codec.BinaryCodec 提供了一套完整的数据序列化和反序列化方法,适用于二进制、JSON 等不同编码格式。常用的函数包括带有或不带长度前缀的二进制编码和解码、JSON 编码和解码,以及支持 panic 的编码/解码版本。选择具体函数时,可以根据数据格式要求、错误处理需求和应用场景来使用这些编码和解码函数。

直接使用 MustMarshal (不加后缀指定类型) 会根据具体实现自动选择二进制编码或 JSON 编码,开发者无需显式指定。

附录

proto 简要教程

一个 .proto 文件用于定义协议缓冲区(Protocol Buffers)的消息和服务结构,主要包含以下几部分:

1. 语法声明(Syntax Declaration)

syntax = "proto3";
  • 这行代码指定使用 proto3 语法版本。proto3 是 Protocol Buffers 的最新版本,相比 proto2,简化了一些规则和功能(例如,移除了可选字段默认值等)。

2. 包声明(Package Declaration)

package example;
  • 声明了 .proto 文件所属的包。包名用于组织代码结构,避免命名冲突。在生成代码时,这个包名会映射到目标语言中的命名空间,例如在 Go 中会生成相应的 package

3. 导入(Imports)

import "google/protobuf/timestamp.proto";
  • 用于引入其他 .proto 文件,重用其中定义的消息或服务。比如 google/protobuf/timestamp.proto 提供了 Timestamp 类型,用于表示时间戳。导入的文件可以是标准库文件或用户自定义文件。

4. 消息定义(Message Definition)

message ExampleMessage {
    int32 id = 1;
    string name = 2;
    repeated string tags = 3;
}
  • message 定义了数据结构,每个 message 相当于一个对象或结构体。消息中包含字段,每个字段都有类型、名称和唯一的标签编号。
  • 字段的标签编号(如 id = 1)用于在序列化数据时标识字段,确保消息的兼容性。不同类型字段的作用:
    • int32string 等基本类型。
    • repeated 表示数组或列表,可以包含多个相同类型的元素。

5. 服务定义(Service Definition)

service ExampleService {
    rpc GetExample (ExampleRequest) returns (ExampleResponse);
}
  • 定义服务接口,类似于定义 API 的端点。service 内部包含一组 RPC 方法,用于客户端和服务器之间的远程过程调用。
  • 每个 RPC 方法指定请求和响应的消息类型(如 ExampleRequestExampleResponse),定义了客户端调用该方法时需要的输入和返回的输出。

6. 枚举类型(Enum Definition)

enum Status {
    UNKNOWN = 0;
    ACTIVE = 1;
    INACTIVE = 2;
}
  • 定义枚举类型,提供一组命名常量。每个枚举值都有一个编号,通常从 0 开始。这些常量可以在消息中用作字段的值,用于表示一组有限的选项或状态。

示例 .proto 文件结构

syntax = "proto3";

package example;

import "google/protobuf/timestamp.proto";

message ExampleRequest {
    int32 id = 1;
}

message ExampleResponse {
    string message = 1;
    google.protobuf.Timestamp timestamp = 2;
}

enum Status {
    UNKNOWN = 0;
    ACTIVE = 1;
    INACTIVE = 2;
}

service ExampleService {
    rpc GetExample (ExampleRequest) returns (ExampleResponse);
}

总结

Protocol Buffers(.proto 文件) 是一种与语言无关、平台无关的序列化数据结构定义方式,主要用于定义复杂的数据实体以及服务接口。虽然它在 Go 中非常常用,但它并不局限于 Go,其他语言(如 Python、Java、C++)也可以通过 .proto 文件生成相应的数据结构和代码。

在 Go 中,.proto 文件通常用来定义以下内容:

  1. 数据结构:定义复杂的数据实体(message),包含字段类型和序号,用于跨网络传输和存储。
  2. 服务接口:定义 gRPC 服务(service),包含可调用的 RPC 方法,规定了请求和响应类型,使不同服务之间能够进行 RPC 通信。

因此,.proto 文件的作用不仅仅是定义数据结构,还可以定义服务接口,使得不同服务之间能够高效通信。

Keeper

在 Cosmos SDK 的项目结构中,keeper 文件夹主要负责模块内部的核心业务逻辑和状态管理。keeper 是每个 Cosmos SDK 模块的重要组件,它封装了模块的数据访问和操作逻辑,确保数据的读取和写入是安全且一致的。以下是 keeper 文件夹的主要内容:

1. Keeper 结构体

  • keeper 文件夹通常包含一个 Keeper 结构体,该结构体负责管理模块的状态、配置和依赖。它通常包含一些存储的引用、其他模块的接口以及上下文,方便模块与 Cosmos SDK 的其他部分进行交互。
  • 典型的 Keeper 结构体定义会包含模块的存储器(store)、上下文(context)、其他模块的引用等,确保模块之间的数据可以安全地访问和修改。
type Keeper struct {
    storeKey   sdk.StoreKey
    cdc        codec.BinaryCodec
    bankKeeper types.BankKeeper
    // 其他依赖
}

2. 构造函数

  • keeper 文件夹中通常会定义一个构造函数,用于初始化 Keeper 结构体。构造函数会接受必要的依赖(如存储键、编码器和其他模块的 keeper),并将其赋值给 Keeper 结构体。
  • 通过构造函数,模块可以确保在创建时正确配置并提供所有依赖关系。
func NewKeeper(
    cdc codec.BinaryCodec,
    key sdk.StoreKey,
    bankKeeper types.BankKeeper,
    // 其他依赖
) Keeper {
    return Keeper{
        storeKey: key,
        cdc:      cdc,
        bankKeeper: bankKeeper,
        // 其他赋值
    }
}

3. 查询逻辑

  • keeper 文件夹包含查询函数,用于处理模块的自定义查询请求。这些函数通过 gRPCCLI 接口向外部提供模块数据的查询。
  • 查询函数会接受上下文和请求参数,访问状态存储,并返回响应。这些函数一般位于 query_xxx.go 文件中,例如 query_say_hello.go
func (k Keeper) SayHello(ctx context.Context, req *types.QuerySayHelloRequest) (*types.QuerySayHelloResponse, error) {
    // 实现查询逻辑
    return &types.QuerySayHelloResponse{Name: "Hello Cosmos"}, nil
}

4. 消息处理逻辑

  • keeper 文件夹中还包含模块的消息处理逻辑。消息是模块对区块链状态进行操作的主要手段。每个消息处理函数都会在 keeper 中实现,并且通常会在 msg_xxx.go 文件中定义。
  • 消息处理函数通过接收请求参数和上下文执行特定的状态更改,如更新账户余额、修改参数等。
func (k Keeper) HandleMsgSend(ctx sdk.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) {
    // 实现消息处理逻辑
    return &types.MsgSendResponse{}, nil
}

5. 状态管理

  • keeper 文件夹还包含状态管理相关的逻辑,用于直接与区块链的存储层交互。Keeper 结构体会使用存储器访问模块的状态数据,并提供创建、读取、更新和删除(CRUD)操作。
  • 这些状态管理函数确保数据的持久性和一致性。例如,可以有 SetDataGetData 函数,用于写入和读取模块数据。
func (k Keeper) SetData(ctx sdk.Context, key string, value string) {
    store := ctx.KVStore(k.storeKey)
    store.Set([]byte(key), []byte(value))
}

func (k Keeper) GetData(ctx sdk.Context, key string) (string, error) {
    store := ctx.KVStore(k.storeKey)
    bz := store.Get([]byte(key))
    if bz == nil {
        return "", errors.New("not found")
    }
    return string(bz), nil
}

6. 事件记录

  • 在某些业务逻辑中,Keeper 还会使用事件记录来追踪操作。这些事件可以用于链上和链下分析,如记录交易或数据更新。
  • 事件记录通常通过 ctx.EventManager() 生成,并随模块的操作一起写入区块链日志。

总结

keeper 文件夹在 Cosmos SDK 模块中扮演了核心角色,负责管理模块的业务逻辑、查询和状态存储。它为模块提供了统一的接口与其他模块和存储进行交互,确保数据操作的安全性和一致性。