跨链三部曲-3:IBC

Cosmos Network 中最令人兴奋的特色是 InterBlockchain Communication (IBC) 协议,允许不同区块链之间进行资产转移。IBC 已经实现为一个 basecoin 插件,本节我将介绍如何使用它进行不同区块链间的资产互转。

IBC

IBC 的目的是让区块链彼此之间充当一个轻量级的客户端。由于我们使用经典的拜占庭一致性容错算法,在客户端上进行验证是简单可行的:只须校验最新块的签名并验证 ABCI 应用的默克尔树根。

Tendermint 共识引擎中,多个校验节点在处理新的区块之前会先达成一致。这意味着在下一个区块来临之前,当前区块的签名及其根状态不会加入到区块链中。每个区块包含一个 LastCommit (用于确认前一个区块的投票信息)和 AppHash(在区块头部,指处理完前一个区块的交易后的默克尔树根的 hash)。

因此,如果我们要从高度 H 验证 AppHash,我们需要高度 H+1 节点的 LastCommit 的签名(请记住:AppHash 只包含了从叶子节点到 H-1 高度节点的所有交易结果)。

不像 PoW,轻量级客户端协议不需要下载并检查所有区块链节点的头部信息,客户端总是能够直接跳跃到最新的头部,只要 validator 节点集还没有变更。如果 validator 节点集发生了变动,客户端需要跟踪这些变化,这个时候才需要从变更点开始下载所有区块点头部。这里,简单起见,我们假设这些 validator 节点集是静态不变的。

现在,让我们来演示 IBC 是如何工作的。假设我们有两条链:chain1 和 chain2,从 chain1 发送数据到 chain2,我们需要这么几个步骤:

  • 在 chain2 上注册 chain1 的信息(chainID、genesis 配置等)
  • 在 chain1 上,广播一条将要外发给 chain2 的 IBC 交易
  • 广播一条交易给 chain2 告诉它 chain1 的最新状态(header和commit信息)
  • chain1 向 chain2 发送 IBC 交易(区块高度、数据以及并默克尔证明),由于 chain2 已经知道了 chain1 的最新状态,它只需验证这个默克尔证明就可以了。

最重要的部分是:

在 chain2 上更新 chain1 的最新的状态,然后 chain1 向 chain2 发出 Merkle proof:我的数据包确实已经发出来了。

上述每一步都包含一种 IBC 交易类型,让我们 one by one 地来看一下:

IBCRegisterChainTx

IBCRegisterChainTx 用来注册一条链到另外一条链上。

1
2
3
4
5
6
type IBCRegisterChainTx struct { BlockchainGenesis }

type BlockchainGenesis struct {
ChainID string
Genesis string
}

对于给定的 chainID,该类型交易只能发送一次,重复发送将返回错误。

IBCUpdateChainTx

IBCUpdateChainTx 用于在其它链上更新当前链的信息。

1
2
3
4
type IBCUpdateChainTx struct {
Header tm.Header
Commit tm.Commit
}

IBCPacketCreateTx

IBCPacketCreateTx 用来创建一个将要外发给其他链的交易。

1
2
3
4
5
6
7
8
9
10
11
type IBCPacketCreateTx struct {
Packet
}

type Packet struct {
SrcChainID string
DstChainID string
Sequence uint64
Type string
Payload []byte
}

IBCPacketPostTx

IBCPacketPostTx 用来向外发送交易到另外一条链。

1
2
3
4
5
type IBCPacketPostTx struct {
FromChainID string // The immediate source of the packet, not always Packet.SrcChainID
FromChainHeight uint64 // The block height in which Packet was committed, to check Proof Packet
Proof *merkle.IAVLProof
}

IBC State

了解完跟跨链相关的交易类型,来看看 state。每条链都会在默克尔树中存储关于 IBC 的 state,对于当前链来说,它记录的每条注册到它上面的其它链的信息包括:

  • Genesis configuration
  • Latest state
  • Headers for recent heights

当然,还会记录所有它收到(ingress) 和外发(egress)的交易(packet)。

每当一笔提交 IBCUpdateChainTx 交易,这条链的会相应地更新它的 state, 每当创建一笔IBCPacketCreateTx 交易,新的 packet 数据会添加到 egress state 中,每当接收到一笔 IBCPacketPostTx 交易并验证通过了 proof 后,新的 packet 数据会添加到 ingress state 中。

Relay

由于我们内部需要这些跟跨链相关的交易,以便在不同链之间以一种安全的方式来跟踪所有 proof,为了让整个流程显得流畅自然,我们可以运行一个 relay 程序来处理这些跟跨链相关的交互操作。

在本例中,只需要 2 步:

  • basecoin relay init:每条链都在上面注册好另外一条链,并确保彼此可以发送和接收交易。
  • basecoin relay start:长轮询,不断地从一条链上拉取跨链交易并转发到另外一条链上。

前提是:relay 程序必须有权限读写这些链的账户及资产信息,因为发送 IBC 交易需要支付一定的 Fee。

Try it out

有了前面这些知识做铺垫,现在,让我们来真实地体验下跨链交易:

Preliminaries

1
2
# first, clean up any old garbage for a fresh slate...
rm -rf ~/.ibcdemo/

为了方便后续操作,先设置好一些环境变量和命令行别名:

