Hacker Dojo
Optimistic Rollup基本概念
1、区块链扩容方案Rollup
有两种方法可以扩展区块链生态系统:
- 可以让区块链本身拥有更高的交易能力
- 可以改变使用区块链的方式
Rollup属于第二种方法,通过将计算移到链下,在链上保留每个交易的一些数据的方式来更快、更便宜地使用区块链,实现扩展。
Rollup的原理:在链上会存在一个智能合约,其中包含一个状态根(state root)。
任何人都可以发布一批汇总交易 (batch),这是一个经由高度压缩的交易集合,其中包含之前的状态根和处理交易之后的新默克尔根。合约会检查批处理中的前一个状态根是否与其当前的状态根相匹配,如果匹配则将状态根切换到新的状态根。
关于如何知道一个batch执行后提交的新root是正确的,存在着两种Rollups的解决方案:
- Optimistic rollup:利用欺诈证明(fraud proofs)
- ZK rollup:利用有效性证明(validity proofs)
2、Optimistic Rollup工作流程
OP Rollup中目前有2种类型的节点,sequencer(排序器)和verifier(验证器)。sequencer和verifier节点都运行L2Geth(由Optimism创建的Geth的轻微修改版本)。
当User提交一个L2 Transaction给sequencer时,sequencer处理这笔交易,将新的状态根提交给L1,同时其他L2节点在其L2副本上处理交易。为了确保L1新的状态是正确的,验证着节点会将自己的心状态根与sequencer提交的状态根进行比较。如果存在差异会开始欺诈证明,存在欺诈行为开始的state root均要被删除,Sequencer需要重新计算丢失的state root。
在L2提交交易:
- 用户将交易发送给sequencer节点,如果是有效交易,sequencer会立即将其添加到L2链中。L2的块大小仅1 个事务,因此能立即将一个新块与新事务一起添加到链中
- 然后在 sequencer 向L2链添加一些交易后,它将调用 L1 上的智能合约(CanonicalTransactionChain.sol)并将这些 L2 交易的交易数据与执行过交易后L2的新state roots
- L1 上的智能合约将交易数据和state roots存储在另一个智能合约中(StateCommitmentChain.sol)
- 交易数据存储在L1之后,validator节点将把交易包含在它们的 L2 链副本中。验证者也可以选择从L2同步,可以将直接从sequencer获得新交易
跨链交易:L1->L2
从L1到L2的交易速度很快,在这个过程中只需依靠sequencer将消息中继到L2链,用户需要将他们的交易数据发送到 L1 上的智能合约(L1StandardBridge.sol)。例如,如果用户想发送10 ETH到他们在L2上的地址,步骤如下:
- 用户向 L1 上的桥接合约(L1StandardBridge.sol)发送 10 ETH
- 合约将ETH锁定在L1上
- 合约将用户的交易添加到交易队列中(CanonicalTransactionChain.sol->enqueue)
- sequencer处理此交易,并将ETH存入用户的L2帐户。
跨链交易:L2->L1
用户会将交易发送到L2上的特定智能合约,然后中继器将读取它并将其发送到 L1。例如用户想将他们在 L2 上的 10 WETH 转移回他们 L1 地址上的 ETH,步骤如下:
- 用户将10 WETH发送到 L2 上的桥接合约(L2StanderBridger.sol)
- 桥接合约销毁WETH并将交易信息发送到另一个称为L2ToL1MessagePasser的智能合约。该智能合约记录了需要从L2发送到L1的交易数据。
- 中继节点从L2ToL1MessagePasser读取此交易数据,并等待欺诈证明窗口(7 天)完成,然后再将交易发送到 L1。
- 随后交易在L1上处理,用户可以从L1中当时锁定资产的桥接合约(L1StandardBridge.sol)中锁提取 ETH提取他们的ETH。
存储在L1上的状态:
由于每个交易的交易数据和结果状态根必须存储在L1上,因此最小化此数据的大小对于降低系统的存储成本至关重要。这就是术语“rollup”的由来。以下步骤解释了每个 L2 事务的数据如何存储在 L1 上:
- sequencer获取许多顺序L2事务的调用数据,并将它们组合成一个batch
- sequencer将这个batch发送到CanonicalTransactionChain合约
- 合约对每个交易的调用数据进行哈希处理,并创建这些哈希值的merkel tree
- CanonicalTransactionChain获取该批次的merkle根并将其发送到专门用于存储的智能合约中
3、欺诈证明
欺诈证明是Optimism系统的一个非常重要的部分,这可以保证sequencer的诚实性。
Optimistic Rollup对状态转移的保证是在一定时间窗口内,允许挑战者提交欺诈证明,在此期间任何人都可以对状态转换提出异议,并开始仲裁。仲裁有两种方式:
- 非交互式欺诈证明
- 如果sequencer将欺诈状态根发布到L1,则验证者节点可以启动欺诈证明并在L1上执行相应的L2交易
- Optimism OVM1.0
- 交互式欺诈证明
- 在L2上由双方进行多轮交互后锁定某条存疑指令后在 L1 仲裁
- Arbitrum/Optimism cannon(2022.3)
在 Optimism 实现中,一个L2区块只包含一笔交易,使得区块的 stateRoot 实际就是这笔交易的 stateRoot;即每笔交易都会有一个对应的状态被提交到 L1,因此它可以被单独挑战
结合代码分析
L1与L2上涉及的主要合约:
位置:optimism/packages/contracts/contracts
1、 L1->L2
在使用L2之前,用户首先需要在L1StandardBridge进行充值。用户通过四个deposit函数可以存入对应的ETH或ERC20代币到L1StandardBridge合约。
/**
* @inheritdoc IL1StandardBridge
*/
function depositETH(uint32 _l2Gas, bytes calldata _data) external payable onlyEOA {
_initiateETHDeposit(msg.sender, msg.sender, _l2Gas, _data);
}
/**
* @inheritdoc IL1StandardBridge
*/
function depositETHTo(
address _to,
uint32 _l2Gas,
bytes calldata _data
) external payable {
_initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
}
或存入ERC20 token:
/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20(
address _l1Token,
address _l2Token,
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) external virtual onlyEOA {
_initiateERC20Deposit(_l1Token, _l2Token, msg.sender, msg.sender, _amount, _l2Gas, _data);
}
/**
* @inheritdoc IL1ERC20Bridge
*/
function depositERC20To(
address _l1Token,
address _l2Token,
address _to,
uint256 _amount,
uint32 _l2Gas,
bytes calldata _data
) external virtual {
_initiateERC20Deposit(_l1Token, _l2Token, msg.sender, _to, _amount, _l2Gas, _data);
}
其中在_initiateETHDeposit函数中,设置了到L2的时候需要调用的函数IL2ERC20Bridge.finalizeDeposit
/**
* @dev Performs the logic for deposits by storing the ETH and informing the L2 ETH Gateway of
* the deposit.
* @param _from Account to pull the deposit from on L1.
* @param _to Account to give the deposit to on L2.
* @param _l2Gas Gas limit required to complete the deposit on L2.
* @param _data Optional data to forward to L2.
* solely as a convenience for external contracts. Aside from enforcing a maximum
* length, these contracts provide no guarantees about its content.
*/
function _initiateETHDeposit(
address _from,
address _to,
uint32 _l2Gas,
bytes memory _data
) internal {
// Construct calldata for finalizeDeposit call
bytes memory message = abi.encodeWithSelector(
IL2ERC20Bridge.finalizeDeposit.selector,
address(0),
Lib_PredeployAddresses.OVM_ETH,
_from,
_to,
msg.value,
_data
);
// Send calldata into L2
// slither-disable-next-line reentrancy-events
sendCrossDomainMessage(l2TokenBridge, _l2Gas, message);
// slither-disable-next-line reentrancy-events
emit ETHDepositInitiated(_from, _to, msg.value, _data);
}
sendCrossDomainMessage
函数触发L1CrossDomainMessenger.sol中的sendMessage
函数:
/**
* Sends a cross domain message to the target messenger.
* @param _target Target contract address.
* @param _message Message to send to the target.
* @param _gasLimit Gas limit for the provided message.
*/
// slither-disable-next-line external-function
function sendMessage(
address _target,
bytes memory _message,
uint32 _gasLimit
) public {
address ovmCanonicalTransactionChain = resolve("CanonicalTransactionChain");
// Use the CTC queue length as nonce
uint40 nonce = ICanonicalTransactionChain(ovmCanonicalTransactionChain).getQueueLength();
bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
_target,
msg.sender,
_message,
nonce
);
// slither-disable-next-line reentrancy-events
_sendXDomainMessage(ovmCanonicalTransactionChain, xDomainCalldata, _gasLimit);
// slither-disable-next-line reentrancy-events
emit SentMessage(_target, msg.sender, _message, nonce, _gasLimit);
}
首先,在这个函数中encodeXDomainCalldata
构造了在L2中对应的L2CrossDomainMessenger.sol合约中,会调用relayMessage
函数:
function encodeXDomainCalldata(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) internal pure returns (bytes memory) {
return
abi.encodeWithSignature(
"relayMessage(address,address,bytes,uint256)",
_target,
_sender,
_message,
_messageNonce
);
}
其次,在_sendXDomainMessag
内部调用了CanonicalTransactionChain合约的enqueue
函数:
/**
* Sends a cross domain message.
* @param _canonicalTransactionChain Address of the CanonicalTransactionChain instance.
* @param _message Message to send.
* @param _gasLimit OVM gas limit for the message.
*/
function _sendXDomainMessage(
address _canonicalTransactionChain,
bytes memory _message,
uint256 _gasLimit
) internal {
// slither-disable-next-line reentrancy-events
ICanonicalTransactionChain(_canonicalTransactionChain).enqueue(
Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER,
_gasLimit,
_message
);
}
CanonicalTransactionChain中的enqueue接收,经过了以下几个步骤:
- 首先,如果攻击者在提交L1->L2的交易时将_gasLimit设置的很大,然后在 L2 合约中消耗,可能会导致Sequencer性能下降。于是Optimism设置了阈值
enqueueL2GasPrepaid
,_gasLimit
超过阈值的部分合约将根据超出的大小按比例燃烧gas。 - msg.sender的说法:对于 L1->L2 消息,如果 msg.sender 为合约,那么在 L2 执行时,Origin 会被加上偏移。
uint160 constant offset = uint160(0x1111000000000000000000000000000000001111);
function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) {
unchecked {
l2Address = address(uint160(l1Address) + offset);
}
}
Call source | tx.origin |
---|---|
L2 user (Externally Owned Account) | The user’s address (same as in Ethereum) |
L1 user (Externally Owned Account) | The user’s address (same as in Ethereum) |
L1 contract (using CanonicalTransactionChain.enqueue) | L1_contract_address + 0x1111000000000000000000000000000000001111 |
- 将交易压入queueElements队列中
- 触发TransactionEnqueued事件
/**
* Adds a transaction to the queue.
* @param _target Target L2 contract to send the transaction to.
* @param _gasLimit Gas limit for the enqueued L2 transaction.
* @param _data Transaction data.
*/
function enqueue(
address _target,
uint256 _gasLimit,
bytes memory _data
) external {
require(
_data.length <= MAX_ROLLUP_TX_SIZE,
"Transaction data size exceeds maximum for rollup transaction."
);
require(
_gasLimit <= maxTransactionGasLimit,
"Transaction gas limit exceeds maximum for rollup transaction."
);
require(_gasLimit >= MIN_ROLLUP_TX_GAS, "Transaction gas limit too low to enqueue.");
// ...
if (_gasLimit > enqueueL2GasPrepaid) {
uint256 gasToConsume = (_gasLimit - enqueueL2GasPrepaid) / l2GasDiscountDivisor;
uint256 startingGas = gasleft();
require(startingGas > gasToConsume, "Insufficient gas for L2 rate limiting burn.");
// 消耗多余的gas
uint256 i;
while (startingGas - gasleft() < gasToConsume) {
i++;
}
}
// ..
address sender;
if (msg.sender == tx.origin) {
sender = msg.sender;
} else {
sender = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
}
bytes32 transactionHash = keccak256(abi.encode(sender, _target, _gasLimit, _data));
queueElements.push(
Lib_OVMCodec.QueueElement({
transactionHash: transactionHash,
timestamp: uint40(block.timestamp),
blockNumber: uint40(block.number)
})
);
uint256 queueIndex = queueElements.length - 1;
emit TransactionEnqueued(sender, _target, _gasLimit, _data, queueIndex, block.timestamp);
}
data-transport-layer会监听事件并存入数据库,由Sequencer查询TransactionEnqueued,转换为交易并挖出对应的区块。
在L2中的L2CrossDomainMessenger
合约中的relayMessage函数会对应的处理这个交易,首先对交易进行如下检查:
- 首先检查了msg.sender,msg.sender需要是L1CrossDomainMessenger
- 并且同一条消息之前没有处理过
- 保证消息的target不是
L2_TO_L1_MESSAGE_PASSER
,防止L2的消息又回环到L1
/**
* Relays a cross domain message to a contract.
* @inheritdoc IL2CrossDomainMessenger
*/
// slither-disable-next-line external-function
function relayMessage(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce
) public {
require(
AddressAliasHelper.undoL1ToL2Alias(msg.sender) == l1CrossDomainMessenger,
"Provided message could not be verified."
);
bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
_target,
_sender,
_message,
_messageNonce
);
bytes32 xDomainCalldataHash = keccak256(xDomainCalldata);
require(
successfulMessages[xDomainCalldataHash] == false,
"Provided message has already been received."
);
if (_target == Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER) {
// Write to the successfulMessages mapping and return immediately.
successfulMessages[xDomainCalldataHash] = true;
return;
}
xDomainMsgSender = _sender;
// slither-disable-next-line reentrancy-no-eth, reentrancy-events, reentrancy-benign
(bool success, ) = _target.call(_message);
// slither-disable-next-line reentrancy-benign
xDomainMsgSender = Lib_DefaultValues.DEFAULT_XDOMAIN_SENDER;
// Mark the message as received if the call was successful. Ensures that a message can be
// relayed multiple times in the case that the call reverted.
if (success == true) {
// slither-disable-next-line reentrancy-no-eth
successfulMessages[xDomainCalldataHash] = true;
// slither-disable-next-line reentrancy-events
emit RelayedMessage(xDomainCalldataHash);
} else {
// slither-disable-next-line reentrancy-events
emit FailedRelayedMessage(xDomainCalldataHash);
}
// Store an identifier that can be used to prove that the given message was relayed by some
// user. Gives us an easy way to pay relayers for their work.
bytes32 relayId = keccak256(abi.encodePacked(xDomainCalldata, msg.sender, block.number));
// slither-disable-next-line reentrancy-benign
relayedMessages[relayId] = true;
}
随后会调用到L2StandardBridge.sol中的finalizeDeposit
函数,在这个函数中最终实现mint
操作:
/**
* @inheritdoc IL2ERC20Bridge
*/
function finalizeDeposit(
address _l1Token,
address _l2Token,
address _from,
address _to,
uint256 _amount,
bytes calldata _data
) external virtual onlyFromCrossDomainAccount(l1TokenBridge) {
// Check the target token is compliant and
// verify the deposited token on L1 matches the L2 deposited token representation here
if (
// slither-disable-next-line reentrancy-events
ERC165Checker.supportsInterface(_l2Token, 0x1d1d8b63) &&
_l1Token == IL2StandardERC20(_l2Token).l1Token()
) {
// When a deposit is finalized, we credit the account on L2 with the same amount of
// tokens.
// slither-disable-next-line reentrancy-events
IL2StandardERC20(_l2Token).mint(_to, _amount);
// slither-disable-next-line reentrancy-events
emit DepositFinalized(_l1Token, _l2Token, _from, _to, _amount, _data);
}
...
}
2、L2->L1
当用户需要在L2上销毁一些代币并在L1上使用它们时,用户可以调用L2StandardBridge.sol中的withdraw函数:
function withdrawTo(
address _l2Token,
address _to,
uint256 _amount,
uint32 _l1Gas,
bytes calldata _data
) external virtual {
_initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _l1Gas, _data);
}
或用户或合约在L2调用入口函数L2CrossDomainMessenger.sendMessage()
:
/**
* Sends a cross domain message to the target messenger.
* @param _target Target contract address.
* @param _message Message to send to the target.
* @param _gasLimit Gas limit for the provided message.
*/
// slither-disable-next-line external-function
function sendMessage(
address _target,
bytes memory _message,
uint32 _gasLimit
) public {
bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
_target,
msg.sender,
_message,
messageNonce
);
sentMessages[keccak256(xDomainCalldata)] = true;
// Actually send the message.
// slither-disable-next-line reentrancy-no-eth, reentrancy-events
iOVM_L2ToL1MessagePasser(Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER).passMessageToL1(
xDomainCalldata
);
// Emit an event before we bump the nonce or the nonce will be off by one.
// slither-disable-next-line reentrancy-events
emit SentMessage(_target, msg.sender, _message, messageNonce, _gasLimit);
// slither-disable-next-line reentrancy-no-eth
messageNonce += 1;
}
sendMessage会进一步进一步调用OVM_L2ToL1MessagePasser.passMessageToL1(),它计算消息哈希并更新合约状态:
/**
* @inheritdoc iOVM_L2ToL1MessagePasser
*/
// slither-disable-next-line external-function
function passMessageToL1(bytes memory _message) public {
// Note: although this function is public, only messages sent from the
// L2CrossDomainMessenger will be relayed by the L1CrossDomainMessenger.
// This is enforced by a check in L1CrossDomainMessenger._verifyStorageProof().
sentMessages[keccak256(abi.encodePacked(_message, msg.sender))] = true;
}
batch-submitter会监听 L2 区块,进行两个操作:
- 打包批量交易 txBatch 提交到 L1
- 打包批量状态 stateBatch 提交到 L1
每隔几秒钟Sequencer就会检查接收到的新交易,将它们与所需的任何其他元数据分批汇总,然后利用CanonicalTransactionChain.appendSequencerBatch()函数进行提交。appendSequencerBatch没有任何参数。批次以紧密打包的格式提交,避免了ABI编码的低效率。
appendSequencerBatch函数:
- 首先计算批次头,然后计算其哈希值
- 然后计算出批次的上下文
- 然后将哈希值和上下文存储在存储器中(batchesRef)
[header, context1, context2, …, tx1, tx2, … ]
function appendSequencerBatch() external { // 通过这个函数L2的交易会打包成一个块作为大批量的交易交给L1处理
uint40 shouldStartAtElement;
uint24 totalElementsToAppend;
uint24 numContexts;
assembly {
shouldStartAtElement := shr(216, calldataload(4))
totalElementsToAppend := shr(232, calldataload(9))
numContexts := shr(232, calldataload(12))
}
...
_appendBatch(
blockhash(block.number - 1),
totalElementsToAppend,
numQueuedTransactions,
blockTimestamp,
blockNumber
);
// slither-disable-next-line reentrancy-events
emit SequencerBatchAppended(
nextQueueIndex - numQueuedTransactions,
numQueuedTransactions,
getTotalElements()
);
// Update the _nextQueueIndex storage variable.
// slither-disable-next-line reentrancy-no-eth
_nextQueueIndex = nextQueueIndex;
}
function _appendBatch(
bytes32 _transactionRoot,
uint256 _batchSize,
uint256 _numQueuedTransactions,
uint40 _timestamp,
uint40 _blockNumber
) internal {
IChainStorageContainer batchesRef = batches();
(uint40 totalElements, uint40 nextQueueIndex, , ) = _getBatchExtraData();
Lib_OVMCodec.ChainBatchHeader memory header = Lib_OVMCodec.ChainBatchHeader({
batchIndex: batchesRef.length(),
batchRoot: _transactionRoot,
batchSize: _batchSize,
prevTotalElements: totalElements,
extraData: hex""
});
emit TransactionBatchAppended(
header.batchIndex,
header.batchRoot,
header.batchSize,
header.prevTotalElements,
header.extraData
);
bytes32 batchHeaderHash = Lib_OVMCodec.hashBatchHeader(header); // 1
bytes27 latestBatchContext = _makeBatchExtraData(
totalElements + uint40(header.batchSize),
nextQueueIndex + uint40(_numQueuedTransactions),
_timestamp,
_blockNumber
);
// slither-disable-next-line reentrancy-no-eth, reentrancy-events
batchesRef.push(batchHeaderHash, latestBatchContext);
}
同时在sequencer执行L2->L1交易时,将修改OVM_L2ToL1MessagePasser状态树(sentMessages[slot]),因而进导致状态的变化,反应在区块头部的stateRoot字段。batch-submitter随后通过appendStateBatch函数打包批量状态到state batch:
/**
* @inheritdoc IStateCommitmentChain
*/
// slither-disable-next-line external-function
function appendStateBatch(bytes32[] memory _batch, uint256 _shouldStartAtElement) public {
// Fail fast in to make sure our batch roots aren't accidentally made fraudulent by the
// publication of batches by some other user.
require(
_shouldStartAtElement == getTotalElements(),
"Actual batch start index does not match expected start index."
);
// Proposers must have previously staked at the BondManager
require(
IBondManager(resolve("BondManager")).isCollateralized(msg.sender),
"Proposer does not have enough collateral posted"
);
require(_batch.length > 0, "Cannot submit an empty state batch.");
require(
getTotalElements() + _batch.length <=
ICanonicalTransactionChain(resolve("CanonicalTransactionChain")).getTotalElements(),
"Number of state roots cannot exceed the number of canonical transactions."
);
// Pass the block's timestamp and the publisher of the data
// to be used in the fraud proofs
_appendBatch(_batch, abi.encode(block.timestamp, msg.sender));
}
在_appendBatch函数中:
- Lib_MerkleTree.getMerkleRoot(_batch)计算这个batch的merkel root
- Lib_OVMCodec.ChainBatchHeader生成这个batch的header
- 然后存储batch到数组中
/**
* Appends a batch to the chain.
* @param _batch Elements within the batch.
* @param _extraData Any extra data to append to the batch.
*/
function _appendBatch(bytes32[] memory _batch, bytes memory _extraData) internal {
address sequencer = resolve("OVM_Proposer");
(uint40 totalElements, uint40 lastSequencerTimestamp) = _getBatchExtraData();
if (msg.sender == sequencer) {
lastSequencerTimestamp = uint40(block.timestamp);
} else {
// We keep track of the last batch submitted by the sequencer so there's a window in
// which only the sequencer can publish state roots. A window like this just reduces
// the chance of "system breaking" state roots being published while we're still in
// testing mode. This window should be removed or significantly reduced in the future.
require(
lastSequencerTimestamp + SEQUENCER_PUBLISH_WINDOW < block.timestamp,
"Cannot publish state roots within the sequencer publication window."
);
}
// For efficiency reasons getMerkleRoot modifies the `_batch` argument in place
// while calculating the root hash therefore any arguments passed to it must not
// be used again afterwards
Lib_OVMCodec.ChainBatchHeader memory batchHeader = Lib_OVMCodec.ChainBatchHeader({
batchIndex: getTotalBatches(),
batchRoot: Lib_MerkleTree.getMerkleRoot(_batch),
batchSize: _batch.length,
prevTotalElements: totalElements,
extraData: _extraData
});
emit StateBatchAppended(
batchHeader.batchIndex,
batchHeader.batchRoot,
batchHeader.batchSize,
batchHeader.prevTotalElements,
batchHeader.extraData
);
batches().push(
Lib_OVMCodec.hashBatchHeader(batchHeader),
_makeBatchExtraData(
uint40(batchHeader.prevTotalElements + batchHeader.batchSize),
lastSequencerTimestamp
)
);
}
在经过预定的欺诈证明期并与StateCommitmentChain核实后,任何人都可以调用relayMessage函数,并在L1上最终完成提款。同时relayMessage也是一个可以pause的函数。
function relayMessage(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce,
L2MessageInclusionProof memory _proof
) public nonReentrant whenNotPaused {
bytes memory xDomainCalldata = Lib_CrossDomainUtils.encodeXDomainCalldata(
_target,
_sender,
_message,
_messageNonce
);
// verify
require(
_verifyXDomainMessage(xDomainCalldata, _proof) == true,
"Provided message could not be verified."
);
...
}
- _verifyXDomainMessage函数负责验证L2->L1交易存在
- 证明 交易 存在 stateRoot 中
- 证明 stateRoot 存在 batchRoot中
/**
* Verifies that the given message is valid.
* @param _xDomainCalldata Calldata to verify.
* @param _proof Inclusion proof for the message.
* @return Whether or not the provided message is valid.
*/
function _verifyXDomainMessage(
bytes memory _xDomainCalldata,
L2MessageInclusionProof memory _proof
) internal view returns (bool) {
return (_verifyStateRootProof(_proof) && _verifyStorageProof(_xDomainCalldata, _proof));
}
_verifyStateRootProof:
- 验证是否在欺诈证明周期中
- verifyStateCommitment函数验证_proof.stateRoot, _proof.stateRootBatchHeader, _proof.stateRootProof之间的一致性
/**
* Verifies that the state root within an inclusion proof is valid.
* @param _proof Message inclusion proof.
* @return Whether or not the provided proof is valid.
*/
function _verifyStateRootProof(L2MessageInclusionProof memory _proof)
internal
view
returns (bool)
{
IStateCommitmentChain ovmStateCommitmentChain = IStateCommitmentChain(
resolve("StateCommitmentChain")
);
return (ovmStateCommitmentChain.insideFraudProofWindow(_proof.stateRootBatchHeader) ==
false &&
ovmStateCommitmentChain.verifyStateCommitment(
_proof.stateRoot,
_proof.stateRootBatchHeader,
_proof.stateRootProof
));
}
// StateCommitmentChain.sol
function verifyStateCommitment(
bytes32 _element,
Lib_OVMCodec.ChainBatchHeader memory _batchHeader,
Lib_OVMCodec.ChainInclusionProof memory _proof
) public view returns (bool) {
require(_isValidBatchHeader(_batchHeader), "Invalid batch header.");
require(
Lib_MerkleTree.verify(
_batchHeader.batchRoot,
_element,
_proof.index,
_proof.siblings,
_batchHeader.batchSize
),
"Invalid inclusion proof."
);
return true;
}
_verifyStorageProof验证
- 信息是由L2CrossDomainMessenger通过L2_TO_L1_MESSAGE_PASSER发送
- 并且与_proof一致。
/**
* Verifies that the storage proof within an inclusion proof is valid.
* @param _xDomainCalldata Encoded message calldata.
* @param _proof Message inclusion proof.
* @return Whether or not the provided proof is valid.
*/
function _verifyStorageProof(
bytes memory _xDomainCalldata,
L2MessageInclusionProof memory _proof
) internal view returns (bool) {
bytes32 storageKey = keccak256(
abi.encodePacked(
keccak256(
abi.encodePacked(
_xDomainCalldata,
Lib_PredeployAddresses.L2_CROSS_DOMAIN_MESSENGER // 1
)
),
uint256(0)
)
);
(bool exists, bytes memory encodedMessagePassingAccount) = Lib_SecureMerkleTrie.get(
abi.encodePacked(Lib_PredeployAddresses.L2_TO_L1_MESSAGE_PASSER),
_proof.stateTrieWitness,
_proof.stateRoot
);
require(
exists == true,
"Message passing predeploy has not been initialized or invalid proof provided."
);
Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.decodeEVMAccount(
encodedMessagePassingAccount
);
return
Lib_SecureMerkleTrie.verifyInclusionProof(
abi.encodePacked(storageKey),
abi.encodePacked(uint8(1)),
_proof.storageTrieWitness,
account.storageRoot
);
}
在relayMessage的剩余部分验证
- 之前没有relay过相同的交易
- 确保这个交易没有被Optimism阻止。
- target不是CanonicalTransactionChain,因此L2->L1消息不能被回送到L2
- 在调用前修改sender
- 触发合约调用
- 调用成功后标记为已经吊用
function relayMessage(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce,
L2MessageInclusionProof memory _proof
) public nonReentrant whenNotPaused {
...
bytes32 xDomainCalldataHash = keccak256(xDomainCalldata);
require(
successfulMessages[xDomainCalldataHash] == false, //1
"Provided message has already been received."
);
require(
blockedMessages[xDomainCalldataHash] == false, //2
"Provided message has been blocked."
);
require(
_target != resolve("CanonicalTransactionChain"), //3
"Cannot send L2->L1 messages to L1 system contracts."
);
xDomainMsgSender = _sender; //4
// slither-disable-next-line reentrancy-no-eth, reentrancy-events, reentrancy-benign
(bool success, ) = _target.call(_message); // 5
// slither-disable-next-line reentrancy-benign
xDomainMsgSender = Lib_DefaultValues.DEFAULT_XDOMAIN_SENDER;
// Mark the message as received if the call was successful. Ensures that a message can be
// relayed multiple times in the case that the call reverted.
if (success == true) {
// slither-disable-next-line reentrancy-no-eth
successfulMessages[xDomainCalldataHash] = true; // 6
// slither-disable-next-line reentrancy-events
emit RelayedMessage(xDomainCalldataHash);
} else {
// slither-disable-next-line reentrancy-events
emit FailedRelayedMessage(xDomainCalldataHash);
}
// Store an identifier that can be used to prove that the given message was relayed by some
// user. Gives us an easy way to pay relayers for their work.
bytes32 relayId = keccak256(abi.encodePacked(xDomainCalldata, msg.sender, block.number));
// slither-disable-next-line reentrancy-benign
relayedMessages[relayId] = true;
}
3、OVM1.0与OVM2.0
前面提到,欺诈证明存有两种实现方式:
- 非交互式欺诈证明
- 交互式欺诈证明
OVM1.0
由于OP Rollup依赖于欺诈证明,所以如果一条交易存在争议,就需要重放该交易以证明交易的结果是否正确,但是有些EVM操作码依赖于系统范围内的参数,这些参数可能会改变(例如存储状态或时间戳),这会导致交易在L1与L2间产生不同的行为。
因此Optimsim实现了OVM来处理L1上的L2争端的机制,这个机制保证可以重现在L1上执行L2事务时存在的 任何“上下文”,并且在理想情况下不引入太多开销。OVM是通过将上下文相关的EVM操作码替换为其对应的OVM操作码来实现的。
OVM2.0
cannon的欺诈证明解决方案:
Reference
- https://vitalik.ca/general/2021/01/05/rollup.html
- https://research.paradigm.xyz/rollups
- https://medium.com/privacy-scaling-explorations/an-introduction-to-optimisms-optimistic-rollup-8450f22629e8
- https://community.optimism.io/docs/developers/
- https://hackmd.io/mv0f3MiNSEi271ojKF3t9w#L2-to-L1-transactions
- https://godorz.info/2022/04/optimism-notes/#L2-gtL1