aelf跨链机制和侧链
aelf跨链是一个很独立的模块,在设计的最初有这么一个需求:假设aelf节点对自己能跨链是无感知的。aelf代码的结构化因此而更好,不过也在一定程度上增加了跨链模块的复杂度。
aelf跨链机制由以下几个组件实现(v1.2.0版本):
AElf.CrossChain.Core
AElf.CrossChain
AElf.GrossChain.Grpc(由于要保持独立,不能搭AElf.OS.Network.Grpc的顺风车了)
crosschain_rpc.proto
AElf.Contracts.CrossChain(跨链合约,aelf的每条链都会部署一个)
asc7.proto
本文目的是解答以下问题:
在aelf网络中,如何启动一条新的侧链
跨链转账是如何实现的
如何利用跨链机制在aelf不同的链之间同步其他数据
父链 -> 子链
子链 -> 父链
子链 -> 子链(同一个父链)
初始化新的链 - Initialize New Side Chain
通过提案的形式创建新的链
侧链在创建以后,需要当前主网BP增加服务器跑新的侧链,因此是需要BP同意的。
开发者(侧链的申请者)通过aelf主链的CrossChain合约的RequestSideChainCreation方法申请创建一条侧链,该方法会通过inline tx调用Parliament合约的CreateProposalBySystemContract方法,后者会随即创建一个提案。要注意的是,RequestSideChainCreation方法的参数包含了新侧链启动后的一系列参数,如索引费用、侧链Primary币种等。
BP发交易通过该侧链创建提案后,开发者继续调用CrossChain合约的ReleaseSideChainCreation方法释放创建侧链的提案,该提案回调CrossChain合约的CreateSideChain方法,完成侧链的创建。
至此,新侧链的信息就通过链上提案的机制,在CrossChain合约中留下了记录。当BP试图启动该侧链的时候,需要用到CrossChain合约里的信息。

附上aelf主网上tDVV侧链创建的两笔交易:
RequestSideChainCreation(因为要创建的是公共侧链,所以参数为空)
部署和启动新的侧链
跨链模块Grpc服务
crosschain_rpc.proto中定义了三类服务:
ParentChainRpc
RequestIndexingFromParentChain:包含Parent Chain回应Side Chain(Child Chain)申请索引数据的逻辑。
RequestChainInitializationDataFromParentChain:Side Chain刚启动的时候,Parent Chain要回应Side Chain(Child Chain),应该使用什么数据来完成对创世区块的初始化。
SideChainRpc
RequestIndexingFromSideChain:包含Side Chain(Child Chain)回应Parent Chain申请索引数据的逻辑。
BasicCrossChainRpc
CrossChainHandShake:主要用途是节点重启后,本地的Grpc客户端实例都没了(因为存在内存里),需要依赖handshake来重建这些Grpc客户端(有一个新的链的节点发过来的Grpc handshake,就往内存里放一个新的Grpc客户端)。具体的重建方式就是发一个
NewChainConnectionEvent事件,交给该事件的处理器去创建。
侧链启动时初始化CrossChain合约
当aelf从创世区块开始启动的时候,会使用一系列的交易构造创世区块。在创世区块中,会完成对系统合约的一系列初始化,其中就包含对该侧链的CrossChain合约的初始化(通过调用CrossChain合约的Initialize方法)。侧链的CrossChain合约初始化的数据来源为AElf.Blockchains.SideChain中实现的SideChainInitializationDataProvider。再往下追究,实际上的数据来源为两处:
配置文件中的
CrossChainsection;构造了一个访问Parent Chain(也就是aelf主链)的Grpc客户端,通过Grpc向aelf主链获取到初始化数据。

