LayerZero是一套全链互操作协议(Omnichain Interoperability protocol),所谓的全链互操作有两个特性:* 全链,不仅仅是## 简介 LayerZero是一套全链互操作协议(Omnichain Interoperability protocol),所谓的全链互操作有两个特性: * 全链,不仅仅是
在消息签名的方案中链上合约是完全相信签名消息的,而消息是否真实由签名方(不管是单方还是多方)来提供保证。LayerZero的方案中,消息需要经过合约验证,验证过程需要依赖两部分消息:Block和Proof。每个消息都可以进行多方签名,而且Block和Proof还需要经过MT算法校验。因此LayerZero的方案相比消息签名方案多了一个维度的安全性。
协议
LayerZero协议的组成部分包括:
- UserApplication(UA),应用LayerZero进行跨链通信的合约。
- LayerZero EndPoint,LayerZero的链上合约。在ChainA链接收UA的请求并输出log;在ChainB链接收Oracle和Relayer提供的MT消息,验证后向UA传递跨链消息。
- Oracle,将含有跨到链ChainB交易的区块头发送到ChainB链上。
- Relayer,将跨到链ChainB上的交易证明发送到ChainB链上。
UserApplication和LayerZero EndPoint均会部署到不同的链上,以一个跨链Token合约XYZ为例:
function send(uint16 _lzChainId, uint _amount) external payable { // burn token from sender _burn(msg.sender, _amount); // encode the payload with the receiver and amount to send bytes memory payload = abi.encode(msg.sender, _amount); // send LayerZero message endpoint.send{value:msg.value}(_lzChainId, dstAdd, payload, payable(msg.sender), address(0x0), bytes("")); } function receive(uint16 _srcChainId, bytes calldata _srcAddress, uint64 _nonce, bytes calldata _payload) external { // receive msg from LayerZero (bytes memory receiverBytes, uint amount) = abi.decode(_payload, (bytes, uint)); address receiver; assembly { receiver := mload(add(receiverBytes, 20)) } // mint token to receiver _mint(receiver, amount); }
当Alice希望从ChainA跨100个XYZ到ChainB时,在ChainA这边,Alice调用XYZ合约的send接口,XYZ合约先销毁Alice的100个token,然后调用LZ的EndPoint合约生成一个log
// chainId是ChainB // payload包含XYZ合约传递的消息(给Alice铸造100个token) event Packet(uint16 chainId, bytes payload);
在ChainB这边,LZ的EndPoint校验了Oracle和Relayer的MT消息之后,调用XYZ的receive接口。XYZ解析payload之后给Alice铸造100个token。
MPT算法
EndPoint中可以支持多种MT校验算法,其中对于EVM链采用的是MPT算法。Merkel Patricia Tree(MPT)是Ethereum中采用的一种数据结构,root hash就是Tree的root Node的hash。Ethereum的区块头中有三种root hash,分别是:stateRoot,transactionsRoot和receiptsRoot。其中TransactionReceipt包含log数据,在ChainB这边要验证的正是ChainA上产生的log,因此Relayer要提供的就是ChainA上receipt的Proof。
在计算receiptsRoot的MPT数据结构中,RLP(transactionIndex)作为Path,RLP(0,gasUsed,logBloom,logs)作为Value。LayerZero的MPT校验算法代码如下:
// hashRoot是receiptsRoot function _getVerifiedLog(bytes32 hashRoot, uint[] memory paths, uint logIndex, bytes[] memory proof) internal pure returns(ULNLog memory) { require(paths.length == proof.length, "ProofLib: invalid proof size"); RLPDecode.RLPItem memory item; bytes memory proofBytes; // 这部分代码验证Proof for (uint i = 0; i < proof.length; i++) { proofBytes = proof[i]; require(hashRoot == keccak256(proofBytes), "ProofLib: invalid hashlink"); item = RLPDecode.toRlpItem(proofBytes).safeGetItemByIndex(paths[i]); if (i < proof.length - 1) hashRoot = bytes32(item.toUint()); } // 这部分代码获取指定的log,3表示获取logs部分 RLPDecode.RLPItem memory logItem = item.typeOffset().safeGetItemByIndex(3); // logIndex表示获取指定位置的log RLPDecode.Iterator memory it = logItem.safeGetItemByIndex(logIndex).iterator(); ULNLog memory log; // 这个log就是event Packet(uint16 chainId, bytes payload) log.contractAddress = bytes32(it.next().toUint()); log.topicZeroSig = bytes32(it.next().getItemByIndex(0).toUint()); log.data = it.next().toBytes(); return log; }
验证Proof这块,比如区块中一共有3笔交易,这些交易的Index经过编码之后的path分别是[0,0],[0,1],[0,2],组成的MPT树为
hashRoot = hash(Proof[0]) paths[0]=1, proof[0] = [ <0>, <hash(proof[1])> ] paths[1]=1, proof[1]= [ <hash0>, <hash(proof[2])>, <hash1>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> ] paths[2]=1, proof[2]= [ <>, <0,gasUsed,logBloom,logs>]
安全模型
LayerZero的安全模型主要依赖两个方面:
- Oracle和Relayer是否可以共谋提供假的验证数据
- 验证代码的实现是否有bug
LayerZero目前采用ChainLink作为Oracle,ChainLink的Oracle安全性有多个级别,其中级别最高的是priceFeed,price由多个节点报价经过一定的算法产生,并且对错误报价有惩罚措施。而Any API的安全性就差多了,Any API一般是供应商自己运行节点提供数据,并没有多个数据源汇总(有时候也不现实,比如数据是独家的),Any API提供的数据主要看供应商的信誉度。区块头这种数据目前并没有经过ChainLink的特殊处理,获取途径应该是通过Any API。
Relayer可以由各个基于LayerZero构建的APP指定,这对用户而言Relayer的安全性是不透明的(Oracle的安全性对用户而言是透明的)。不同能力的项目方可以使用不同层级安全性的Relayer,有的可能使用一个私钥来控制Relayer,有的可能使用多签,有的可能使用联邦共识,安全性越高成本就会越高,对项目方的技术和管理能力要求就越高。
在验证逻辑这块LayerZero出过一个极其严重的bug(目前已经修复)。下面的代码是修复前的MPT验证逻辑:
function _getVerifiedLog(bytes32 hashRoot, uint receiptSlotIndex, uint logIndex, bytes[] memory proof, uint[] memory pointers) internal pure returns(ULNLog memory) { // walk and assert the hash links of MPT uint pointer; bytes memory proofBytes; for (uint i = 0; i < proof.length; i++) { proofBytes = proof[i]; require(hashRoot == keccak256(proofBytes), "LayerZero: invalid hashlink"); if (i < pointers.length) { pointer = pointers[i]; assembly { hashRoot := mload(add(add(proofBytes, pointer), 32)) } } } // 后面的代码略 }
这个版本的验证原理跟上文的一样,只不过在取hashRoot的时候可以出现越界,这里的越界是指取到的hashRoot不在proofBytes的范围内。在第一次for循环的时候relayer可以故意设置一个较大的pointer使得取到的hashRoot并不是MPT树中的正确的hash,然后再伪造后续的proof,这样就能绕过代码的MPT校验。
为了避免代码上可能有的漏洞导致被黑客攻击,relayer可以进行一种预提交,这种预提交其实就是将消息在ChainB链上模拟提交一遍,然后看看是否会出现非预期的严重后果。预提交是一种非常不错的防范措施,当年Poly Network被攻击就是relayer直接将能修改keeper的恶意交易发送到了链上,如果有预提交那么这种攻击就可能被拦截下来。