Datachain团队致力于开发通过IBC(Inter-Blockchain Communication)异构跨链的模块和框架——“YUI”:
相关代码库有:
IBC为不同账本间的互操作协议,初始为Cosmos的一个核心模块,使得多个基于tendermint 或 不基于tendermint的账本之间可相互连通。
理论上,任何链都可通过IBC相互通讯。
IBC以Interchain standard (ICS)进行了标准化:
IBC协议采用分层设计,主要分为2层:
实现IBC协议的大多数工作集中在TAO层。一旦目标ledger的TAO层已实现,则很容易在TAO层之上实现不同的APP层协议。
IBC/TAO层的主要作用为:在两链之间以reliable,ordered and authenticated方式relay packets。
IBC/TAO实现为智能合约,在通过IBC相互连接的两个区块链上运行,这些智能合约称为“IBC/TAO模块”。 智能合约(IBC/TAO模块)中包含了以下元素:
在on-chain light client的基础之上,定义了connection abstraction和channel abstraction,用于连接2条链上的智能合约,并在二者之间relay packets。
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中的智能合约状态。
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发起交易来触发】
IBC/TAO合约中的channel语义语法可参看 ICS-4。
IBC中的channel abstraction用于表示不同链上2个智能合约的connect pair。
channel建立的握手机制与connection类似。一旦建立,channel可用于在链之间进行packet relay。
packet relay过程本身也采用类似TCP的3-way handshake机制。
channel握手过程的状态变化如下:【所有操作都是由relayer发起交易来触发】
一旦channel建立,2个智能合约就可发送和接收packets(packet内容为任意bytes sequences):
单一且简单的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的流程为:
sendPacket
operation for a packet that specifies the amount and denomination of the locked tokens。recvPacket
operation for the packet。acknowledgePacket
。
将该token由chainB再transfer转回chainA的流程为:
sendPacket
operation for a packet that specifies the amount and denomination of the locked tokens。recvPacket
operation for the packet。acknowledgePacket
。
若已实现了IBC/TAO,则以上提到的ICS-20 module仅需具有如下函数:
https://github.com/hyperledger-labs/yui-ibc-solidity【可支持以太坊等兼容solidity EVM的链】中IBC合约解析:
1)IBC/TAO层合约有:【主要有3个合约:light client合约、IBCHost合约 和 IBCHandler合约。】
library Height {
//struct definition
struct Data {
uint64 revision_number;
uint64 revision_height;
}
}
[32 bytes Vanity, List, 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。】setIBCModule
,参数为IBCHandler合约地址。使得generateClientIdentifier、generateConnectionIdentifier、generateChannelIdentifier、setClientImpl、setClientType、setClientState、setConsensusState、setProcessedTime、setProcessedHeight、setConnection、setChannel、setNextSequenceSend、setNextSequenceRecv、setNextSequenceAck、setPacketCommitment、deletePacketCommitment、setPacketAcknowledgementCommitment、setPacketReceipt、setExpectedTimePerBlock、claimCapability、authenticateCapability等操作仅能由IBCHandler合约调用。2)IBC/APP层合约有:【主要有3个合约:ICS20Bank、ICS20TransferBank 和 SimpleToken合约。】
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;
}
.......
}
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层部署及调用基本流程为:
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合约调用。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等函数。】registerClient
函数,参数为 light client 类型(如BesuIBFT2ClientType)和 相应的light client合约地址(如IBFT2Client合约地址)。作用为:调用IBCHost合约,设置clientRegistry[clientType]=clientAddress
。【每种client类型仅能注册一次】IBC/APP层合约 ICS20Bank、ICS20Transfer、ICS20TransferBank、IICS20Bank、IICS20Transfer、SimpleToken之间的关系为:【下图中的IBCAppModule 对应为 ICS20TransferBank合约。】
IBC/APP层合约有:【主要有3个合约:ICS20Bank、ICS20TransferBank 和 SimpleToken合约。】
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
的流程为:
get_ethProof
读取该区块相应的proof证明,包含accountProof和storageProof,存储在state.ethProof结构体中:type ETHProof struct {
AccountProofRLP []byte
StorageProofRLP [][]byte
}
UpdateClient
的流程为:
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,
}
}
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
的流程为:
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,
}
}
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);
}
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合约中。
capabilities[portID]
调用相应的DAPP应用合约中的onChanOpenInit、channelOpenTry、channelOpenAck、onChanOpenConfirm、onChanCloseInit、onChanCloseConfirm等函数。目前作用是设置该channel的escrowAddress为ICS20TransferBank合约地址。 】yui-ibc-solidity/tests/e2e/chains_test.go
示例中:
setOperator
函数,将ICS20TransferBank合约地址设置为OPERATOR角色。deposit
函数,往ICS20Bank合约地址中存入100token,在ICS20Banke合约中,会维护aliceA的balance:_balances[id][account] += amount;
。sendTransfer
函数,将其在chainA的100个token 通过chanA.PortID, chanA.ID(sourcePort和sourceChannel) 转给 chainB的bobB。设置的timeoutHeight为当前区块高度+1000
。sendTransfer
中:
_balances[id][account]
状态;若有前缀,说明是之前收到的其它链的token再转出,会直接将那100个token 从 ICS20Bank合约的_balances[id][account]
中减去。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
}
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)
);
}
}
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
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
,其中第二个元素为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。
IBFT2.0 Light Client 初始化时,需要一个trusted source。
考虑到validator set会更新,引入了trusting period,即a period of the validator set of height can be trusted:
T
before the current time.因此,在light client初始化时,需指定trusting period参数为T
,若T=0
,则指信任的validator set不会变,可一直信任。每个区块,仅可增加或减少1个validator。
当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】
yui-ibc-solidity/contracts/lib/TrieProofs.sol
,参考了https://github.com/lorenzb/proveth/blob/master/onchain/ProvethVerifier.sol。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。
若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应能检测到这样的失败或攻击。这部分工作未来将实现。