当新侧链的CrossChain合约初始化完成后,今后和Parent Chain互相索引(Indexing)数据就方便了。
跨链模块作为节点插件
aelf链启动的过程中,会通过扫描INodePlugin的实现,发现CrossChainPlugin,从而加载跨链模块(abp提供的机制)。根据配置文件中CrossChain.ParentChainId的值,链会得知自己是否有主链,从而决定之后的行为。(新侧链从创世区块启动的时候需要配置CrossChain.ParentChainId,以及其他与Parent Chain节点能形成连接的Grpc参数)加载跨链模块,实质上是加载ICrossChainCommunicationPlugin的两个实现:
GrpcCrossChainClientNodePlugin:侧链启动后都会通过该服务创建一个面向Parent Chain的客户端;而Parent Chain得知自己有侧链刚启动后,也会面向Side Chain(Child Chain)创建一个对应的客户端(与Side Chain(Child Chain)完成handshake后监听和处理
NewChainConnectionEvent事件)。GrpcCrossChainServerNodePlugin:主侧链启动后都会通过该服务创建一个作为服务端的Grpc客户端,该客户端会同时启动
crosschain_rpc.proto中定义的三类服务,保留节点重启以后根据handshake重建Grpc客户端的可能性。
被创建的Grpc客户端都存放在GrpcCrossChainClientProvider中。
跨链转账 - Cross Chain Transfer
在正式介绍跨链数据索引之前,使用aelf的同构跨链转账作为例子。从用户的视角,跨链转账是一个非常容易完成、只是需要等待一两分钟的操作:

但是实际上,在用户发起CrossChainTransfer交易的同时,跨链机制就开始运作了。
关于transactionStatusMerkleTreeRoot的更多解释,见aelf区块的数据结构。
如果链A和链B能够互相索引,那就意味着链A(B)的以下数据都会被定期地通过跨链Grpc机制同步到链B(A)上:
Parent Chain索引Side Chain(Child Chain)的数据时:
chainId:标识Child Chain
height:高度
blockHeaderHash:对应高度的区块哈希(这就意味着能够索引的数据一定是lib高度以下的区块)
transactionStatusMerkleTreeRoot:对应高度的所有交易Id和执行状态作为叶子结点,组成的Merkle Tree的Root
Side Chain(Child Chain)索引Parent Chain的数据时,要多一些信息,正是多出来的这些信息,使得同Parent Chain的两条Side Chains(Child Chains,兄弟链)自动实现了数据的互相索引:
chainId:标识Parent Chain(不重要)
height:高度
transactionStatusMerkleTreeRoot:对应高度的所有交易Id和执行状态作为叶子结点,组成的Merkle Tree的Root
extraData:对应高度的区块头额外数据(主要是共识信息等用来完成区块验证的数据)
crossChainExtraData:跨链数据的区块头额外数据,其实是最近一次索引到的所有Child Chain(s)的transactionStatusMerkleTreeRoot构造的Merkle Tree的Root
indexedMerklePath:Parent Chain在height指定的高度,正好索引到当前Side Chain(Child Chain)的多个高度的信息的时候,每个高度的transactionStatusMerkleTreeRoot相对于所有索引信息生成的Merkle Tree Root(就是上面的crossChainExtraData)的Merkle Path
链A(B)的每个高度的数据,都会被通过提案的方式,同步到链B(A)上。(下图省略了Parliament通过提案的步骤。)这个自动提案,利用了aelf System Transaction机制,文档见aelf系统交易机制。

上图中CrossChain的几个方法,其实都定义在acs7.proto中,作为aelf同构跨链的合约实现标准。如果对ProposeCrossChainIndexing交易的入参感兴趣,可以看一下这个(侧链 -> 主链)和这个(主链 -> 侧链)交易。实际上,在上图描述的流程中,CrossChainReceiveToken交易就是构造了一个Merkle Path,在链B上证明交易CrossChainTransfer是存在于链A的。由于用户的跨链转账信息已经包含在交易CrossChainTransfer的参数中了,因此直接使用该交易的bytes足以把转账金额、代币之类的信息同步给B链。B链根据这些信息,便可以完成跨链转账的代币的发放。但是要跨链的数据并不总是能如此轻而易举地得到。如果单纯想把链A某个合约中的state同步给链B,就需要亲自构造一笔交易,在交易参数中附上需要同步的state,通过该交易能执行成功(被打包进区块里),向链B证明state在链A存在。TokenInfo的同步就是通过这种方式完成的。
跨链数据索引 - Cross Chain Indexing
这里就以把aelf主链某个代币的TokenInfo同步给aelf侧链作为例子,说明跨链数据索引如何能够实现state在不同链的同步(或者说向一条链证明,另一条的state确实如此)。

