毕设10 - Juno(wasmd) 初见,节点部署和简单功能实践

| 笔记 | 14102 | 36分钟 | 毕设CosmosJunoCosmWasmwasmd

本文写作时 Juno 版本:v25.0.0-2-gd47e0713

还有官方 GPTs:Juno Simpler,数据库大概是 v12 左右的,但是还不错基本问题都能解决。

Juno 是在 wasmd 的基础上开发的,我误打误撞从 Juno 开始学习了。
本文的内容几乎可以直接迁移到 wasmd 使用和学习中,部分区别我在章节末作出了补充。
(只要全文替换 juno 为 wasm ,修改仓库和docker地址,就是 wasmd 的初见教程了)

Juno 仓库
wasmd仓库

1 运行 Juno 本地节点

git clone https://github.com/CosmosContracts/juno.git
cd juno
make install

1.1 Docker

docker run -it --rm -p 26657:26657 -p 26656:26656 -p 1317:1317 -p 9090:9090 \
  ghcr.io/cosmoscontracts/juno:latest ./setup_and_run.sh

当 Docker 容器启动后,你可以通过以下命令确认节点是否运行正常:

curl http://localhost:26657/status

如果节点运行正常,会收到 JSON 格式的状态信息。

但是发现这个 latest 版本才到 v12。手动换到最新的 v25。

查看版本:https://github.com/CosmosContracts/juno/pkgs/container/juno/versions?filters%5Bversion_type%5D=tagged

docker pull ghcr.io/cosmoscontracts/juno:25 

该版本docker镜像仍存在问题,/opt/run_junod.sh 中设置 --minimum-gas-prices 0.0001ujunox 代币类型是 ujunox ,而 setup_junod.sh 中设置的代币类型是 ustake

vi /opt/run_junod.sh

修改代币类型为 ustake 后,退出容器,将其保存为新的镜像:

(in docker) # exit
docker commit <my_container_name> <new_image_name>

然后启动新的镜像:

docker run --rm -it -p 26657:26657 --name my-juno-local <new_image_name> ./setup_and_run.sh
docker run --rm -it -p 26657:26657 --name my-juno-local my_juno25 ./setup_and_run.sh

补充

wasmd 的 docker 为:

cosmwasm/wasmd:latest

直接使用 wasmd 的镜像不需要以上复杂的修改。

1.2 本地脚本运行

./setup_junod.sh [其他账户]

可以直接创建其他账户,但是必须提前使用 junod keys add <账户名称> $KEYRING 创建好。

#!/bin/sh
#set -o errexit -o nounset -o pipefail

PASSWORD=${PASSWORD:-1234567890}
STAKE=${STAKE_TOKEN:-ustake}
FEE=${FEE_TOKEN:-ucosm}
CHAIN_ID=${CHAIN_ID:-testing}
MONIKER=${MONIKER:-node001}
KEYRING="--keyring-backend test"
TIMEOUT_COMMIT=${TIMEOUT_COMMIT:-5s}
BLOCK_GAS_LIMIT=${GAS_LIMIT:-10000000} # should mirror mainnet

echo "Configured Block Gas Limit: $BLOCK_GAS_LIMIT"

# check the genesis file
GENESIS_FILE="$HOME"/.juno/config/genesis.json
if [ -f "$GENESIS_FILE" ]; then
  echo "$GENESIS_FILE exists..."
else
  echo "$GENESIS_FILE does not exist. Generating..."

  junod init --chain-id "$CHAIN_ID" "$MONIKER"
  junod add-ica-config
  # staking/governance token is hardcoded in config, change this
  sed -i "s/\"stake\"/\"$STAKE\"/" "$GENESIS_FILE"
  # this is essential for sub-1s block times (or header times go crazy)
  sed -i 's/"time_iota_ms": "1000"/"time_iota_ms": "10"/' "$GENESIS_FILE"
  # change gas limit to mainnet value
  sed -i 's/"max_gas": "-1"/"max_gas": "'"$BLOCK_GAS_LIMIT"'"/' "$GENESIS_FILE"
  # change default keyring-backend to test
  sed -i 's/keyring-backend = "os"/keyring-backend = "test"/' "$HOME"/.juno/config/client.toml
fi

