解析Optimism在代码层面是如何工作的
Optimism是一个建立在以太坊之上的乐观Rollup。什么是乐观Rollup?它又是如何在代码层面上工作的?这篇文章将解释。
我们还将介绍为什么Rollup需要链间通信以及这种通信是如何实现的。我们将看到实现rollup最重要功能的实际代码片断。
以下是这篇文章的大纲:
首先,什么是rollup?它是使以太坊更有效率的方法之一,通常被称为L2解决方案。有3种L2解决方案类型:状态通道、plasma和Rollup。我很快会有一篇关于 L2解决方案的分类
的文章,将详细介绍这个问题。下面是关于什么是Rollup,特别是乐观 Rollup的一个简短总结。
以太坊上有一个智能合约(称为RollupL1
),它允许ETH的存款/提款。当你的钱存入RollupL1
时,你可以认为它是在L2。L2的资金流动比L1的资金快得多,因为L2的交易更有效,更快。这一点是如何实现的呢?
还有一个在以太坊外的程序(称为RollupL2
)。它可以更快地处理交易,因为它不需要通过以太坊缓慢而昂贵的共识机制。它可以处理一堆交易,将它们合并(把它们卷在一起,即 Rollup)成一个批,并将该批次提交给RollupL1
。
RollupL2
可以是另一个更快的区块链上的智能合约,也可以是一个传统的web2服务器。每种方法都有优点和缺点,如延迟和去中心化。
通过在链外处理交易,你可以从2个方面更节省:
还有一个省力轴:你不需要在以太坊的每个交易之后计算新的状态。你看,当你直接在以太坊上提交一个交易,以太坊需要计算账户的新状态。这是很昂贵的。通过将这项工作转移到L2,你可以避免在以太坊上进行这种昂贵的计算。
但是,RollupL1
是否应该相信RollupL2
提交给它的新状态?它应该验证吗?如果它验证,它浪费了同样的计算,所以失去了Rollup的意义。
乐观 Rollup通过先信任来解决这个问题:他们只是相信新提交的状态而不做任何验证(他们是非常乐观的✨)。但是,他们将新提交的批次锁定一个星期(称为 挑战窗口
)。任何人都可以在这个挑战窗口期间提交数学证明,如果他们发现了欺诈性的状态更新,就可以获得奖励。如果该批次在这一周内没有争议,它就被认为是最终的。
奖励的资金来自于提交批次的人的质押金。如果你想提交批次交易,你需要提交一个质押金。
这就是乐观 Rollup工作的宏观描述。
从上层来看,乐观 Rollup需要3个的功能:
以下是实现上述功能的Optimism智能合约的图示:
来自Optimism 文档
现在让我们来看看最重要部分的实际代码:
这个桥的工作原理是在 L1 锁定资金,在 L2 铸造相应的数量。要提取资金,桥会销毁 L2 的资金并释放锁定的 L1 资金。
下面是存入资金的函数( L1StandardBridge.sol):
/**
* @title L1StandardBridge
* @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard
* tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits
* and listening to it for newly finalized withdrawals.
*
*/
contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {
function depositETHTo(
address _to,
uint32 _l2Gas,
bytes calldata _data
) external payable {
_initiateETHDeposit(msg.sender, _to, _l2Gas, _data);
}
/**
* @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. This data is provided
* 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);
}
// ... other functions (OMITTED)
}
该函数是L1StandardBridge合约的一部分,它部署在以太坊上。它非常简单:接受ETH(通过payable
关键字自动完成),将函数的所有参数编码为一条消息,并将消息发送到跨域信使。
跨域信使在L1和L2之间广播消息。我们将在稍后介绍它。
在L2上有一个相应的函数来监听这些消息。L2StandardBridge
合约就是这样做的。这个合约部署在一个独立的L2区块链上(比以太坊快)。
/**
* @title L2StandardBridge
* @dev The L2 Standard bridge is a contract which works together with the L1 Standard bridge to
* enable ETH and ERC20 transitions between L1 and L2.
* This contract acts as a minter for new tokens when it hears about deposits into the L1 Standard
* bridge.
* This contract also acts as a burner of the tokens intended for withdrawal, informing the L1
* bridge to release L1 funds.
*/
contract L2StandardBridge is IL2ERC20Bridge, CrossDomainEnabled {
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);
} else {
// ... handle error (OMITTED)
}
}
// ... other functions (OMITTED)
}
这个函数只是运行一些检查和铸造新的代币。我应该有提到,你可以使用这个桥来转账任意的ERC-20代币,而不仅仅是ETH(ETH只是被包裹在一个ERC-20接口中)。
同样,也有相应的功能用于将资金从L2转移到L1。也是用一个X域(x-domain)信使来完成。为了简洁起见,我将跳过它们。
L1和L2之间的通信是通过一个X域信使合约进行的(每个链上都有一个副本)。在内部,这个合约只是存储消息,并依靠 中继器
来通知其他链(L1或L2)有新消息。
由于没有原生的L1 ↔ L2通信。每一方都有
onNewMessage
这样的函数,中继器需要使用传统的web2 HTTPs来调用它们。
例如,下面是L1→L2交易如何在L1上存储/排队代码(CanonicalTransactionChain.sol):
/**
* @title CanonicalTransactionChain
* @dev The Canonical Transaction Chain (CTC) contract is an append-only log of transactions
* which must be applied to the rollup state. It defines the ordering of rollup transactions by
* writing them to the 'CTC:batches' instance of the Chain Storage Container.
* The CTC also allows any account to 'enqueue' an L2 transaction, which will require that the
* Sequencer will eventually append it to the rollup state.
*
*/
contract CanonicalTransactionChain is ICanonicalTransactionChain, Lib_AddressResolver {
uint40 private _nextQueueIndex; // index of the first queue element not yet included
Lib_OVMCodec.QueueElement[] queueElements;
/**
* 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 {
// ...a bunch of unimportant stuff omitted
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);
}
}
中继器会通知L2在队列中有一个新消息。
Optimism上有一个排序器,其工作是接受L2交易,检查其有效性,并将状态更新作为一个待定块应用到其本地状态。这些待处理区块会定期大批量地提交给以太坊(L1)进行最终确定性处理。
以太坊上接受这些批次的函数是appendSequencerBatch
,这是L1上CanonicalTransactionChain
合约的一部分。在内部,appendSequencerBatch
使用下面的函数来处理批次(CanonicalTransactionChain.sol):
/**
* @title CanonicalTransactionChain
* @dev The Canonical Transaction Chain (CTC) contract is an append-only log of transactions
* which must be applied to the rollup state. It defines the ordering of rollup transactions by
* writing them to the 'CTC:batches' instance of the Chain Storage Container.
* The CTC also allows any account to 'enqueue' an L2 transaction, which will require that the
* Sequencer will eventually append it to the rollup state.
*
*/
contract CanonicalTransactionChain is ICanonicalTransactionChain, Lib_AddressResolver {
/**
* Inserts a batch into the chain of batches.
* @param _transactionRoot Root of the transaction tree for this batch.
* @param _batchSize Number of elements in the batch.
* @param _numQueuedTransactions Number of queue transactions in the batch.
* @param _timestamp The latest batch timestamp.
* @param _blockNumber The latest batch blockNumber.
*/
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);
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);
}
}
batchesRef
是一个用于数据存储的辅助合约。那是存储批处理的地方。batchesRef
)。以后,哈希和上下文将被用来验证争议。
将交易卷成一个批次并提交的排序器 目前还是中心化的--由Optimism组织控制。但他们有计划在未来将这个角色去中心化。你也可以直接向CanonicalTransactionChain
提交你自己的批次,而不通过排序器,但这将是更昂贵的,因为提交批次的固定成本完全由自己支付,而不是在许多的交易中分摊。
在高层次上,争议的工作方式是提交一个状态更新无效的证明,并根据存储的状态更新(存储的批次的元数据:哈希和上下文)验证这个证明。
负责处理争议的合约是OVMFraudVerifier
。该合约是OVM - Optimism虚拟机的一部分(类似于EVM - 以太坊虚拟机)。以下是处理纠纷的主要函数:
contract OVM_FraudVerifier is Lib_AddressResolver, OVM_FraudContributor, iOVM_FraudVerifier {
/**
* Finalizes the fraud verification process.
* @param _preStateRoot State root before the fraudulent transaction.
* @param _preStateRootBatchHeader Batch header for the provided pre-state root.
* @param _preStateRootProof Inclusion proof for the provided pre-state root.
* @param _txHash The transaction for the state root
* @param _postStateRoot State root after the fraudulent transaction.
* @param _postStateRootBatchHeader Batch header for the provided post-state root.
* @param _postStateRootProof Inclusion proof for the provided post-state root.
*/
function finalizeFraudVerification(
bytes32 _preStateRoot,
Lib_OVMCodec.ChainBatchHeader memory _preStateRootBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _preStateRootProof,
bytes32 _txHash,
bytes32 _postStateRoot,
Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
Lib_OVMCodec.ChainInclusionProof memory _postStateRootProof
)
override
public
contributesToFraudProof(_preStateRoot, _txHash)
{
iOVM_StateTransitioner transitioner = getStateTransitioner(_preStateRoot, _txHash);
// ... a bunch of require statements omitted
// If the post state root did not match, then there was fraud and we should delete the batch
require(
_postStateRoot != transitioner.getPostStateRoot(),
"State transition has not been proven fraudulent."
);
_cancelStateTransition(_postStateRootBatchHeader, _preStateRoot);
// TEMPORARY: Remove the transitioner; for minnet.
transitioners[keccak256(abi.encodePacked(_preStateRoot, _txHash))] = iOVM_StateTransitioner(0x0000000000000000000000000000000000000000);
emit FraudProofFinalized(
_preStateRoot,
_preStateRootProof.index,
_txHash,
msg.sender
);
}
/**
* Removes a state transition from the state commitment chain.
* @param _postStateRootBatchHeader Header for the post-state root.
* @param _preStateRoot Pre-state root hash.
*/
function _cancelStateTransition(
Lib_OVMCodec.ChainBatchHeader memory _postStateRootBatchHeader,
bytes32 _preStateRoot
)
internal
{
iOVM_StateCommitmentChain ovmStateCommitmentChain = iOVM_StateCommitmentChain(resolve("OVM_StateCommitmentChain"));
iOVM_BondManager ovmBondManager = iOVM_BondManager(resolve("OVM_BondManager"));
// Delete the state batch.
ovmStateCommitmentChain.deleteStateBatch(
_postStateRootBatchHeader
);
// Get the timestamp and publisher for that block.
(uint256 timestamp, address publisher) = abi.decode(_postStateRootBatchHeader.extraData, (uint256, address));
// Slash the bonds at the bond manager.
ovmBondManager.finalize(
_preStateRoot,
publisher,
timestamp
);
}
}
finalizeFraudVerification
检查_postStateRoot
(由验证者提交)是否不等于由排序者提交的状态根。_cancelStateTransition
中删除该批次,并削减排序者的存款(为了成为一个排序者,你需要锁定一个存款。当你提交一个欺诈性的批次时,你的押金就会被削减,这笔钱就会给验证者,作为保持整个机制运行的激励)。这就是Optimism智能合约的要点解析。我希望你对以太坊和L2的未来感到乐观!
本翻译由 Duet Protocol 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!