1
2
3
4
5
6
7
8
export BCHOME1_CLIENT=~/.ibcdemo/chain1/client
export BCHOME1_SERVER=~/.ibcdemo/chain1/server
export BCHOME2_CLIENT=~/.ibcdemo/chain2/client
export BCHOME2_SERVER=~/.ibcdemo/chain2/server
alias basecli1="basecli --home $BCHOME1_CLIENT"
alias basecli2="basecli --home $BCHOME2_CLIENT"
alias basecoin1="basecoin --home $BCHOME1_SERVER"
alias basecoin2="basecoin --home $BCHOME2_SERVER"

设置两条链的 chainID

1
2
export CHAINID1="test-chain-1"
export CHAINID2="test-chain-2"

由于我们是在一个机器上运行两条基于 tendermint 的区块链,我们需要给两条链设置不同的端口:

1
2
3
4
export PORT_PREFIX1=1234
export PORT_PREFIX2=2345
export RPC_PORT1=${PORT_PREFIX1}7
export RPC_PORT2=${PORT_PREFIX2}7

Setup Chain 1

创建两个 test-chain-1 链的账户:

1
2
3
4
basecli1 keys new money
basecli1 keys new gotnone
export MONEY=$(basecli1 keys get money | awk '{print $2}')
export GOTNONE=$(basecli1 keys get gotnone | awk '{print $2}')

初始化这条链,它会给 $MONEY 账户打很多钱:

1
basecoin1 init --chain-id $CHAINID1 $MONEY

启动 basecoin 区块链:

1
2
3
sed -ie "s/4665/$PORT_PREFIX1/" $BCHOME1_SERVER/config.toml

basecoin1 start &> basecoin1.log &

test-chain-1 跑起来了,你可以通过 tail -f basecoin1.log 实时查看它的日志。

接下来,我们将 basecli 客户端连接上 test-chain-1,验证 test-chain-1 的最新状态,下面第一个账户应该显示有钱,第二个显示没钱。

1
2
3
basecli1 init --node=tcp://localhost:${RPC_PORT1} --genesis=${BCHOME1_SERVER}/genesis.json
basecli1 query account $MONEY
basecli1 query account $GOTNONE

Setup Chain 2

设置 test-chain-2test-chain-1 步骤类似:

1
2
3
4
basecli2 keys new moremoney
basecli2 keys new broke
MOREMONEY=$(basecli2 keys get moremoney | awk '{print $2}')
BROKE=$(basecli2 keys get broke | awk '{print $2}')

准备创世块,启动节点:

1
2
3
4
5
basecoin2 init --chain-id $CHAINID2 $(basecli2 keys get moremoney | awk '{print $2}')

sed -ie "s/4665/$PORT_PREFIX2/" $BCHOME2_SERVER/config.toml

basecoin2 start &> basecoin2.log &

我们将 basecli 客户端连接上 test-chain-2,验证 test-chain-2 的最新状态,同样,下面第一个账户应该显示有钱,第二个显示没钱。

1
2
3
basecli2 init --node=tcp://localhost:${RPC_PORT2} --genesis=${BCHOME2_SERVER}/genesis.json
basecli2 query account $MOREMONEY
basecli2 query account $BROKE

Connect these chains

OK,现在我们有两条独立的 basecoin 区块链在运行,让我们启动 relay 程序把它们串联起来,让它们可以彼此发送交易给对方。

relay 账户需要有余额来支付 IBC 消息,因此,我们需要先往 relay 账户充值部分钱。

1
2
3
4
5
6
7
8
9
10
# note that this key.json file is a hardcoded demo for all chains, this will
# be updated in a future release
RELAY_KEY=$BCHOME1_SERVER/key.json
RELAY_ADDR=$(cat $RELAY_KEY | jq .address | tr -d \")

basecli1 tx send --amount=100000mycoin --sequence=1 --to=$RELAY_ADDR --name=money
basecli1 query account $RELAY_ADDR

basecli2 tx send --amount=100000mycoin --sequence=1 --to=$RELAY_ADDR --name=moremoney
basecli2 query account $RELAY_ADDR

一切准备就绪,启动 relay 程序:

1
2
3
4
5
6
7
8
basecoin relay init --chain1-id=$CHAINID1 --chain2-id=$CHAINID2 \
--chain1-addr=tcp://localhost:${RPC_PORT1} --chain2-addr=tcp://localhost:${RPC_PORT2} \
--genesis1=${BCHOME1_SERVER}/genesis.json --genesis2=${BCHOME2_SERVER}/genesis.json \
--from=$RELAY_KEY

basecoin relay start --chain1-id=$CHAINID1 --chain2-id=$CHAINID2 \
--chain1-addr=tcp://localhost:${RPC_PORT1} --chain2-addr=tcp://localhost:${RPC_PORT2} \
--from=$RELAY_KEY &> relay.log &

Sending cross-chain payments

好了,test-chain-1test-chain-2 通过 IBC 协议彼此连接好,跨链中最艰巨的部分已经完成,现在,就是见证 跨链交易 奇迹的时刻:

1
2
3
4
5
6
7
8
# Here's an empty account on test-chain-2
basecli2 query account $BROKE
# Let's send some funds from test-chain-1
basecli1 tx send --amount=12345mycoin --sequence=2 --to=test-chain-2/$BROKE --name=money
# give it time to arrive...
sleep 2
# now you should see 12345 coins!
basecli2 query account $BROKE

Conclusion

本节我们详细阐述了 IBC 的工作机制,并演示了如何利用它在链间通信,并手把手教你进行跨链交易。其中,IBC 最核心的部分是:

在 chain2 上更新 chain1 的最新的状态,然后 chain1 向 chain2 发出 Merkle proof:我的数据包确实已经发出来了。

彦祖老师 wechat