APP_TOML_CONFIG="$HOME"/.juno/config/app.toml
APP_TOML_CONFIG_NEW="$HOME"/.juno/config/app_new.toml
CONFIG_TOML_CONFIG="$HOME"/.juno/config/config.toml
if [ -n "$UNSAFE_CORS" ]; then
  echo "Unsafe CORS set... updating app.toml and config.toml"
  # sorry about this bit, but toml is rubbish for structural editing
  sed -n '1h;1!H;${g;s/# Enable defines if the API server should be enabled.\nenable = false/enable = true/;p;}' "$APP_TOML_CONFIG" > "$APP_TOML_CONFIG_NEW"
  mv "$APP_TOML_CONFIG_NEW" "$APP_TOML_CONFIG"
  # ...and breathe
  sed -i "s/enabled-unsafe-cors = false/enabled-unsafe-cors = true/" "$APP_TOML_CONFIG"
  sed -i "s/cors_allowed_origins = \[\]/cors_allowed_origins = \[\"\*\"\]/" "$CONFIG_TOML_CONFIG"
fi

# speed up block times for testing environments
sed -i "s/timeout_commit = \"5s\"/timeout_commit = \"$TIMEOUT_COMMIT\"/" "$CONFIG_TOML_CONFIG"

# are we running for the first time?
if ! junod keys show validator $KEYRING; then
  (echo "$PASSWORD"; echo "$PASSWORD") | junod keys add validator $KEYRING

  # hardcode the validator account for this instance
  echo "$PASSWORD" | junod genesis add-genesis-account validator "1000000000$STAKE,1000000000$FEE" $KEYRING

  # (optionally) add a few more genesis accounts
  for addr in "$@"; do
    echo $addr
    junod genesis add-genesis-account "$addr" "1000000000$STAKE,1000000000$FEE,5000000000uusd"
  done

  # submit a genesis validator tx
  ## Workraround for https://github.com/cosmos/cosmos-sdk/issues/8251
  (echo "$PASSWORD"; echo "$PASSWORD"; echo "$PASSWORD") | junod genesis gentx validator "250000000$STAKE" --chain-id="$CHAIN_ID" --amount="250000000$STAKE" $KEYRING
  ## should be:
  # (echo "$PASSWORD"; echo "$PASSWORD"; echo "$PASSWORD") | junod genesis gentx validator "250000000$STAKE" --chain-id="$CHAIN_ID"
  junod genesis collect-gentxs
fi

run_junod.sh

#!/bin/sh

if test -n "$1"; then
    # need -R not -r to copy hidden files
    cp -R "$1/.juno" /root
fi

mkdir -p /root/log
junod start --rpc.laddr tcp://0.0.0.0:26657 --minimum-gas-prices 0.0001ustake --trace

setup_and_run.sh

#!/bin/sh

if test -n "$1"; then
    # need -R not -r to copy hidden files
    cp -R "$1/.wasmd" /root
fi

mkdir -p /root/log
wasmd start --rpc.laddr tcp://0.0.0.0:26657 --trace

补充

Juno 脚本在 wasmd 上进行了一些参数调整,wasmd 的更简洁,适合学习:

#!/bin/sh
#set -o errexit -o nounset -o pipefail

PASSWORD=${PASSWORD:-1234567890}
STAKE=${STAKE_TOKEN:-ustake}
FEE=${FEE_TOKEN:-ucosm}
CHAIN_ID=${CHAIN_ID:-testing}
MONIKER=${MONIKER:-node001}

wasmd init --chain-id "$CHAIN_ID" "$MONIKER"
# staking/governance token is hardcoded in config, change this
sed -i "s/\"stake\"/\"$STAKE\"/" "$HOME"/.wasmd/config/genesis.json
# this is essential for sub-1s block times (or header times go crazy)
sed -i 's/"time_iota_ms": "1000"/"time_iota_ms": "10"/' "$HOME"/.wasmd/config/genesis.json

if ! wasmd keys show validator; then
  (echo "$PASSWORD"; echo "$PASSWORD") | wasmd keys add validator
fi
# hardcode the validator account for this instance
echo "$PASSWORD" | wasmd genesis add-genesis-account validator "1000000000$STAKE,1000000000$FEE"

# (optionally) add a few more genesis accounts
for addr in "$@"; do
  echo $addr
  wasmd genesis add-genesis-account "$addr" "1000000000$STAKE,1000000000$FEE"
done

# submit a genesis validator tx
## Workraround for https://github.com/cosmos/cosmos-sdk/issues/8251
(echo "$PASSWORD"; echo "$PASSWORD"; echo "$PASSWORD") | wasmd genesis gentx validator "250000000$STAKE" --chain-id="$CHAIN_ID" --amount="250000000$STAKE"
## should be:
# (echo "$PASSWORD"; echo "$PASSWORD"; echo "$PASSWORD") | wasmd gentx validator "250000000$STAKE" --chain-id="$CHAIN_ID"
wasmd genesis collect-gentxs

