对于 COSMOS 新手而言,下面有一些有用的链接: |
COSMOS 建立在 CometBFT 之上,提供了身份验证、账户管理、防重放攻击、多签名账户等功能。同时还提供了 ignite cli 工具来简化开发流程。
COSMOS 基础架构如下:
模块
要实现 COSMOS 模块,需要实现 AppModule 接口。AppModule 接口定义如下:
type AppModule interface {
appmodule.AppModule
AppModuleBasic
}
将 AppModule 展开,其定义如下:
type AppModule interface {
// IsOnePerModuleType is a marker function just indicates that this is a one-per-module type.
IsOnePerModuleType()
// IsAppModule is a dummy method to tag a struct as implementing an AppModule.
IsAppModule()
// HasName allows the module to provide its own name for legacy purposes. Newer apps should specify the name for their modules using a map using NewManagerFromMap.
Name() string
RegisterLegacyAminoCodec(*codec.LegacyAmino)
RegisterInterfaces(types.InterfaceRegistry)
RegisterGRPCGatewayRoutes(client.Context, *runtime.ServeMux)
}
对于一个 Module 而言,其生命周期如下:
IBC 使用
要使用 COSMOS,最简单的方法是使用 ignite cli 工具。
ignite scaffold 提供了一系列的命令来生成模块、消息、查询等代码:
命令 | 作用 | 用法 |
---|---|---|
chain | 生成一个区块链项目 |
|
module | 生成一个新的模块 |
|
query | 生成一个 Query 服务 |
|
list | 生成一个 List 数据结构 |
|
map | 生成一个 Map 数据结构 |
|
single | 生成一个单例数据结构 |
|
IBC 基础
下面的例子在 IBC 的基础上,修复了官方的 Bug。
创建区块链
首先,我们需要创建一个区块链:
$ ignite scaffold chain planet --no-module
$ cd planet
创建模块
然后,我们创建一个模块:
$ ignite scaffold module blog --ibc
为类型生成 CRUD 操作
$ ignite scaffold list post title content creator --no-message --module blog
$ ignite scaffold list sentPost postID:uint title chain creator --no-message --module blog
$ ignite scaffold list timeoutPost title chain creator --no-message --module blog
创建 IBC 包
$ ignite scaffold packet ibcPost title content --ack postID:uint --module blog
修改源码
上面 我们为 blog 模块生成了具体的 CRUD 操作,但是具体的代码需要我们自己实现。
message IbcPostPacketData {
string title = 1;
string content = 2;
string creator = 3;
}
x/blog/keeper/msg_server_ibc_post.go
func (k msgServer) SendIbcPost(goCtx context.Context, msg *types.MsgSendIbcPost) (*types.MsgSendIbcPostResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// TODO: logic before transmitting the packet
// Construct the packet
var packet types.IbcPostPacketData
packet.Title = msg.Title
packet.Content = msg.Content
packet.Creator = msg.Creator
// Transmit the packet
_, err := k.TransmitIbcPostPacket(
ctx,
packet,
msg.Port,
msg.ChannelID,
clienttypes.ZeroHeight(),
msg.TimeoutTimestamp,
)
if err != nil {
return nil, err
}
return &types.MsgSendIbcPostResponse{}, nil
}
x/blog/keeper/ibc_post.go
func (k Keeper) OnRecvIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData) (packetAck types.IbcPostPacketAck, err error) {
// validate packet data upon receiving
if err := data.ValidateBasic(); err != nil {
return packetAck, err
}
k.SetPost(ctx, types.Post{
Title: data.Title,
Content: data.Content,
})
// TODO: packet reception logic
return packetAck, nil
}
x/blog/keeper/ibc_post.go
func (k Keeper) OnAcknowledgementIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData, ack channeltypes.Acknowledgement) error {
switch dispatchedAck := ack.Response.(type) {
case *channeltypes.Acknowledgement_Error:
// TODO: failed acknowledgement logic
_ = dispatchedAck.Error
return nil
case *channeltypes.Acknowledgement_Result:
// Decode the packet acknowledgment
var packetAck types.IbcPostPacketAck
if err := types.ModuleCdc.UnmarshalJSON(dispatchedAck.Result, &packetAck); err != nil {
// The counter-party module doesn't implement the correct acknowledgment format
return errors.New("cannot unmarshal acknowledgment")
}
// TODO: successful acknowledgement logic
k.SetSentPost(ctx, types.SentPost{
Id: packetAck.PostId,
Title: data.Title,
Chain: packet.DestinationPort + "-" + packet.DestinationChannel,
})
return nil
default:
// The counter-party module doesn't implement the correct acknowledgment format
return errors.New("invalid acknowledgment format")
}
}
x/blog/keeper/ibc_post.go
func (k Keeper) OnTimeoutIbcPostPacket(ctx sdk.Context, packet channeltypes.Packet, data types.IbcPostPacketData) error {
// TODO: packet timeout logic
k.SetTimeoutPost(ctx, types.TimeoutPost{
Title: data.Title,
Chain: packet.DestinationChannel + "-" + packet.DestinationPort,
})
return nil
}
测试 IBC 模块
要测试 IBC 模块,首先需要在项目根路径下创建两个配置文件:
earth.yml
version: 1
build:
proto:
path: proto
third_party_paths:
- third_party/proto
- proto_vendor
accounts:
- name: alice
coins:
- 1000token
- 100000000stake
- name: bob
coins:
- 500token
- 100000000stake
faucet:
name: bob
coins:
- 5token
- 100000stake
host: 0.0.0.0:4500
genesis:
chain_id: earth
validators:
- name: alice
bonded: 100000000stake
home: $HOME/.earth
mars.yml
version: 1
build:
proto:
path: proto
third_party_paths:
- third_party/proto
- proto_vendor
accounts:
- name: alice
coins:
- 1000token
- 1000000000stake
- name: bob
coins:
- 500token
- 100000000stake
faucet:
name: bob
coins:
- 5token
- 100000stake
host: :4501
genesis:
chain_id: mars
validators:
- name: alice
bonded: 100000000stake
app:
api:
address: :1318
grpc:
address: :9092
grpc-web:
address: :9093
config:
p2p:
laddr: :26658
rpc:
laddr: :26659
pprof_laddr: :6061
home: $HOME/.mars
然后运行:
$ ignite chain serve -c earth.yml
$ ignite chain serve -c mars.yml
$ rm -rf ~/.ignite/relayer
还需要安装 hermes:
$ ignite app install -g github.com/ignite/apps/hermes
然后运行:
$ ignite relayer hermes configure \
"earth" "http://localhost:26657" "http://localhost:9090" \
"mars" "http://localhost:26659" "http://localhost:9092" \
--chain-a-faucet "http://0.0.0.0:4500" \
--chain-b-faucet "http://0.0.0.0:4501" \
--chain-a-port-id "blog" \
--chain-b-port-id "blog" \
--channel-version "blog-1"
这次运行会失败,但是不用担心,将 ~/.ignite/relayer/hermes/earth_mars
修改为下面的内容:
Details
[global]
log_level = 'error'
[mode]
[mode.clients]
enabled = true
refresh = true
misbehaviour = true
[mode.connections]
enabled = true
[mode.channels]
enabled = true
[mode.packets]
enabled = true
clear_interval = 100
clear_on_start = true
tx_confirmation = true
auto_register_counterparty_payee = false
[rest]
enabled = false
host = '127.0.0.1'
port = 3000
[telemetry]
enabled = false
host = '127.0.0.1'
port = 3001
[[chains]]
id = 'earth'
type = 'CosmosSdk'
ccv_consumer_chain = false
rpc_addr = 'http://localhost:26657'
grpc_addr = 'http://localhost:9090'
event_source = {batch_delay = '500ms', mode = 'push', url = 'ws://localhost:26657/websocket'}
rpc_timeout = '10s'
trusted_node = true
account_prefix = 'cosmos'
key_name = 'wallet'
address_type = {derivation = 'cosmos'}
key_store_type = 'Test'
store_prefix = 'ibc'
default_gas = 1000000
max_gas = 10000000
gas_price = {denom = 'stake', price = 0.001}
gas_multiplier = 1.2
max_msg_num = 30
max_tx_size = 2097152
clock_drift = '5s'
max_block_time = '30s'
trusting_period = '14days'
trust_threshold = {denominator = '3', numerator = '2'}
memo_prefix = ''
sequential_batch_tx = false
[[chains]]
id = 'mars'
type = 'CosmosSdk'
ccv_consumer_chain = false
rpc_addr = 'http://localhost:26659'
grpc_addr = 'http://localhost:9092'
event_source = {batch_delay = '500ms', mode = 'push', url = 'ws://localhost:26659/websocket'}
rpc_timeout = '10s'
trusted_node = true
account_prefix = 'cosmos'
key_name = 'wallet'
address_type = {derivation = 'cosmos'}
key_store_type = 'Test'
store_prefix = 'ibc'
default_gas = 1000000
max_gas = 10000000
gas_price = {denom = 'stake', price = 0.001}
gas_multiplier = 1.2
max_msg_num = 30
max_tx_size = 2097152
clock_drift = '5s'
max_block_time = '30s'
trusting_period = '14days'
trust_threshold = {denominator = '3', numerator = '2'}
memo_prefix = ''
sequential_batch_tx = false
然后重新运行上面的命令。运行过程中碰到的所有选项都置空即可。
开始测试
使用下面的命令启动测试:
$ ignite relayer hermes start "earth" "mars"
发送包:
$ planetd tx blog send-ibc-post blog channel-0 "Hello" "Hello Mars, I'm Alice from Earth" --from alice --chain-id earth --home ~/.earth
$ planetd q blog list-post --node tcp://localhost:26659
API 服务器
Cosmos 提供了 gRPC, RESTful 和 CometBFT 三种 API 服务器。REST 接口定义于 OpenAPI 规范文件中,调用时转发到 gRPC 服务器。因此 OpenAPI 可以访问所有 gRPC 存在的服务。
默认情况下 gRPC 服务器监听在 localhost:9090 端口,REST 服务器监听在 http://0.0.0.0:1317 端口。
Query 服务会通过 gRPC 接口暴露出去,但是 Msg 服务只能通过编程的形式访问[1]。 |
API 使用
由于 MSG 相关的 API 只能通过编程的形式访问,这里以 blog 教程为例了解其 API 的用法:
首先添加依赖项:
module blogclient
go 1.20
require (
blog v0.0.0-00010101000000-000000000000
github.com/ignite/cli v0.25.2
)
replace blog => ../blog
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
然后创建一个 main.go 文件:
main.go
package main
import (
"context"
"fmt"
"log"
// Importing the general purpose Cosmos blockchain client
"github.com/ignite/cli/v28/ignite/pkg/cosmosclient"
// Importing the types package of your blog blockchain
"blog/x/blog/types"
)
func main() {
ctx := context.Background()
addressPrefix := "cosmos"
// Create a Cosmos client instance
client, err := cosmosclient.New(ctx, cosmosclient.WithAddressPrefix(addressPrefix))
if err != nil {
log.Fatal(err)
}
// Account `alice` was initialized during `ignite chain serve`
accountName := "alice"
// Get account from the keyring
account, err := client.Account(accountName)
if err != nil {
log.Fatal(err)
}
addr, err := account.Address(addressPrefix)
if err != nil {
log.Fatal(err)
}
// Define a message to create a post
msg := &types.MsgCreatePost{
Creator: addr,
Title: "Hello!",
Body: "This is the first post",
}
// Broadcast a transaction from account `alice` with the message
// to create a post store response in txResp
txResp, err := client.BroadcastTx(ctx, account, msg)
if err != nil {
log.Fatal(err)
}
// Print response from broadcasting a transaction
fmt.Print("MsgCreatePost:\n\n")
fmt.Println(txResp)
// Instantiate a query client for your `blog` blockchain
queryClient := types.NewQueryClient(client.Context())
// Query the blockchain using the client's `PostAll` method
// to get all posts store all posts in queryResp
queryResp, err := queryClient.ListPost(ctx, &types.QueryListPostRequest{})
if err != nil {
log.Fatal(err)
}
// Print response from querying all the posts
fmt.Print("\n\nAll posts:\n\n")
fmt.Println(queryResp)
}