1. 引言
Datachain团队致力于开发通过IBC(Inter-Blockchain Communication)异构跨链的模块和框架——“YUI”:
相关代码库有:
- https://github.com/hyperledger-labs/yui-fabric-ibc
- https://github.com/hyperledger-labs/yui-ibc-solidity【可支持以太坊等兼容solidity EVM的链】
- https://github.com/hyperledger-labs/yui-corda-ibc
- https://github.com/hyperledger-labs/yui-relayer
2. IBC 总览
IBC为不同账本间的互操作协议,初始为Cosmos的一个核心模块,使得多个基于tendermint 或 不基于tendermint的账本之间可相互连通。
理论上,任何链都可通过IBC相互通讯。
IBC以Interchain standard (ICS)进行了标准化:
3. IBC 架构设计
IBC协议采用分层设计,主要分为2层:
- IBC/TAO:底层的transport,authentication and ordering层。
- IBC/APP:基于TAO构建的上层应用层。
实现IBC协议的大多数工作集中在TAO层。一旦目标ledger的TAO层已实现,则很容易在TAO层之上实现不同的APP层协议。
3.1 IBC/TAO层(合约)
IBC/TAO层的主要作用为:在两链之间以reliable,ordered and authenticated方式relay packets。
- reliable:是指 源链仅发送一个packet,目标链仅接收一次,二者无需信任任何第三方。事实上,链之间并不相互通信。因此,需要“relayer”来relay packets from one to another,但是relayer是无需许可的,任何人都可运行relayer。
- ordered:是指 目标链接收packet的顺序与源链发送packet的顺序一致。
- authenticated:是指 IBC relay packets采用“channel”抽象,channel的每个end都专门分配给特定的智能合约。因此,若目标链通过channel收到了一个packet,则可说明 源链端分配给该channel的特定智能合约发送了该packet。任何其他智能合约都无法使用该channel来发送packet。
IBC/TAO实现为智能合约,在通过IBC相互连接的两个区块链上运行,这些智能合约称为“IBC/TAO模块”。 智能合约(IBC/TAO模块)中包含了以下元素:
- on-chain light client:为IBC/TAO的基础,在无需信任第三方的情况下,验证某状态确实存在对方链上。
- connection abstraction
- channel abstraction
在on-chain light client的基础之上,定义了connection abstraction和channel abstraction,用于连接2条链上的智能合约,并在二者之间relay packets。
3.1.1 IBC/TAO合约中的on-chain light client
IBC/TAO合约中的on-chain light client语法和语义 可参看 ICS-2标准。
当“对方链”上有新的区块头时,relayer会query该区块头,并将该区块头提交到“本地链”的IBC/TAO合约。然后IBC/TAO合约中会运行“对方链”的 light client协议 来 验证该协议是否有效,若有效,则更新其 ClientState 以反应“对方链”的状态。
当ClientState 更新为“对方链”的最新区块头之后,IBC/TAO合约可检查在“对方链”是否存在某presented state。
比如,若“对方链”采用merkle tree来存储其world state,并在每个区块头中包含该merkle tree的root hash(与以太坊类似),可采用merkle proof来证明该tree中的智能合约状态。
3.1.2 IBC/TAO合约中的connection abstraction
IBC/TAO合约中的connection语义语法可参看 ICS-3。
IBC上下文中的"connection"表示为不同链上的2个ClientState组成的connected pair。
在开始用“channel” relay packets之前,两条链上的IBC/TAO需确定并验证要与之通信的ClientState。connection abstraction就用于此目的。链之间的connection建立机制类似于TCP的3-way handshake。
connection握手过程的状态变化如下:【所有操作都是由relayer发起交易来触发】
- 1)connOpenInit:在发起链上会创建并存储a new connection in INIT status。
- 2)connOpenTry:对方链 若验证 发起链 上该connection为INIT status,则在自身链上创建并存储a new connection in TRYOPEN status。
- 3)connOpenAck:发起链 若验证 对方链 上该connection为TRYOPEN status,则将自身链上该connection的状态由INIT更新为OPEN。
- 4)connOpenConfirm:对方链 若验证 发起链 上该connection状态已由INIT更新为OPEN,则将自身链上该connection状态由TRYOPEN更新为OPEN。
3.1.3 IBC/TAO合约中的channel abstraction
IBC/TAO合约中的channel语义语法可参看 ICS-4。
IBC中的channel abstraction用于表示不同链上2个智能合约的connect pair。
channel建立的握手机制与connection类似。一旦建立,channel可用于在链之间进行packet relay。
packet relay过程本身也采用类似TCP的3-way handshake机制。
channel握手过程的状态变化如下:【所有操作都是由relayer发起交易来触发】
- 1)chanOpenInit:在发起链上会创建并存储a new channel in INIT status。
- 2)chanOpenTry:对方链 若验证 发起链 上该channel为INIT status,则在自身链上创建并存储a new channel in TRYOPEN status。
- 3)chanOpenAck:发起链 若验证 对方链 上该channel为TRYOPEN status,则将自身链上该channel的状态由INIT更新为OPEN。
- 4)chanOpenConfirm:对方链 若验证 发起链 上该channel状态已由INIT更新为OPEN,则将自身链上该channel状态由TRYOPEN更新为OPEN。
3.1.4 packet relay
一旦channel建立,2个智能合约就可发送和接收packets(packet内容为任意bytes sequences):
- 1)sendPacket:在源链上创建并存储了一个sequence number为 N=nextSequenceNumber 的新packet。然后nextSequenceNumber 会加1。【不是由链外实体直接触发,而是由App合约触发。】
- 2)recvPacket:目标链 若验证 源链 上确实发送(创建)了该packet,则会在自身链上创建并存储一个sequence number为N的新packet。【由relayer触发。】
- 3)acknowledgePacket:删除sequence number为N的packet。【由relayer触发。】
3.2 IBC/APP层(合约)
单一且简单的packet relay 机制(IBC/TAO)支持任意跨链协议。建立在IBC/TAO之上的应用程序协议统称为IBC/APP。
Cross-chain token transfer举例:
ICS-20 为IBC/APP protocol例子,支持跨链token transfer。IBC/APP implementers无需设计或实现整个链的互操作机制,仅需实现sendPacket
,recvPacket
和acknowledgePacket
相关插件。
token由chainA通过ICS-20 transfer 到 chainB的流程为:
- 1)chainA上原子执行如下操作:
- Locking tokens in the ICS-20 module。
- 然后执行a
sendPacket
operation for a packet that specifies the amount and denomination of the locked tokens。
- 2)chainB上原子执行如下操作:
- 运行
recvPacket
operation for the packet。 - 然后mint voucher tokens that is equivalent to the locked tokens in the ICS-20 module。
- 运行
- 3)chainA上正常运行
acknowledgePacket
。
将该token由chainB再transfer转回chainA的流程为:
- 1)chainB上原子执行如下操作:
- Burning voucher tokens in the ICS-20 module。
- 然后执行a
sendPacket
operation for a packet that specifies the amount and denomination of the locked tokens。
- 2)chainA上原子执行如下操作:
- 运行
recvPacket
operation for the packet。 - 然后unlock voucher tokens that is equivalent to the burned vouchers in the ICS-20 module。
- 运行
- 3)chainB上正常运行
acknowledgePacket
。
若已实现了IBC/TAO,则以上提到的ICS-20 module仅需具有如下函数:
- locking and unlocking tokens
- minting and burning vouchers
4. yui-ibc-solidity IBC合约解析
https://github.com/hyperledger-labs/yui-ibc-solidity【可支持以太坊等兼容solidity EVM的链】中IBC合约解析:
-
1)IBC/TAO层合约有:【主要有3个合约:light client合约、IBCHost合约 和 IBCHandler合约。】
- IBCIdentifier:为library。主要为keccak256运算,用于生成client、consensus、connection、channel、packet、packetAcknowledgement 相关的commitment key,以及clientState、consensusState、connection、channel、packet、packetAcknowledgement 相关的Commitment Slot,和 part、channel 相关的capability path。
- IBCHeight:为library。主要为对Height.Data.revision_number的各种运算。
library Height { //struct definition struct Data { uint64 revision_number; uint64 revision_height; } }
- IBCMsgs:为library。定义了ICS-026中的client、connection handshake、channel handshake、channel closing 以及 packet relay等相关消息结构体。
- IClient:为interface。定义了getTimestampAtHeight、getLatestHeight、checkHeaderAndUpdateState、verifyClientState、verifyClientConsensusState、verifyConnectionState、verifyChannelState、verifyPacketCommitment 和 verifyPacketAcknowledgement等接口函数。
- MockClient:为合约。为对IClient接口的mock实现,实际并未做任何验证,用于测试场景。
- IBFT2Client:为合约。为对 Hyperledger Besu的IBFT 2.0(即Proof-of-Authority (PoA) Byzantine-Fault-Tolerant (BFT) )共识算法 进行验证的light client。【 Hyperledger Besu为以太坊联盟链方案,支持动态validator set。在区块中有额外的data field存储共识结果:
[32 bytes Vanity, List<Validators>, Votes, Round number, Commit Seals]
,其中第二个元素为a list of each address in a validator set of this block,第5个元素为commit seals to this block by the validator set,每个区块节点会verifies the commit seals to validates the block according to Algorithm 1。】 - IBCHost:为合约。部署完成后,owner后续需调用
setIBCModule
,参数为IBCHandler合约地址。使得generateClientIdentifier、generateConnectionIdentifier、generateChannelIdentifier、setClientImpl、setClientType、setClientState、setConsensusState、setProcessedTime、setProcessedHeight、setConnection、setChannel、setNextSequenceSend、setNextSequenceRecv、setNextSequenceAck、setPacketCommitment、deletePacketCommitment、setPacketAcknowledgementCommitment、setPacketReceipt、setExpectedTimePerBlock、claimCapability、authenticateCapability等操作仅能由IBCHandler合约调用。 - IBCClient:为library。实现了client相关函数,create和update等操作仅能由IBCHandler合约调用。
- IBCConnection:为library。实现了connection相关函数,仅由IBCHandler合约调用。以及各种state、commitment和ack的verify函数。
- IBCChannel:为library。实现了channel相关函数,以及sendPacket和recvPacket,writeAcknowledgement和acknowledgePacket函数,仅由IBCHandler合约调用。
- IBCHandler:为合约。部署时,需指定IBCHost合约地址。提供了registerClient(注册client类型,如mock还是ibft2.0 client,需由owner调用)、以及对IBCMsgs各消息的响应函数的实现。
- IBCModule:为interface,抽象了onChanOpenInit、onChanOpenTry、onChanOpenAck、onChanOpenConfirm、onChanCloseInit、onChanCloseConfirm、onRecvPacket和onAcknowledgementPacket接口函数。
-
2)IBC/APP层合约有:【主要有3个合约:ICS20Bank、ICS20TransferBank 和 SimpleToken合约。】
- ICS20Bank:为APP合约 之 ICS20Bank合约,除具体实现了IICS20Bank中定义的抽象接口之外,还额外实现了deposit和withdraw函数。
- ICS20Transfer:构建时需指定IBCHost和IBCHandler合约地址,为ICS20TransferBank的子合约,除具体实现了IBCModule中定义的抽象接口之外,还额外实现了IICS20Transfer中的sendTransfer函数。
- ICS20TransferBank:构建时需指定IBCHost、IBCHandler以及ICS20Bank合约地址,为App合约 之 ICS20TransferBank合约。
- IICS20Bank:为interface,抽象了transferFrom、mint、burn接口函数。
- IICS20Transfer:为interface,抽象了sendTransfer接口函数。
- SimpleToken:为APP合约 之 token合约。此处定义为了ERC20合约。
sendPacket的内容为:
library Packet {
//struct definition
struct Data {
uint64 sequence;
string source_port;
string source_channel;
string destination_port;
string destination_channel;
bytes data;
Height.Data timeout_height;
uint64 timeout_timestamp;
}
.......
}
4.1 yui-ibc-solidity IBC/TAO层合约
IBC/TAO层合约 IBCHost、IBCHandler、IBCClient、IBCConnection、IBCChannel、IBFT2Client、MockClient、IClient、IBCHeight、IBCModule、IBCMsgs、IBCIdentifier之间的关系为:
IBC/TAO层主要有3个合约:IBCHost合约、IBCHandler合约、IBFT2Client合约(和(或)MockClient测试合约)。
令:
//portID
const PortTransfer = "transfer"
//light client类型
const BesuIBFT2ClientType = "hyperledger-besu-ibft2"
const MockClientType = "mock-client"
IBC/TAO层部署及调用基本流程为:
- 1)部署IBFT2Client合约。
- 2)部署IBCHost合约。
- 3)部署IBCHandler合约,部署时,需指定IBCHost合约地址。
- 4)IBCHost合约owner 调用其自身的
setIBCModule
函数,将ibcModule设置为IBCHandler合约地址,使得其generateClientIdentifier、generateConnectionIdentifier、generateChannelIdentifier、setClientImpl、setClientType、setClientState、setConsensusState、setProcessedTime、setProcessedHeight、setConnection、setChannel、setNextSequenceSend、setNextSequenceRecv、setNextSequenceAck、setPacketCommitment、deletePacketCommitment、setPacketAcknowledgementCommitment、setPacketReceipt、setExpectedTimePerBlock、claimCapability、authenticateCapability等操作仅能由IBCHandler合约调用。 - 5)IBCHandler合约owner 调用其自身的
bindPort
函数,参数为portID 和 DAPP ICS20TransferBank合约地址。作用为:调用IBCHost合约,将capabilities[portID]
数组中插入ICS20TransferBanke合约地址。【同一portID的capabilities数组支持配置多个DAPP应用合约地址。但是在getModuleOwner中默认只返回第一个DAPP地址,实际还是一个portID对应一个DAPP合约。】【在channel握手协议中,会根据capabilities[portID]
调用相应的DAPP应用合约中的onChanOpenInit、channelOpenTry、channelOpenAck、onChanOpenConfirm、onChanCloseInit、onChanCloseConfirm等函数。】 - 6)IBCHandler合约owner 调用其自身的
registerClient
函数,参数为 light client 类型(如BesuIBFT2ClientType)和 相应的light client合约地址(如IBFT2Client合约地址)。作用为:调用IBCHost合约,设置clientRegistry[clientType]=clientAddress
。【每种client类型仅能注册一次】
4.2 yui-ibc-solidity IBC/APP层合约
IBC/APP层合约 ICS20Bank、ICS20Transfer、ICS20TransferBank、IICS20Bank、IICS20Transfer、SimpleToken之间的关系为:【下图中的IBCAppModule 对应为 ICS20TransferBank合约。】
IBC/APP层合约有:【主要有3个合约:ICS20Bank、ICS20TransferBank 和 SimpleToken合约。】
- ICS20Bank:为APP合约 之 ICS20Bank合约,除具体实现了IICS20Bank中定义的抽象接口之外,还额外实现了deposit和withdraw函数。
- ICS20Transfer:构建时需指定IBCHost和IBCHandler合约地址,为ICS20TransferBank的子合约,除具体实现了IBCModule中定义的抽象接口之外,还额外实现了IICS20Transfer中的sendTransfer函数。
- ICS20TransferBank:构建时需指定IBCHost、IBCHandler以及ICS20Bank合约地址,为App合约 之 ICS20TransferBank合约。
- IICS20Bank:为interface,抽象了transferFrom、mint、burn接口函数。
- IICS20Transfer:为interface,抽象了sendTransfer接口函数。
- SimpleToken:为APP合约 之 token合约。此处定义为了ERC20合约。
const (
DefaultChannelVersion = "ics20-1"
BlockTime uint64 = 1000 * 1000 * 1000 // 1[sec]
DefaultDelayPeriod uint64 = 3 * BlockTime
DefaultPrefix = "ibc"
TransferPort = "transfer"
RelayerKeyIndex uint32 = 0
)
yui-ibc-solidity/tests/e2e/chains_test.go
的SetupTest
为:
func (suite *ChainTestSuite) SetupTest() {
//信任的A链FullNode,对应的RPC接口为http://127.0.0.1:8645
chainClientA, err := client.NewBesuClient("http://127.0.0.1:8645", clienttypes.BesuIBFT2Client)
suite.Require().NoError(err)
//信任的B链FullNode,对应的RPC接口为http://127.0.0.1:8745
chainClientB, err := client.NewBesuClient("http://127.0.0.1:8745", clienttypes.BesuIBFT2Client)
suite.Require().NoError(err)
// A链和B链之间的ibcID
ibcID := uint64(time.Now().UnixNano())
// 2018/3018为全局的chainID。chainA和chianB分别维护各链信任的FullNode信息,以及各自所部署的TAO和APP合约信息。
suite.chainA = ibctesting.NewChain(suite.T(), 2018, *chainClientA, testchain0.Contract, mnemonicPhrase, ibcID)
suite.chainB = ibctesting.NewChain(suite.T(), 3018, *chainClientB, testchain1.Contract, mnemonicPhrase, ibcID)
// 会分别对链A和链B进行UpdateHeader操作
suite.coordinator = ibctesting.NewCoordinator(suite.T(), suite.chainA, suite.chainB)
}
UpdateHeader
的流程为:
- 1)定时30秒 -》GetIBFT2ContractState:读取信任的FullNode当前区块,调用
get_ethProof
读取该区块相应的proof证明,包含accountProof和storageProof,存储在state.ethProof结构体中:
type ETHProof struct {
AccountProofRLP []byte
StorageProofRLP [][]byte
}
- 2)解析当前区块头信息,存入state.ParsedHeader;验证当前区块头中有超过2/3 validator签名,将每个validator的seal信息拼接在一起,存入state.CommitSeals。
- 3)若当前区块高度大于链上light client合约中记录的header高度,则更新chain.LastContractState值为当前state。(第一次启动时,light client合约header信息为空,则直接更新chain.LastContractState为当前state)【此时并未调用light client合约更新。】
UpdateClient
的流程为:
- 1)ConstructIBFT2MsgUpdateClient:调用IBCHost合约的
getClientState
函数,读取链上存储的clientState的高度作为trustedHeight
,将对方链上的新header信息打包:
func (chain *Chain) ConstructIBFT2MsgUpdateClient(counterparty *Chain, clientID string) ibchandler.IBCMsgsMsgUpdateClient {
trustedHeight := chain.GetIBFT2ClientState(clientID).LatestHeight
cs := counterparty.LastContractState.(client.IBFT2ContractState)
var header = ibft2clienttypes.Header{
BesuHeaderRlp: cs.SealingHeaderRLP(),
Seals: cs.CommitSeals,
TrustedHeight: trustedHeight,
AccountStateProof: cs.ETHProof().AccountProofRLP,
}
bz, err := MarshalWithAny(&header)
if err != nil {
panic(err)
}
return ibchandler.IBCMsgsMsgUpdateClient{
ClientId: clientID,
Header: bz,
}
}
- 2)调用IBCHandler合约的
updateClient
函数,更新相应的consensusState和root。【其中checkHeaderAndUpdateState
会分别对storageProof和stateProof进行验证。】
function updateClient(IBCHost host, IBCMsgs.MsgUpdateClient calldata msg_) external {
host.onlyIBCModule();
bytes memory clientStateBytes;
bytes memory consensusStateBytes;
Height.Data memory height;
bool found;
(clientStateBytes, found) = host.getClientState(msg_.clientId);
require(found, "clientState not found");
(clientStateBytes, consensusStateBytes, height) = getClient(host, msg_.clientId).checkHeaderAndUpdateState(host, msg_.clientId, clientStateBytes, msg_.header);
persist states
host.setClientState(msg_.clientId, clientStateBytes);
host.setConsensusState(msg_.clientId, height, consensusStateBytes);
host.setProcessedTime(msg_.clientId, height, block.timestamp);
host.setProcessedHeight(msg_.clientId, height, block.number);
}
TestChannel
的流程为:
- 1)SetupClients:在2条链上分别创建对方链的client:【会返回在链上分配的clientId:clientA 和 clientB】
- 1.1)ConstructIBFT2MsgCreateClient:记录对方链的chainID、IBCHost合约地址,以及在
UpdateHeader
过程中记录在chain.LastContractState中的header、root以及validators信息。
func (chain *Chain) ConstructIBFT2MsgCreateClient(counterparty *Chain) ibchandler.IBCMsgsMsgCreateClient { clientState := ibft2clienttypes.ClientState{ ChainId: counterparty.ChainIDString(), IbcStoreAddress: counterparty.ContractConfig.GetIBCHostAddress().Bytes(), LatestHeight: ibcclient.NewHeightFromBN(counterparty.LastHeader().Number), } consensusState := ibft2clienttypes.ConsensusState{ Timestamp: counterparty.LastHeader().Time, Root: counterparty.LastHeader().Root.Bytes(), Validators: counterparty.LastContractState.(client.IBFT2ContractState).Validators(), } clientStateBytes, err := MarshalWithAny(&clientState) if err != nil { panic(err) } consensusStateBytes, err := MarshalWithAny(&consensusState) if err != nil { panic(err) } return ibchandler.IBCMsgsMsgCreateClient{ ClientType: ibcclient.BesuIBFT2Client, Height: clientState.LatestHeight.ToCallData(), ClientStateBytes: clientStateBytes, ConsensusStateBytes: consensusStateBytes, } }
- 1.2)调用本链IBCHandler合约的
createClient
函数,会在IBCHost合约中生成相应的clientId,并基于该clientId,在IBCHost合约中存储相应的clientType、clientState、ConsensusState、block.timestamp、block.number等信息。
function createClient(IBCHost host, IBCMsgs.MsgCreateClient calldata msg_) external { host.onlyIBCModule(); (, bool found) = getClientByType(host, msg_.clientType); require(found, "unregistered client type"); string memory clientId = host.generateClientIdentifier(msg_.clientType); host.setClientType(clientId, msg_.clientType); host.setClientState(clientId, msg_.clientStateBytes); host.setConsensusState(clientId, msg_.height, msg_.consensusStateBytes); host.setProcessedTime(clientId, msg_.height, block.timestamp); host.setProcessedHeight(clientId, msg_.height, block.number); }
- 1.1)ConstructIBFT2MsgCreateClient:记录对方链的chainID、IBCHost合约地址,以及在
- 2)CreateConnection:在链A和链B之间,链A上有clientB,链B上有clientA,遵循connection握手协议:
- 2.1)调用链A的IBCHandler合约的
ConnectionOpenInit
函数,会在IBCHost合约中生成相应的connectionId,并在IBCHost合约中存储相应的connection信息。【通过监听GeneratedClientIdentifier事件,来获取所创建的connectionId】
function connectionOpenInit(IBCHost host, IBCMsgs.MsgConnectionOpenInit memory msg_) public returns (string memory) { host.onlyIBCModule(); ConnectionEnd.Data memory connection = ConnectionEnd.Data({ client_id: msg_.clientId, versions: getVersions(), state: ConnectionEnd.State.STATE_INIT, delay_period: msg_.delayPeriod, counterparty: msg_.counterparty }); string memory connectionId = host.generateConnectionIdentifier(); host.setConnection(connectionId, connection); return connectionId; }
UpdateHeader
:以确保A链上成功创建了connectionId,获取新的区块,更新程序本地A链状态。
UpdateClient
:将A链状态更新至B链的light client合约中。-
2.2)查询链A上的proofConnection和proofClient,作为参数打包,调用链B的IBCHandler合约的
connectionOpenTry
函数,会做相应的connectionState和clientState验证,在IBCHost合约中生成相应的connectionId,并在IBCHost合约中存储相应的connection信息。【通过监听GeneratedClientIdentifier事件,来获取所创建的connectionId】【verifyConnectionState和verifyClientState
可再细看】
UpdateHeader
:以确保B链上成功创建了connectionId,获取新的区块,更新程序本地B链状态。
UpdateClient
:将B链状态更新至A链的light client合约中。 -
2.3)查询链B上的proofConnection和proofClient,作为参数打包,调用链A的IBCHandler合约的
connectionOpenAck
函数,会做相应的connectionState和clientState验证,并在IBCHost合约中更新相应的connection信息。【verifyConnectionState和verifyClientState
可再细看】
UpdateHeader
:以确保A链上交易成功,获取新的区块,更新程序本地B链状态。
UpdateClient
:将A链状态更新至B链的light client合约中。 -
2.4)查询链A上的proofConnection,作为参数打包,调用链B的IBCHandler合约的
connectionOpenConfirm
函数,会做相应的connectionState验证,并在IBCHost合约中更新相应的connection信息。【verifyConnectionState
可再细看】
UpdateHeader
:以确保B链上交易成功,获取新的区块,更新程序本地A链状态。
UpdateClient
:将B链状态更新至A链的light client合约中。
- 2.1)调用链A的IBCHandler合约的
- 3)CreateChannel:总体流程与CreateConnection类似,只是调用的为合约中channel相关函数。【与connection握手不同之处:在channel握手协议中,会根据
capabilities[portID]
调用相应的DAPP应用合约中的onChanOpenInit、channelOpenTry、channelOpenAck、onChanOpenConfirm、onChanCloseInit、onChanCloseConfirm等函数。目前作用是设置该channel的escrowAddress为ICS20TransferBank合约地址。 】
yui-ibc-solidity/tests/e2e/chains_test.go
示例中:
- 1)在chainA上部署SimpleToken合约时,默认会给部署者deployerA分配所有的token。
- 2)部署ICS20Bank合约。
- 3)部署ICS20TransferBank合约,部署时需指定IBCHost、IBCHandler以及ICS20Bank合约地址。
- 4)ICS20Bank合约部署者,调用
setOperator
函数,将ICS20TransferBank合约地址设置为OPERATOR角色。 - 5)授权IBCBank合约地址,可代deployerA花费100 token。
- 6)SimpleToken合约部署者deployerA调用 ICS20Bank合约的
deposit
函数,往ICS20Bank合约地址中存入100token,在ICS20Banke合约中,会维护aliceA的balance:_balances[id][account] += amount;
。 - 7)aliceA调用ICS20TransferBank合约的
sendTransfer
函数,将其在chainA的100个token 通过chanA.PortID, chanA.ID(sourcePort和sourceChannel) 转给 chainB的bobB。设置的timeoutHeight为当前区块高度+1000
。
在sendTransfer
中:- 7.1)会判断传入的denom参数是单纯的SimpleToken合约地址,还是前缀有sourcePort+sourceChannel。若为单纯的合约地址,则将那100token 直接转给sourceChannel的escrowAddress(即ICS20TransferBank合约地址),实际维护的是ICS20Bank合约的
_balances[id][account]
状态;若有前缀,说明是之前收到的其它链的token再转出,会直接将那100个token 从 ICS20Bank合约的_balances[id][account]
中减去。 - 7.2)封装
FungibleTokenPacketData
,再进一步封装Packet.Data
,调用IBCHandler合约的sendPacket
函数。会验证之前已bindPort
本DAPP合约地址;确保channel、connection权限状态正常;更新IBCHost的sequenceSend序号,同时设置IBCHost合约中的commits map为commitments[IBCIdentifier.packetCommitmentKey(portId, channelId, sequence)] = makePacketCommitment(packet);
【若该packet已处理完成,会删除,防止replay攻击】。最终会释放sendPacket event。
function _sendPacket(FungibleTokenPacketData.Data memory data, string memory sourcePort, string memory sourceChannel, uint64 timeoutHeight) virtual internal { (Channel.Data memory channel, bool found) = ibcHost.getChannel(sourcePort, sourceChannel); require(found, "channel not found"); ibcHandler.sendPacket(Packet.Data({ sequence: ibcHost.getNextSequenceSend(sourcePort, sourceChannel), source_port: sourcePort, source_channel: sourceChannel, destination_port: channel.counterparty.port_id, destination_channel: channel.counterparty.channel_id, data: FungibleTokenPacketData.encode(data), timeout_height: Height.Data({revision_number: 0, revision_height: timeoutHeight}), timeout_timestamp: 0 })); } function sendPacket(IBCHost host, Packet.Data calldata packet) external { host.onlyIBCModule(); Channel.Data memory channel; ConnectionEnd.Data memory connection; IClient client; Height.Data memory latestHeight; uint64 latestTimestamp; uint64 nextSequenceSend; bool found; channel = mustGetChannel(host, packet.source_port, packet.source_channel); require(channel.state == Channel.State.STATE_OPEN, "channel state must be OPEN"); require(hashString(packet.destination_port) == hashString(channel.counterparty.port_id), "packet destination port doesn't match the counterparty's port"); require(hashString(packet.destination_channel) == hashString(channel.counterparty.channel_id), "packet destination channel doesn't match the counterparty's channel"); connection = mustGetConnection(host, channel); client = IBCClient.getClient(host, connection.client_id); (latestHeight, found) = client.getLatestHeight(host, connection.client_id); require(packet.timeout_height.isZero() || latestHeight.lt(packet.timeout_height), "receiving chain block height >= packet timeout height"); (latestTimestamp, found) = client.getTimestampAtHeight(host, connection.client_id, latestHeight); require(found, "consensusState not found"); require(packet.timeout_timestamp == 0 || latestTimestamp < packet.timeout_timestamp, "receiving chain block timestamp >= packet timeout timestamp"); nextSequenceSend = host.getNextSequenceSend(packet.source_port, packet.source_channel); require(nextSequenceSend > 0, "sequenceSend not found"); require(packet.sequence == nextSequenceSend, "packet sequence != next send sequence"); nextSequenceSend++; host.setNextSequenceSend(packet.source_port, packet.source_channel, nextSequenceSend); host.setPacketCommitment(packet.source_port, packet.source_channel, packet.sequence, packet); // TODO emit an event that includes a packet }
- 7.1)会判断传入的denom参数是单纯的SimpleToken合约地址,还是前缀有sourcePort+sourceChannel。若为单纯的合约地址,则将那100token 直接转给sourceChannel的escrowAddress(即ICS20TransferBank合约地址),实际维护的是ICS20Bank合约的
- 8)UpdateHeader&UpdateClient:更新程序本地状态,并更新链B上的light client合约状态。
- 9)调用链A的IBCHost合约,读取当前最新的sequenceSend,基于此监听IBCHandler合约释放的sendPacket事件,过滤出符合相应序号、sourcePort和sourceChannel的sendPacket事件。
- 10)等待DelayPeriod(此处为1S),若不等待直接操作下一步会报错。
- 11)调用
eth_getProof
,获取链A该packet的storage proof。然后调用链B的IBCHandler合约的recvPacket
函数:会调用ICS20TransferBank 合约的onRecvPacket
函数,mint相应的金额给receiver,同时返回相应的acknowledgement。然后验证channel、connection和packet是否在有效期;验证packetCommitment;在IBCHost合约中设置packetReceipt;在IBCHost合约中设置PacketAcknowledgementCommitment;释放 WriteAcknowledgement事件 和 RecvPacket事件。
function onRecvPacket(Packet.Data calldata packet) external virtual override returns (bytes memory acknowledgement) {
FungibleTokenPacketData.Data memory data = FungibleTokenPacketData.decode(packet.data);
strings.slice memory denom = data.denom.toSlice();
strings.slice memory trimedDenom = data.denom.toSlice().beyond(
_makeDenomPrefix(packet.source_port, packet.source_channel)
);
if (!denom.equals(trimedDenom)) { // receiver is source chain
return _newAcknowledgement(
_transferFrom(_getEscrowAddress(packet.destination_channel), data.receiver.toAddress(), trimedDenom.toString(), data.amount)
);
} else {
string memory prefixedDenom = _makeDenomPrefix(packet.destination_port, packet.destination_channel).concat(denom);
return _newAcknowledgement(
_mint(data.receiver.toAddress(), prefixedDenom, data.amount)
);
}
}
- 12)UpdateHeader&UpdateClient:更新程序本地状态,并更新链B上的light client合约状态。
- 13)等待DelayPeriod(此处为1S),若不等待直接操作下一步会报错。
- 14)调用
eth_getProof
,获取链B上(packet.DestinationPort, packet.DestinationChannel, packet.Sequence)
的packetAcknowledgementCommitment 对应的storageProof。调用链A的IBCHandler合约的acknowledgePacket
函数:会调用ICS20TransferBank合约的onAcknowledgementPacket
函数,若acknowledgement中包含了失败信息,即意味着token转移失败,将token返回给sender;若成功,则什么都不做。然后验证channel、connection;验证packetAcknowledgement;若为ORDERED channel,则更新sequenceAck序号;删除该已处理的packetCommitment,防止replay攻击。
参考资料
[1] How Cosmos’s IBC Works to Achieve Interoperability Between Blockchains
[2] IBFT 2.0 Light Client
附录A——Hyperledger Besu的IBFT2.0 Light Client
Hyperledger Besu为以太坊联盟链方案,支持动态validator set。
Hyperledger Besu支持多种共识算法,其中IBFT 2.0(即Proof-of-Authority (PoA) Byzantine-Fault-Tolerant (BFT) )共识算法最受欢迎。
当采用IBFT2.0共识时,在区块中有额外的data field存储共识结果:[32 bytes Vanity, List<Validators>, Votes, Round number, Commit Seals]
,其中第二个元素为a list of each address in a validator set of this block,第5个元素为commit seals to this block by the validator set,每个区块节点会verifies the commit seals to validates the block according to Algorithm 1。
附录A.1 light client trusted source & trusting period
IBFT2.0 Light Client 初始化时,需要一个trusted source。
考虑到validator set会更新,引入了trusting period,即a period of the validator set of height can be trusted:
- The new block must be verified by the validator set of a block generated within time duration
T
before the current time.
因此,在light client初始化时,需指定trusting period参数为T
,若T=0
,则指信任的validator set不会变,可一直信任。每个区块,仅可增加或减少1个validator。
附录A.2 light client verification
当light client初始化了trusting period 和 a header from trusted source,需要基于 该trusting header 和 相应的trusted validators 来验证 incoming header。
对新提交的区块,validation function
需验证以下条件:【假设 B h B_h Bh为高度为 h h h的区块, V h V_h Vh为区块 B h B_h Bh的validator set, B T h BT_h BTh为区块 B h B_h Bh的timestamp, N o w ( ) Now() Now()为当前时间, T P TP TP为trusting period。
假设当前的trusted block高度为 n n n,untrusted block高度为 n + m n+m n+m, n > 0 且 m > 0 n>0且m>0 n>0且m>0】
- 1)当前时间在最新trusted block的trusting period: B T n < N o w ( ) < B T n + T P BT_n<Now()<BT_n+TP BTn<Now()<BTn+TP
- 2) B n + m B_{n+m} Bn+m具有 V n V_n Vn的1/3签名。因为最大拜占庭数量为 f ( n ) = ( n − 1 ) / 3 f(n)=(n-1)/3 f(n)=(n−1)/3,该要求可保证至少有1个honest validator。
- 3) B n + m B_{n+m} Bn+m具有 V N + M V_{N+M} VN+M的2/3+ 签名。可保证 n + m n+m n+m高度的固化区块是valid的。
- 4)此外,还会verifyStorageProof。具体见
yui-ibc-solidity/contracts/lib/TrieProofs.sol
,参考了https://github.com/lorenzb/proveth/blob/master/onchain/ProvethVerifier.sol。
附录A.3 light client liveness analysis
light client的liveness取决于validator set的变换。IBFT2.0支持Dynamic ValidatorSet,每个block仅可增加或减少1个validator。
因此,需确保存在某高度,上一节中的validation function
可验证该高度的区块是由IBFT2.0协议生成的。显然,对于validator set数量增加的情况,总是存在a verifiable height。接下来仅考虑validator set 数量减少的情况:
令validator set为 V V V,减少量为 Δ \Delta Δ,有 Δ ⊆ V \Delta\subseteq V Δ⊆V:
∣ V ∧ V − Δ ∣ ≥ ∣ V ∣ ∗ 1 / 3 |V \wedge V-\Delta|\geq |V| * 1/3 ∣V∧V−Δ∣≥∣V∣∗1/3
若该方程式成立,则总是存在a block height with a validator set that can validate the block to be validated。在IBFT2.0中,该方程式将成立,因为 ∣ V − Δ ∣ ≥ 1 |V-\Delta|\geq 1 ∣V−Δ∣≥1且 Δ = 1 \Delta=1 Δ=1。
附录A.4 light client fork detection
若the failure model of IBFT 2.0 is violated and there are more than (n-1) / 3 failed nodes, multiple valid confirmed blocks may be generated。
light client应能检测到这样的失败或攻击。这部分工作未来将实现。