1.3 分析输出信息

从你提供的节点输出日志来看,运行的节点正在正常参与区块链网络的共识和状态更新流程。以下是日志中各部分的解析:

1.3.1 关键信息解析

  1. 区块执行和状态提交

    INF executed block height=171 module=state num_invalid_txs=0 num_valid_txs=0
    • 含义:第 171 个区块已经被成功执行。没有无效交易(num_invalid_txs=0),也没有有效交易(num_valid_txs=0)。
    • 模块state 模块负责应用区块内容并更新状态。
    INF committed state app_hash=30D13E32FA9357F0C27B6A5DC48E65E99EA2C302B3CEEED4F44A673BA1D85AEC height=171 module=state num_txs=0
    • 含义:第 171 个区块的状态变更已被提交,app_hash 是状态哈希,用于验证状态一致性。
    • 模块state 模块。
    INF indexed block events height=171 module=txindex
    • 含义:与第 171 个区块相关的事件(如交易)已被索引,便于后续查询。
    • 模块txindex 模块(交易索引)。
  2. 新高度的启动

    INF Timed out dur=1994.796713 height=172 module=consensus round=0 step=RoundStepNewHeight
    • 含义:网络进入了新的区块高度(172)的第 0 轮共识阶段。这表明之前的区块已完成处理,节点正在准备生成新的区块。
  3. 提案接收与共识

    INF received proposal module=consensus proposal="Proposal{172/0 (...)}"
    • 含义:节点收到了高度为 172 的区块提案(proposal),提议者是 3DAD15B485682FBD43119CDDE685C5142B9B0915
    • 模块consensus 模块。
    INF received complete proposal block hash=01B19714D56F0DB95008F250E626B8E3531AE5A74AA3FBB22768991CD3450D4B height=172 module=consensus
    • 含义:节点接收到了完整的提案,包含区块的哈希(block hash)。
    • 模块consensus 模块。
    INF finalizing commit of block hash=01B19714D56F0DB95008F250E626B8E3531AE5A74AA3FBB22768991CD3450D4B height=172 module=consensus num_txs=0
    • 含义:节点达成共识并提交高度为 172 的区块,区块内没有交易。
  4. 铸造代币

    INF minted coins from module account amount=63ustake from=mint module=x/bank
    • 含义:区块链网络铸造了 63 个单位的 ustake 代币(可能是通胀奖励或区块奖励)。
  5. 确保网络连接

    INF Ensure peers module=pex numDialing=0 numInPeers=0 numOutPeers=0 numToDial=10
    • 含义:节点当前没有入站或出站的网络连接,但它尝试拨号以连接到其他节点(最大 10 个节点)。
    • 模块pex 模块(负责点对点连接管理)。
    INF No addresses to dial. Falling back to seeds module=pex
    • 含义:没有可用的节点地址用于连接,节点将回退到预设的种子节点。

1.3.2 总结

  • 节点正在正常运行,且成功参与了网络共识流程。
  • 当前的区块中没有有效交易(所有 num_valid_txs=0),可能是因为没有用户向链发送交易。
  • 状态哈希(app_hash)验证了区块链状态的一致性。
  • 节点正在尝试建立与其他节点的连接,但暂时没有连接上,可能需要检查网络配置或种子节点的可用性。

2 简单测试

为了模拟远程网络环境(即本地通过 rpc/http 请求访问区块链),按照 1.1 节的方式,使用修改后的docker镜像启动节点:

docker run -it -p 26657:26657 --name my-juno-local my_juno25 ./setup_and_run.sh

为了方便关机后恢复区块链的状态,不添加 --rm 选项。通过 log 实时获取区块链的输出:

docker log -f my-juno-local

以下提到的 本地 即宿主机,模拟普通用户通过 junod 来访问区块链。

~/.juno/config/client.toml 中设置了 junod 作为客户端的配置,将其修改为:

chain-id = "testing"
keyring-backend = "test"
output = "text"
node = "tcp://localhost:26657"
broadcast-mode = "sync"

并且记住,stake 和 交易货币的名字:

STAKE=ustake
FEE=ucosm

2.1 创建本地钱包,并转账

1 创建本地钱包

junod keys add mywallet

这一步并不需要和区块链交互,只是在本地的 ~/.juno/keyring-test 中生成了 mywallet.info

检查是否已创建钱包:

junod keys show mywallet 

只获取钱包的地址(-a, address):

junod keys show mywallet -a

钱包地址例如:

juno1ddtgryk2xgt3qlsrx8z2kat9gpluw8886wpwhu

此时在本地创建了一个名叫 mywallet 的钱包,里面什么资产都没有。

