LayerZero是一套全链互操作协议(Omnichain Interoperability protocol),所谓的全链互操作有两个特性:1、全链,不仅仅是EVM链,除了EVM链之外还有BTC这种UTXO链,Sonala这种非EVM链。2、互操作,代币的跨链仅仅是互操作的一种,在A链进行抵押然后在B链进行借贷也是一种互操作。
LayerZero是一套全链互操作协议(Omnichain Interoperability protocol),所谓的全链互操作有两个特性:
现有的跨链操作方式有这么几种:
显然中心化交易所是无法实现这互操作这个目标的,而侧链的安全性往往没有主链高,并且单就跨链互操作这个目标来说侧链显的比较笨重(需要部署一套独立的区块链,需要共识机制和发行代币)。目前市场接受度比较高的解决方案是跨链桥,桥的作用是指将一条链上的状态变动准确安全的通知到另外一条链上。比如XYZ是一个支持跨链的代币,Alice在Ethereum上销毁了100个XYZ,并且指定跨到BSC上去。当桥把Ethereum上发生的事情通知到BSC上的XYZ合约时,XYZ就给Alice铸造出对应数量的代币,这样就实现了代币跨链。
既然是传递消息,那么谁干这件事情就是其中的关键,比如某个中心化结构特别值得信任,那么大家可以让他来传递消息。WBTC就是这么由Bitcoin跨到Ethereum上的,中心化机构是BitGo。根据消息传递方式的不同,跨链桥可以分为
LayerZero就是一种基于MT算法的跨链解决方案,具体而言是由一方(Oracle)提供区块头,由独立的另一方(Relayer)提供交易证明,当区块头和交易证明能验证的时候跨链操作就可以执行。
在消息签名的方案中链上合约是完全相信签名消息的,而消息是否真实由签名方(不管是单方还是多方)来提供保证。LayerZero的方案中,消息需要经过合约验证,验证过程需要依赖两部分消息:Block和Proof。每个消息都可以进行多方签名,而且Block和Proof还需要经过MT算法校验。因此LayerZero的方案相比消息签名方案多了一个维度的安全性。
LayerZero协议的组成部分包括:
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。
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的安全模型主要依赖两个方面:
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的恶意交易发送到了链上,如果有预提交那么这种攻击就可能被拦截下来。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!