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的恶意交易发送到了链上,如果有预提交那么这种攻击就可能被拦截下来。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注