查询钱包余额:

junod query bank balances <address>

或合并成一行:

junod query bank balances $(junod keys show mywallet -a)

2 validator 转账

如果你是按照第一步运行区块链的,那么区块链在创建之初,就已经分配了一个名外 validator 的账户,有很多初始资产 1000000000ustake,1000000000ujuno

现在让 validator 给 mywallet 进行转账。

(这一步不能在本地执行,因为 validator 的账户信息存放在 docker 容器里,没有账户的私钥是无法让它转账的)

进入 docker ,执行:

junod tx bank send validator <recipient_address> \
	100000ucosm --fees 200ustake --chain-id testing --keyring-backend test

<recipient_address> 替换为你的钱包地址。

junod tx bank send validator juno1ddtgryk2xgt3qlsrx8z2kat9gpluw8886wpwhu 100000ucosm --fees 200ustake --chain-id testing --keyring-backend test

junod tx bank send validator juno1ddtgryk2xgt3qlsrx8z2kat9gpluw8886wpwhu 100000ustake --fees 200ustake --chain-id testing --keyring-backend test

如果执行成功,可以在本地查询到你的资产:

junod query bank balances $(junod keys show mywallet -a)
balances:
- amount: "100000"
  denom: ucosm
- amount: "100000"
  denom: ustake
pagination:
  next_key: null
  total: "0"

2.2 部署和调用智能合约

0 前置步骤

安装 rust 环境

sudo apt update && sudo apt upgrade -y
sudo apt install curl build-essential -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.bashrc

检查是否安装成功:

rustc --version
cargo --version

安装 cargowasm 编译工具链,用于编译 Rust 智能合约:

rustup default stable
rustup target add wasm32-unknown-unknown

确保你有一个账户(例如 mywallet)并且有足够的资产来支付部署和调用费用。

1 创建合约项目

如果未安装 cargo-generate,请运行:

cargo install cargo-generate --locked

使用模板创建项目:

cargo generate --git https://github.com/CosmWasm/cw-template.git --name simple_counter
cd simple_counter

这将获取一个简单计数器智能合约。

2 编译智能合约

编译(未优化)

cargo wasm

得到编译好的wasm文件 ./target/wasm32-unknown-unknown/release/simple_counter.wasm

编译并优化

只要不超过大小限制, Juno 支持上传未优化的 wasm。但是为了减少合约的大小,以节省区块链上的存储成本和提高加载效率,通常需要对 wasm 文件进行优化

cosmwasm 提供了一个优化器,封装了编译和优化的全过程。

docker pull cosmwasm/optimizer:0.16.0
# 确保位于智能合约项目的根目录
docker run --rm -v "$(pwd)":/code cosmwasm/optimizer:0.16.0
  • -v "$(pwd)":/code 将当前目录挂载到容器中的 /code 目录。
  • 镜像会自动构建和优化你的智能合约,并输出优化后的 .wasm 文件到项目的 artifacts 目录中。

但是有两个问题:

  1. 下载速度很慢,甚至有 crate 下载失败,可能docker镜像不能走我宿主机的代理?
  2. 每次启动都得下载,效率太低。

为了解决这些问题:

  1. 使用共享 Cargo 缓存

    将宿主机的 Cargo 缓存目录挂载到容器中,这样即使重新运行也能复用已下载的依赖:

    -v "$HOME/.cargo/registry":/usr/local/cargo/registry :挂载依赖缓存。

    -v "$HOME/.cargo/git":/usr/local/cargo/git:挂载 Git 缓存。

  2. 运行 docker run 时传递代理环境变量:

    -e HTTP_PROXY=http://your-proxy:port
    -e HTTPS_PROXY=http://your-proxy:port 

完整的命令如下:

docker run --rm -v "$(pwd)":/code \
    -v "$HOME/.cargo/registry":/usr/local/cargo/registry \
    -v "$HOME/.cargo/git":/usr/local/cargo/git \
    -e HTTP_PROXY=http://192.168.3.56:21882 \
    -e HTTPS_PROXY=http://192.168.3.56:21882 \
    cosmwasm/optimizer:0.16.0

此时在 ./artifacts 中得到了优化后的 wasm 文件,文件缩小了约 25%:

240K    simple_counter.wasm(优化前)
184K    simple_counter.wasm(优化后)

3 部署智能合约

3.1 上传合约

(注意需要 mywallet 钱包里有足够的钱)

junod tx wasm store simple_counter.wasm --from mywallet --gas 2000000 --fees 50000ustake
  • gas 表示交易执行所允许的最大 Gas 单位数量。
  • fees 是为交易支付的手续费。

