区块链互操作性是指链A与链B交互数据的能力。近年来区块链生态快速扩张,出现了大量具有不同属性的区块链网络,互操作性是区块链设计时的一个重要考虑指标。不具有互操作性,网络具有孤立于更大生态的风险,为此,激励了项目方研究和开发互操作性解决方案。每种互操作性解决方案具有不同的权衡和底层技术。本文由Polygon团队提供的解决方案,为Polygon zkEVM L2网络提供了原生的互操作性。
bridge为基础设施元素,允许L1与L2之间进行资产迁移和通信。从用户角度来看,bridge可在不改变资产数量或资产功能的情况下,将资产由网络A转移至网络B;bridge也可以在网络间发送data payload(即跨链消息传递)。
Polygon zkEVM这样的L2 rollups,其L2 State Transitions和交易的数据可用性 均由L1合约来保证,因此,若正确设计L2架构,可仅依赖于合约逻辑来同步bridge的两端,而不需要可信的链下relayer来跨网络同步bridge两端。需注意的是,本bridge方案必须在L2层包含相应的设计。
如图1所示,bridging interface为部署在L1和L2网络上的bridge合约,用户可用于:
bridge中包含了名为Global Exit Merkle Tree(GEMT)的默克尔树。在GEMT树中,每个叶子节点表示了特定网络的Exit Merkle Tree(EMT).GEMT树中仅包含了2个叶子节点,一个对应为L1 EMT root,另一个对应为L2 EMT root。GEMT树的结构如图2所示:
GEMT为固定只有2个叶子节点的常规默克尔树,而EMT为append only sparse Merkle Trees(SMT)且具有固定的depth(Polygon zkEVM中设计depth为32)。SMT为大量使用的默克尔树,可在链上高效使用——详情见附录A。
特定网络EMT的每个叶子节点,表示了从该网络往外 bridge某资产(或某资产的representative token)或 发送某消息 的意图。EMT每个叶子节点为如下参数的abi encoded packed structure的keccak256哈希值:
uint8 leafType
:0表示asset,1表示message。int32 originNetwork
:为原始资产所属的Origin Network ID。address originAddress
:若leafType=0
,则为Origin network token address,其中“0x000…0000”保留为ether token address;若leafType=1
,则为message的msg.sender。uint32 destinationNetwork
:为bridging的destination网络ID。address destinationAddress
:为目标网络中接收bridged asset的收款方地址。uint256 amount
:bridge的token或ether数量。bytes32 metadataHash
:为metadata哈希值。metadata将包含所转移资产信息或所转移message payload信息。一旦某leaf添加到EMT中,将计算新的EMT root,以及新的GEMT root。GEMT root将在网络间同步,使得可在对方网络中证明leaf inclusion,并完成bridge操作。
大多数bridge架构都使用在双方网络上的智能合约来实现。但是,为同步二者的GEMT,有一部分bridge逻辑必须与L2 State管理架构进行集成。因此,为理解本brIdge方案,还需要考虑L2 State管理中涉及到的链下角色——如Sequencer、Aggregator以及PolygonZkEVM.sol合约。
此外,bridge架构中还包含以下元素:
图3展示了详细的bridge架构,以及为实现bridge操作的finality,双方网络是如何交互的。具体分为2种bridge操作:
PolygonZkEVMBridge.sol合约为特定网络用户的bridging interface,因此在每个网络都有一个PolygonZkEVMBridge.sol合约。
PolygonZkEVMBridge.sol合约具有:
PolygonZkEVMBridge.sol合约目前有2种bridging函数:
bridgeAsset
函数bridgeMessage
函数默认L2网络的账号是没有ether来支付交易手续费的,当claiming源自L1的Asset或Message时,调用L2 bridge合约claiming函数的 L2 claiming交易可以不支付gas费,由polygon zkEVM协议来资助。
PolygonZkEVMBridge.sol合约目前有2种claiming函数:
claimAsset
函数claimMessage
函数bridgeAsset
函数用于向另一网络转移资产:
function bridgeAsset(
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
address token,
bool forceUpdateGlobalExitRoot,
bytes calldata permitData
)
bridgeAsset
函数参数有:
token
:为原始网络的ERC20 token地址,若为“0x0000…0000”,则意味着用户想要转移ether。destinationNetwork
:为目标网络的网络ID,必须不同于 调用本函数的所属网络ID,否则交易将被rever。destinationAddress
:目标网络接收所bridge token的收款方地址。amount
:bridge的token数量。permitData
:为具有EIP-2612 Permit扩展的ERC-20 token的 已签名permit data,用于改变某账号的ERC-20 allowance,并允许bridge合约将所bridged token转移给自身。
所bridge的资产类型有3种,对应的bridgeAsset
有3条可能的执行流:
(1)所bridged asset为ether。
bridgeAsset
函数的token
参数为“0x0000…0000”。amount
参数。originNetwork
参数将设置为L1网络ID。(2)所bridged asset为源自另一网络ERC20 token的representative ERC-20 token。
// Wrapped Token information struct
struct TokenInformation {
uint32 originNetwork;
address originTokenAddress;
}
bridgeAsset
函数的token
参数为wrappedTokenToTokenInfo map中的某key,则意味着所bridged token为源自另一网络ERC-20 token的representative ERC-20 token。amount
参数相应数量的token将被burn,bridge合约无需用户许可,有权burn相应的token。leaf中的originAddress
和originNetwork
参数将从wrappedTokenToTokenInfo map相应value中获取。(3)所bridged asset为源自本网络的ERC-20 token。
amount
参数相应数量的token将lock在bridge合约中。permitData
参数。originAddress
和originNetwork
参数分别为当前网络ID和ERC-20 token合约地址。meatadataHash
参数计算方式为:metadataHash = keccak256(
abi.encode(
IERC20MetadataUpgradeable(token).name(),
IERC20MetadataUpgradeable(token).symbol(),
IERC20MetadataUpgradeable(token).decimals()
)
)
对应在bridgeAsset
函数中的metadata具体表示为: // Encode metadata
metadata = abi.encode(
_safeName(token),
_safeSymbol(token),
_safeDecimals(token)
);
最后的仔细步骤则是相同的,与资产类型无关。剩余的leaf参数有:
leafType
参数:设置为0,表示资产。destinationNetwork
和destinationAddress
参数:根据调用bridgeAsset
函数的相应参数设置。bridgeAsset流程中:
BridgeEvent
事件bridgeMessage
函数用于向另一网络转移消息:
function bridgeMessage(
uint32 destinationNetwork,
address destinationAddress,
bool forceUpdateGlobalExitRoot,
bytes calldata metadata
)
bridgeMessage
函数的参数有:
destinationNetwork
:为目标网络的网络ID,必须不同于调用本函数所在的网络ID,否则交易将被revert。destinationNetwork
:为目标网络接收bridged message的接收地址。forceUpdateGlobalExitRoot
:标记是否更新新的global exit root。metadata
:为Message payload。bridgeMessage
函数将:
BridgeEvent
事件bridgeAsset
函数类似,调用GEMT合约更新new EMT rootbridgeMessage
函数与bridgeAsset
函数的主要差异在于:
leafType
参数为1orginAddress
和metadataHash
参数分别为msg.sender值和message payload的哈希值。bridgeMeesage
函数调用交易的msg.value中,在目标网络上接收消息的同时,可destinationAddress.call{value: amount}
获得相应的ether。claimAsset
函数用于claim源自另一网络bridge来的资产:
function claimAsset(
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProof,
uint32 index,
bytes32 mainnetExitRoot,
bytes32 rollupExitRoot,
uint32 originNetwork,
address originTokenAddress,
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
bytes calldata metadata
)
claimAsset
函数参数有:
smtProof
:为Merkle proof,即为验证该leaf所需的sibling nodes array。index
:为leaf index。mainnetExitRoot
:为包含该leaf的L1 EMT root。rollupExitRoot
:为包含该leaf的L2 EMT root。originNetwork
:为所bridge资产所属的原始网络ID。originTokenAddress
:为原始网络的ERC-20 token地址,若为0x0000…0000,则表示claIm的为ether资产。destinationNetwork
:为目标网络ID,即为调用claimAsset
函数所属的网络ID。destinationAddress
:为接收bridged token的收款方地址。amount
:所claim的token数量。metadata
:
claimAsset
函数所属网络的ERC-20 token,则metatdata值为0(metadata
参数设置为0x)。 // Encode metadata,bridgeAsset本网络ERC-20 token
metadata = abi.encode(
_safeName(token),
_safeSymbol(token),
_safeDecimals(token)
);
// claimAsset时,基于metadata构建相应的wrap token
// Get ERC20 metadata
(
string memory name,
string memory symbol,
uint8 decimals
) = abi.decode(metadata, (string, string, uint8));
// Create a new wrapped erc20 using create2
TokenWrapped newWrappedToken = (new TokenWrapped){
salt: tokenInfoHash
}(name, symbol, decimals);
claimAsset
函数会根据用户提供的参数来验证相应leaf的有效性。
为避免重放共计,需确保指定leaf仅能成功验证一次。PolygonZkEVMBridge.sol合约具有claimedBitMap map来存储每个已成功验证leaf index的nullifier bit,具体如图5所示:
为优化storage slots usage,claimedBitMap map中的每个条目会hold 256 nullifier bits for 256 already verified leaves。
认定 某指定leaf的merkle proof是有效的,需满足以下条件:
claimAsset
函数所属网络ID 一致。mainnetExitRoot
参数和rollupExitRoot
参数哈希获得的GEMT root结果,必须已存在于PolygonZkEVMGlobalExitRoot.sol 合约中。若该leaf验证成功,与该leaf index相应的claimedBitMap map中的bit将被nullified,后续的流程如图6所示:
与bridgeAsset
一致,claimAsset
根据资产类型不同,有3种可能的执行流程:
(1)所claimed asset为ether:
originTokenAddress
参数为“0x0000…0000”amount
参数对应数量的ether将发送到destinationAddress
参数对应的地址账号中。(2)所claimed asset为源自本网络的ERC-20 token:
originNetwork
参数对应 调用claimAsset
函数所属网络ID。amount
对应发送到destinationAddress
参数对应地址账号中的相应ERC-20 token数量。(3)所claimed asset为源自另一网络ERC-20 token的representative ERC-20 token:
create2
opcode,相应的salt为tokenInfoToWrappedToken map的key值。该salt值根据originNetwork
和originTokenAddress
参数计算而来: // The tokens is not from this network
// Create a wrapper for the token if not exist yet
bytes32 tokenInfoHash = keccak256(
abi.encodePacked(originNetwork, originTokenAddress)
);
amount
参数数量的token到destinationAddress
参数对应的账号中。create2
opcode以及之前计算的salt 来部署新的representative ERC-20 token合约。使用create2
opcode和指定的salt,可确定性的绑定 representative token的合约地址 与 origin network的origin token合约地址。部署成功后,可mint对应amount
参数数量的token到destinationAddress
参数对应的账号中,同时:
NewWrappedToken
事件。最终,无论是claim的是哪种资产,都会释放ClaimEvent
事件。
claimMessage
函数用于claim源自其它网络的message:
function claimMessage(
bytes32[_DEPOSIT_CONTRACT_TREE_DEPTH] calldata smtProof,
uint32 index,
bytes32 mainnetExitRoot,
bytes32 rollupExitRoot,
uint32 originNetwork,
address originAddress,
uint32 destinationNetwork,
address destinationAddress,
uint256 amount,
bytes calldata metadata
)
与claimAsset
函数类似,claimMessage
会验证用户给定的leaf,由于二者的leaf格式是一样的,因此这2个函数的参数也是一样的。
与claimAsset
函数一样,若leaf验证通过,通过设置相应leaf index对应在claimdBitMap map中的bit,来将相应leaf index nullified。
然后哦,底层会调用destinationAddress
参数:
// Execute message
// Transfer ether
/* solhint-disable avoid-low-level-calls */
(bool success, ) = destinationAddress.call{value: amount}(
abi.encodeCall(
IBridgeMessageReceiver.onMessageReceived,
(originAddress, originNetwork, metadata)
)
);
可看出,为调用onMessageReceived
函数设置了originAddress, originNetwork, metadata
参数,且若message中包含了ether,相应的call value设置为amount
参数。其中metadata
参数为message payload。
注意,messaging service可用于将ether转移给Externally owned accounts(EOAs),但是,EOAs无法解析消息,因此message payload对其不可用。
最终,若message发送成功,则会释放ClaimEvent
事件。
PolygonZkEVMGlobalExitRoot.sol合约为L1合约,可计算和存储每个new GEMT root。为确保所添加的每个leaf在未来都可被验证,需要存储每个GEMT root。名为globalExitRootMap的map用来存储所有已计算的GEMT roots。
L1 PolygonZkEVMBridge.sol合约在验证leaf时,会从globalExitRootMap map中获取GEMT roots。
L1 PolygonZkEVMGlobalExitRoot.sol合约中updateExitRoot
函数用于更新EMT roots并计算新的GEMT root,updateExitRoot
函数会 由L1 PolygonZkEVM.sol合约 或 由L1 PolygonZkEVMBridge.sol合约 调用:
updateExitRoot
函数,则将更新L2 EMT root。updateExitRoot
函数,则将更新L1 EMT root。function updateExitRoot(bytes32 newRoot) external
每个L2 state transition均由L1 PolygonZkEVM.sol合约固化(通过验证Aggregator提交的ZKP证明),通过调用L1 PolygonZkEVMGlobalExitRoot.sol合约的updateExitRoot
函数,将更新new L2 EMT root。
当有new leaf添加到L1 PolygonZkEVMBridge.sol合约的L1 EMT中时,通过调用L1 PolygonZkEVMGlobalExitRoot.sol合约的updateExitRoot
函数,将更新new L1 EMT root。
最终,都会释放UpdateGlobalExitRoot
事件。
L2 PolygonZkEVMGlobalExitRootL2.sol 合约 为部署在L2上的“特殊”合约。
【v1.1版本的bridge文档与当前的实现有出入】
Aggregator zkEVM node软件在执行完transactions batches时,可直接访问L2 PolygonZkEVMGlobalExitRootL2.sol 合约的lastRollupExitRoot storage slots。lastRollupExitRoot storage slots中存储的为L2 EMT root。
随后,该L2 EMT root会和L2 State transition proof一起,提交到L1 PolygonZkEVM.sol合约中。若该proof验证通过,则会更新L1 PolygonZkEVMGlobalExitRoot.sol合约中的new L2 EMT root,并计算new GEMT,从而使得L1 PolygonZkEVMBridge.sol合约可获得有效的GEMT roots来验证包含在aggregated batches中的user claim transactions。
sparse Merkle tree基础只是可参看:
sparse Merkle tree为具有intractable size的Merkle tree,可以以有效的方式处理。假设它几乎是空的,事实上,最初它是空的。当它中的所有叶子都具有相同的零(空)值时,它被认为是空的,由于这种假设,可以通过 log 2 ( n ) \log_2(n) log2(n)次哈希运算来计算root,其中 n n n是树上的叶子数量。请注意,而对于non-sparse tree,需要 2 n − 1 2n−1 2n−1次哈希运算才能计算root。在计算空树根的过程中,在每个level中,所有节点都将取相同的值,因此不需要计算每个level中的所有子树。当树的特定level的节点为空时,该节点的值都被命名为Zero Hash,并将表示具有 x x x个零叶子的subtree。
以3层(8个叶子)的empty sparse Merkle tree为例,相应的Zero H按时list为:
刨除预计算的ZH evaluations,当向3层SMT树插入一个新的叶子节点时,仅需要 1 + log 2 ( n ) 1+\log_2(n) 1+log2(n)次哈希运算。事实上,compute ZH on the fly 要比 从storage slots中读取预计算值 更gas efficient。new value将添加到SMT树的empty leaf中。
以向第0个空叶子节点插入value ( L 0 ) (L_0) (L0)为例,所需的哈希运算为:
当计算具有 n n n个叶子节点的Merkle root时,仅需要 1 + log 2 ( n ) 1+\log_2(n) 1+log2(n)次哈希运算,后续依次添加新的leaf,计算新root,所需的哈希运算次数也为 1 + log 2 ( n ) 1+\log_2(n) 1+log2(n)。
B x B_x Bx(Branch)为all the values of the already computed subtrees,其为在链上存储该树所需的storage slots。因此,在链上存储incremental Sparse Merkle tree所需的总storage slots数为 1 + log 2 ( n ) 1+\log_2(n) 1+log2(n)。
SMT的inclusion proof 与 常规Merkle tree的inclusion proof 验证操作都是一样的,仅需要 1 + log 2 ( n ) 1+\log_2(n) 1+log2(n)次哈希运算。此外,为生成inclusion proof,需知道树中已包含的所有叶子节点——由于已包含在transaction calldata或释放的event事件中,因此无需访问storage slots来获取。
总之,SMT为广泛使用的默克尔树,其处理效率高,且可用少量storage slots来存储。链上计算的效率高 意味着 更低的交易gas费,以及使用更少的storage。
[1] polygon zkEVM Technical Document Bridge v.1.1