在这个过程中,从向侧链证明主链上某个数据存在的角度来看,ValidateTokenInfoExists交易的作用和CrossChainTransfer扮演了相同的角色:
ValidateTokenInfoExists的参数中包含了TokenInfo的全部信息;
ValidateTokenInfoExists能够在主链打包成功,就意味着该交易参数中的TokenInfo与主链Token合约的TokenInfo是一致的(这个逻辑包含在ValidateTokenInfoExists方法本身中)。
在侧链执行CrossChainCreateToken方法的过程中,也会断言,用于证明TokenInfo在主链为真的交易就是主链Token合约的ValidateTokenInfoExists交易。这样就完成了把链A的state同步给链B。
跨链验证的Merkle Path获取
主链索引到侧链的信息,和侧链索引到主链的信息,并不完全一致。对于Child Chain角色,Parent Chain只有一个。当需要在Child Chain上验证Parent Chain上某个交易Tx是否存在的时候,假设Parent Chain的这个交易被打包在高度H上,前后的逻辑如下
在Chain Chain某个高度,通过跨链索引机制记录了Parent Chain的高度H的所有交易Id和执行状态组成的Merkle Tree Root:transactionStatusMerkleTreeRoot;
在Parent Chain上,通过Web API
GetMerklePathByTransactionId,传入Tx的交易Id,查到该Tx相对于transactionStatusMerkleTreeRoot的Merkle Path;在Chain Chain对应的验证方法(最终都会调到CrossChain合约的VerifyTransaction方法)中,传入Tx的二进制数据和Merkle Path,在验证过程中会把Tx的二进制数据和
TransactionResultStatus.Mined组合后再哈希为叶子结点,如果通过了Merkle Proof,就可以证明交易Tx在Parent Chain不仅存在,而且执行成功了。
而对于Parent Chain,Child Chain可能有多个。当需要在Parent Chain上验证Child Chain上某个交易Tx是否存在的时候,同样假设Child Chain的这个交易被打包在高度H上,前后的逻辑如下:
在Parent Chain的某个高度,通过跨链索引机制记录了Child Chain高度H的所有交易Id和执行状态组成的Merkle Tree Root:transactionStatusMerkleTreeRoot;
但是,一个Parent Chain理论上可以拥有无上限个Child Chain,不应当针对每个Child Chain的每个高度,都存一下这个高度的transactionStatusMerkleTreeRoot,Parent Chain的选择是如果一次索引了某条Child Chain的多个区块,就把这些区块各自的transactionStatusMerkleTreeRoot组成一个Merkle Tree,只存其Root;
Parent Chain不再有Child Chain每个高度的transactionStatusMerkleTreeRoot后,就给在Parent Chain验证Child Chain上交易信息时,获取Merkle Path造成了困难:如果只通过Web API
GetMerklePathByTransactionId,只能获取到交易Tx针对transactionStatusMerkleTreeRoot的Merkle Path,离多个transactionStatusMerkleTreeRoot进一步构成的Root还有一段距离;这一段距离,Parent Chain的节点会通过Grpc发送给Child Chain:就是indexedMerklePath,这个值可以通过CrossChain合约的
GetBoundParentChainHeightAndMerklePathByHeight方法暴露出来。因此为了在Parent Chain上验证Child Chain上的交易Tx存在,除了要使用Web APIGetMerklePathByTransactionId获取从Tx(加其执行状态)到transactionStatusMerkleTreeRoot的Merkle Path之外,还需要通过CrossChain合约的GetBoundParentChainHeightAndMerklePathByHeight方法获取从transactionStatusMerkleTreeRoot到Parent Chain记录的Root的Merkle Path;把两段Merkle Path连接起来,就可以在Parent Chain上证明Child Chain上的Tx(及执行结果)是存在的了。
Last updated