最后会返回 txhash ,根据 txhash 查询结果:

junod query tx <txhash> --output json | jq '.raw_log | fromjson'

记录其中的 code_id

3.2 实例化合约

junod tx wasm instantiate <code_id> '{"count":0}' --from mywallet --label "Simple Counter" --gas 200000 --fees 5000ustake --admin $(junod keys show mywallet -a)
junod tx wasm instantiate 1 '{"count":0}' --from mywallet --label "Simple Counter" --gas 200000 --fees 5000ustake --admin $(junod keys show mywallet -a)
  • --admin 指定一个管理员账户
  • 或者设置 --no-admin 表示合约不可更改,一旦部署,任何人都不能升级或迁移该合约。

根据 txhash 查询结果:

junod query tx <txhash> --output json | jq '.raw_log | fromjson'

记录其中的 _contract_address ,例如:

juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8

3.3 调用合约方法

  • 增加计数器的值

    junod tx wasm execute <contract_address> '{"increment":{}}' --from mywallet --gas 200000 --fees 5000ustake
    junod tx wasm execute juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8 '{"increment":{}}' --from mywallet --gas 200000 --fees 5000ustake
  • 查询计数器的值

    junod query wasm contract-state smart <contract_address> '{"get_count":{}}'
    junod query wasm contract-state smart juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8 '{"get_count":{}}'

尝试多次增加计数器的值,查看返回结果:

data:
  count: 2
操作类型是否消耗 Gas是否支付手续费是否改变链上状态是否需要签名
查询(query
执行(execute

2.3 增加新节点

新增一个节点,加入这个区块链。使用本地的多个 docker 容器模拟多个独立的节点。

1 初始化新节点

用之前配置好的镜像 my_juno25 启动一个新的 docker 容器。(记得换一个端口映射)

docker run -it -p 26658:26657 --name my-juno-local-2 my_juno25 /bin/sh

初始化新节点:

junod init <new_node_name> --chain-id <existing_chain_id>
junod init node002 --chain-id testing

2 复制创世区块

从主节点(现有节点)获取 genesis.json 文件,并复制到新节点的配置目录。在宿主机中使用 docker cp

mkdir -p ~/tmp/
docker cp my-juno-local:/root/.juno/config/genesis.json /root/tmp/
docker cp /root/tmp/genesis.json my-juno-local-2:/root/.juno/config/

3 设置种子节点

种子节点是区块链网络中特定设置的几个稳定的节点,能保持在线状态并提供稳定的连接。新节点通过和种子节点建立P2P连接,来加入区块链网络。

在我们这个例子里,种子节点就是第一个创建的主节点。

修改 ~/.juno/config/config.toml 中的:

vi ~/.juno/config/config.toml 
seeds = "<node_id>@<ip_address>:26656"

其中 node_id 是种子节点的 id,在 my-juno-local 镜像中执行如下命令获取节点 id:

junod tendermint show-node-id

ip地址,使用 docker 的默认虚拟网络,在主节点中执行:

ip addr

如果主节点是第一个 docker 容器,应该是 172.17.0.2 。(也可以自定义新的docker network,不在此赘述)

最终修改为:

seeds = "4a47e5f773f7012fd82ea195be94ace24bca13ca@172.17.0.2:26656"

或者使用一行指令:

sed -i 's/^seeds = ""/seeds = "<node_id>@<ip_address>:26656"/' ~/.juno/config/config.toml
sed -i 's/^seeds = ""/seeds = "4a47e5f773f7012fd82ea195be94ace24bca13ca@172.17.0.2:26656"/' ~/.juno/config/config.toml

4 启动新节点

使用 run_junod.sh 脚本:

cd /opt
./run_junod.sh

会开始同步区块链数据,直到同步到最近的节点。

5 验证新节点

在宿主机中:

curl http://localhost:26658/status

向新的节点发起查询请求:

junod query wasm contract-state smart <contract_address> '{"get_count":{}}' \
	--node "tcp://localhost:26658"

总结

至此,终于运行了一个 cosmos 区块链,并且在上面部署运行了 Wasm 智能合约。

最后,我发现 Juno 是在 wasmd 的基础上开发的,我误打误撞从 Juno 开始学习了。本文的内容几乎可以直接迁移到 wasmd 使用和学习中。

之后的任务是:

  1. 如何把 Wasm 模块单独拿出来,例如把 CosmWasm 的虚拟机放到另一个服务器上,让节点通过远程调用的方式执行智能合约。
  2. 把 CosmWasm 虚拟机移植到 TEE 中。

明天先继续写专利。