本文是 Onther 发布的 Optimism Bedrock Wrap-Up 系列的第三部分,深入探讨了 Optimism 中存款和取款的流程。
Onther 旨在为当前对 Optimism 和 Ethereum 生态系统的发展感兴趣的开发者提供有价值的信息。
你可以在这里查看本文的韩文版本。
图. Ethereum Coin旁边的 Optimism 插图 (来源:unplash)
这篇文章是“Optimism Bedrock Wrap-Up 系列”的第三部分,该系列由 Onther 计划的五篇文章组成。它系统地分解了存款和取款过程,通过逐步分析逐层揭示底层代码逻辑。
考虑到该系列的相互关联性,我们建议按顺序阅读文章,以获得连贯的理解。
系列 1. Bedrock 升级概述**:它提供了 Bedrock 升级、其组件以及在其层中部署的智能合约的概述。 Optimism Bedrock 总结系列 1
系列 2. Bedrock 升级以来的主要变化: 在本节中,我们的目标是阐明 Bedrock 升级引入的重大变化,为全面理解奠定基础,从而顺利阅读本系列接下来的部分。
Optimism Bedrock Wrap-Up Series 2 Bedrock 升级的主要变化
系列 3. 存款/取款流程分析: 我们将对存款/取款流程进行逐步分析,揭示其各层中的核心代码逻辑。
Optimism Bedrock Wrap Up Series 3 存款/取款流程
系列 4. 区块推导: 一旦在 Layer 2(Optimism 主网)上生成区块,系统就会启动一个过程,将这些区块 Roll-up 到 Layer 1。随后,在区块推导阶段,仅使用已 Roll-up 的数据重建 L2 区块。我们将提供详细的步骤指导,并在此过程中进行代码检查。
Optimism Bedrock Wrap-Up Series 4 分析区块推导流程
系列 5. Optimism Bedrock 组件的角色和行为: 作为本系列的最后一部分,我们将全面检查 Op-Batcher 和 Op-Proposer 的角色和操作逻辑。
Optimism Bedrock Wrap-Up Series 5 Optimism Bedrock 的基本组件的角色和行为
Optimism 中的存款和取款流程构成基本且关键的程序,是连接 L1 和 L2 的关键。存款是利用 Optimism 的第一步,使用户能够将资产从 L1 转移到 L2,从而使这些资产可以在 L2 环境中访问。另一方面,取款可确保将某人的资产从 L2 环境安全地返回到 L1,并由连贯且令人信服的证明支持。在本系列中,我们将仔细检查管理存款和取款过程中各层之间交互的代码。
在 Bedrock 版本中,“存款交易”包括所有由 L1 触发的 L2 交易和合约调用。
图. L1 到 L2 存款流程
在上图中,存款过程中调用的必要合约和函数是基于存款流程构建的。
①. 存款流程由 StandardBridges 发起和结束,支持 Lock & Mint 功能,用于 L1 和 L2 之间实际的资产转移。
②. L1CrossDomainMessenger 接收交易,调用 OptimismPortal 合约的 depositTransaction 函数,从而触发 TransactionDeposited 事件。
③. Op-node 组件监控 L1 交易信息,解析并向执行引擎 (Op-geth) 传递详细信息,以便在 OptimismPortal 中发生存款交易时在 L2 中执行存款交易。
④. 解析后的信息被传输到 L2CrossDomainMessenger,调用 relayMessage 函数,最终通过 L2StandardBridge 将资产传递给 L2 用户。
现在,让我们通过代码深入研究这个过程的复杂性。
①. L1StandardBridge 合约识别要存款的资产,锁定它们,并利用 L1CrossDomainMessenger 合约的 sendMessage 函数将存款交易中继到 L2。
图. L1StandardBridge.sol/depositERC20, depositETH (来源:github 链接)
资产的分类源于处理 ERC20 和 ETH 的存款过程的区别。每个路径都涉及调用独特的函数。这种差异始于资产锁定过程,其中 ERC20 资产锁定在 L1StandardBridge 上,而 ETH 资产锁定在 OptimismPortal 中。(请注意,Optimism 支持存款特定 ERC20 代币,这些代币分类为 MINTABLE_ERC20,你可以通过 链接 验证支持的代币)。
depositERC20 / depositETH 的参数:
由于_depositERC20_和_depositETH_函数调用的流程非常相似,因此我们不会区分这两种资产,并假设存款涉及 ETH。
在此之后,_initiateETHDeposit 函数调用 L1StandardBridge.sol 合约中的 _initiateBridgeETH。
图.StandardBridge.sol/_initiateBridgeETH (来源:github 链接)
messenger.sendMessage 参数
图. CrossDomainMessenger.sol/sendMessage (来源:github 链接)
sendMessage 参数
②. L1CrossDomainMessenger 使用 _sendMessage 函数调用 OptimismPortal 合约的 depositTransaction 函数,从而触发 TransactionDeposited 事件。
图. L1CrossDomainMessenger.sol/_sendMessage (来源:github 链接)
_sendMessage 参数
以下描述了_OptimismPortal_合约的depositTransaction。
图. OptimismPortal.sol/depositTransaction (来源:github 链接)
depositTransaction 函数由 OptimismPortal 提出,用于将存款消息传递到 L2。
depositTransaction 参数
现在,为了完成 L1 上的存款流程,OptimismPortal 触发 TransactionDeposited 事件。
上述步骤集中于从 L1 传输数据以在 L2 中执行交易。在随后的阶段,op-node 和执行引擎负责处理存款交易。在此阶段,op-node 通过存款交易构造 L2 区块的属性,启动新 L2 区块的创建,并促进其传输。
③ Op-node 负责解析到 L2 的存款交易,监听所有 L1 交易信息,并在交易发生在_OptimismPortal_中后立即将其解析。
在本课程中,分析代码级别发生的每个函数或方法会太多,因此我将根据关键逻辑的流程对其进行总结。
图. state.go/Driver (来源:github 链接)
Driver 在其 eventLoop() 中运行,接收对 op-node 的总体行为施加控制和命令的事件。这些指令包括启动或停止 L2 区块生成、获取 L1 区块的新区块头信息或确定区块类型等任务。
图. pipeline.go/Step (来源:github 链接), engine_queue.go/Step (来源:github 链接)
Step() 函数迭代 pipeline.go 和 engine_queue.go 中的函数。它通过管道添加新的 L1 区块数据,同时保持与执行引擎队列的同步。
随后,我们继续将 NextAttributes 添加到位于 batch 队列和引擎队列之间的属性队列。此队列负责将包含多个 L1 交易的 batch 转换为 payload 属性。转换后的 payload 属性随后被转发到引擎队列。此过程用作 payload 属性的模板创建,从而可以在特定 epoch 期间从 L1 交易生成 L2 区块。
图. sequencer.go/StartBuildingBlock (来源:github 链接)
创建 L2 区块的准备工作从 StartBuildingBlock 方法开始,该方法从 l1OriginSelector 对象获取 L1 区块信息。
在此之后,创建一个名为“fetchCtx”的context.Context对象,用于调用AttributesBuilder对象中的PreparePayloadAttributes方法。此时,我们着手准备 payload 属性,如下所示:
图. attributes_queue.go/AttributesBuilder(来源:github 链接), attributes.go/PreparePayloadAttributes(来源:github 链接)
AttributesBuilder 接口中定义的 PreparePayloadAttributes 方法被调用,并返回三个参数:ctx context.Context、l2Parent eth.L2BlockRef 和 epoch eth.BlockID. 调用多个函数来提取每个参数中包含的数据,其中关键函数是 DeriveDeposits 函数。
图. deposit.go/DeriveDeposits (来源:github 链接)
DeriveDeposits 函数将一个名为 receipts 的 *types.Receipt 指针切片和一个名为 depositContractAddr 的 common.Address 作为参数。为此,DeriveDeposit 函数首先调用 UserDeposits 函数,并以 receipts 和 depositContractAddr 作为参数,如下所示。
图. deposit.go/UserDeposits (来源:github 链接)
UserDeposits 迭代 L1 交易收据数组的日志字段,如果找到与 depositContractAddr 匹配的日志,则它会解组(解码)该收据,并将其作为输出切片添加到 *types.DepositTx。
通过这种方式,源自 OptimismPortal 的存款交易被识别并从源自 L1 的交易中提取。
一旦我们返回了 *types.DepositTx,我们就会返回到 DeriveDeposit 函数来执行剩余的步骤。
图. deposit.go/DeriveDeposits (来源:github 链接)
encodedTxs:使用一个 for 循环来连续获取 *types.DepositTx 值,并使用 types.NewTx(tx).MarshalBinary() 方法将它们编码为字节数组。如果此过程成功,则所有字节数组都存储在 opaqueTx 中,并且发生的任何错误都记录在 err 中。
在从 DeriveDeposits 函数获取所有必要的返回值后,我们返回到 PreparePayloadAttributes 函数,并通过返回一个指针 (*eth.PayloadAttributes ) 来完成它,该指针封装了制作新的 L2 区块所必需的 payload 属性。
在此之后,在 createNextAttributes 方法中,它正在为要创建的后续 L2 区块生成 payload 属性。
图. attributes_queue.go/createNextAttributes (来源:github 链接)
生成新的 payload 属性等同于将新队列附加到 AttributesQueue。AttributesQueue 中的 ‘builder’ 对象使用 ‘PreparePayloadAttributes’ 方法来获取后续 L2 区块的 payload 属性。随后,它将交易的数量和 batch 的时间戳记录到相应的 PayloadAttributes 对象,最终创建并返回一个新的 PayloadAttributes 对象。
在创建 PayloadAttributes 后,让我们返回到 sequencer 的 BuildingBlock 方法来完成区块创建。
图. sequencer.go/StartBuildingBlock (来源:github 链接)
使用 ConfirmPayload 方法来识别构建 L2 区块中的任何错误。如果未检测到任何错误,则继续在 *eth.ExecutionPayload 指针中记录已完成的 L2 区块。
重要的是要注意,sequencer 不会直接生成区块;相反,sequencer 和执行引擎通过引擎 API 协作来实现区块的创建。
图. engine_update.go/StartPayload (来源:github 链接)
StartPayload() 启动执行引擎中给定 payload 的构建。此函数运行 ForkchoiceUpdate(),它通过 engine_forkchoiceUpdatedV1 API 处理执行引擎 (op-geth) 中的交易并创建 L2 区块。我们将在本系列的第 4 部分中更详细地介绍这一点,因为它在本系列中涵盖的内容变得太多了。
④. 随着执行引擎处理存款交易,将调用_CrossDomainMessenger_的relayMessage。
图. CrossDomainMessenger.sol/relayMessage (来源:github 链接)
relayMessage 参数
如果中继消息没有错误,则使用中继的 calldata 将资金传递到实际目标地址。
图. CrossDomainMessenger.sol/relayMessage (来源:github 链接)
xDomainMsgSender = _sender
bool success = SafeCall.call(_target, gasleft() — RELAY_RESERVED_GAS, _value, _message)
最后,我们使用中继的存款信息调用 L2StandardBridge,并将其传递到要存款的帐户。
图. L2StandardBridge.sol/finalizeDeposit (来源:github 链接)
finalizeDeposit 函数采用上述六个参数并完成 L2 上的存款。该函数首先检查要存款的资产是 ETH 还是 ERC20,如果是 ETH,它会调用 finalizeBridgeETH 函数,并使用 _from、_to、_amount 和 _extraData 来完成存款,如果是 ERC20 代币,它会调用 finalizeBridgeERC20 函数,并使用 _l2Token, _l1Token, _from, _to, _amount,_ 和 _extraData 来完成存款。
这就是存款流程的工作方式,下一步是分析取款流程。
图. Optimism Bridge 取款进度捕获屏幕。(来源:optimism bridge)
当用户通过 Optimism Bridge 进行取款时,取款屏幕如上图所示,用户需要提交三个交易才能进行取款。
①. 取款发起交易: 这是用户在 L2 中发送第一个取款请求的交易。
②. 取款证明交易: 这是两阶段取款的第一步,用户在 L1 中向 OptimismPortal 提交 ‘proveWithdrawalTransaction’ 以证明取款是有效的。
③. 取款完成交易: 这对应于用户在完成最终确定期(以前在 Legacy 版本中称为 Challenge Period)后提交的 ‘finalizeWithdrawalTransaction’ 以收回资产。它标志着两阶段取款的最后一步,用户在其中声明他们的取款。
在第 2 系列中,我们对两阶段取款进行了简要概述。本系列从发起取款的一方的角度深入研究了取款过程的详细代码级分析。
①. 取款发起交易
图. 取款发起交易的函数路径
上图说明了从 L2 发起的取款交易的功能路径,就在将其传递到 L1 之前。
图. L2StandardBridge.sol/withdraw (来源:github 链接)
取款请求从部署在 L2 中的 L2StandardBridge.sol 的 withdraw 函数开始。此函数仅适用于从 L2 中提取可铸造的 ERC20 代币 (OptimismMintableERC20) 或 ETH。
取款 参数。
然后_withdraw函数调用_initiateWithdrawal_函数。
图. L2StandardBridge.sol/_initiateWithdrawal (来源:github 链接)
至关重要的是要强调,在 Bedrock 升级之前生成的 ETH 由一个名为 LEGACY_ERC20_ETH 的预部署合约管理。但是,在 Bedrock 升级之后,所有 ETH 都迁移到本机概念。因此,Bedrock 升级之前和之后生成的 ETH 应区别对待。
LEGACY_ERC20_ETH(0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000)
OPTIMISM_MINTABLE_ERC20_FACTORY(0x4200000000000000000000000000000000000012;)
如果 ETH 是在 Legacy 版本之前创建的,则通过 StandardBridge.sol 中的 _initiateBridgeETH 函数调用 _emitETHBridgeInitiated 函数来执行取款。如果 ETH 或 ERC20 代币是在 Bedrock 升级后创建的,则通过 StandardBridge.sol 中的 _initiateBridgeERC20 函数调用 _emitERC20BridgeInitiated 函数来执行取款。
由于_LEGACY_ERC20_ETH_已弃用,我们将假设我们提取的是在 Bedrock 升级后生成的 ETH。
图. StandardBridge.sol/_initiateBridgeERC20 (来源:github 链接)
在此之后,_initiateBridgeERC20 函数准备调用 _L2CrossdomainMessenger 的 sendMessage 函数。
__initiateBridgeERC20 参数_。
并且有一些检查:
一旦发出 ERC20BridgeInitiated 事件,系统就会准备通过调用 MESSENGER.sendMessage 来调用 sendMessage 函数。
图. StandardBridge.sol/MESSENGER.sendMessage (来源:github 链接)
仔细检查 MESSENGER.sendMessage 可以发现以下详细信息:
随后,调用 sendMessage 函数以将取款消息中继到 L1。
图. CrossDomainMessenger.sol/sendMessage (来源:github 链接)
sendMessage 函数采用三个参数。
由于 _sendMessage_ 是 L1 和 L2 跨域 messenger 使用的通用函数,因此我们随后再次调用 L2CrossDomainMessenger 的 _sendMessage。在此之后,_sendMessage 触发 L2ToL1MessagePasser 中的 initiateWithdrawal 函数,该函数从当前状态检索原始 withdrawal 字段的 withdrawalHash 值。
图.L2ToL1MessagePasser.sol/initiateWithdrawal (来源:github [链接](https://github.com/ethereum-optimism/optimism/blob/62c7f既然在 L2 中发起的提款交易已经由 proposer 提交给 L2OutputOracle,那么后续步骤包括将“证明”与消息一起提交给 OptimismPortal。在这里,用户将生成证明所需的必要输入值从 L2 传达给 relayer。然后,relayer 生成证明并将其提交给 OptimismPortal。
图. OptimismPortal.sol/proveWithdrawalTransaction(来源:github 链接),Types.sol/WithdrawalTransaction,OutputRootProof(来源:github 链接)
proveWithdrawalTransaction() 或“证明”提款,包含四个参数:
虽然我们已经概述了“证明”由这四个参数组成,但问题是:如何从不同的位置获取信息?为了解决这个问题,Optimism 提供了在 L2 输出汇总到 L1 后,通过桥接服务直接调用 proveMessage() 的功能。
图. sdk/src/cross-chain-messenger.ts/proveMessage(来源:github 链接)
proveMessage 函数接收并存储创建证明所需的必要数据,包括以下组件:
图. cross-chain-messenger.ts/proveMessage, getBedrockMessageProof(来源:github 链接)
① 为了生成 proof,我们利用 getMessageBedrockOutput 来查询和检索提交给 L2OutputOracle 的输出根。
② 接下来,我们开始创建 Withdrawal。这涉及生成带有多个参数的 stateTrieProof。这些参数包括与提款的插槽位置相对应的插槽哈希值、messagePasserStorageRoot(L2ToL1MessagePasser 合同的存储根)和提款哈希。”
一旦以如上所述的方式获得所有必要的提款信息,就会组装以下参数组。
图. cross-chain-messenger.ts/proveMessage(来源:github 链接), OptimismPortal.sol/proveWithdrawalTransaction(来源:github 链接)
因此,我们通过调用 OptimismPortal 中的 proveWithdrawalTransaction 函数来结束该过程,并提供上述四个参数组。
图. OptimismPortal.sol/getL2Output(来源:github 链接)
在 proveWithdrawalTransaction() 函数的后续步骤中,执行一个程序来验证准备好的证明的输出根。为了获得提款交易的输出根,使用 L2OutputIndex 函数从 L2OutputOracle 合同中检索特定的输出根。然后,系统验证此输出根是否与作为参数接收的输出根一致,从而表明验证过程已完成。
图. Types.sol 中着色的 OutputRootProof 参数。(来源:github 链接)
花一点时间检查 OutputRootProof,输出根本质上是一个包含版本和 payload 信息的哈希。反过来,payload 包括诸如 state_root、withdrawal_storage_root, 和 latest_block_hash 等组件。
图. OptimismPortal.sol/provenWithdrawal(来源:github 链接)
最终,该证明被添加到名为 provenWithdrawal 的 mapping 变量中,从而触发一个事件,表明已为相应的提款提交了证明。
图. Optimism 桥提款进度捕获屏幕(来源:optimism 桥)
提交提款证明后,7 天的最终确定期开始。在此期间之后,用户需要亲自发起提款声明。在这种情况下,用户可以通过桥接服务执行 finalizeMessage() 函数来最终确定他们的提款。在内部,finalizeMessage() 在 OptimismPortal 中调用 finalizeWithdrawalTransaction()。
③. 提款最终确定交易
图. 提款确认交易:在确认期后声明提款的过程。
这是最终确定提款的最后一步。 finalizeWithdrawalTransaction 使用提款哈希作为标识符,获取在证明过程中创建的 ProvenWithdrawal。
图. OptimismPortal.sol/finalizeWithdrawalTransaction(来源:github 链接)
此后,代码检查 provenWithdrawal.timestamp 以确定该特定证明的最终确定期(在主网上为 7 天)是否已结束。值为 0 表示最终确定期仍在进行中,而时间戳在最终确定期过去后分配。
还执行了几个额外的检查,包括验证当前证明中的 provenWithdrawal.outputroot 与 L2OutputOracle 中提款的输出根之间的匹配。如果所有这些检查都成功通过,则可以通过调用 CrossDomainMessenger 的 relayMessage 函数来执行 ETH 的提款。
图. OptimismPortal.sol/SafeCall.callWithMinGas(来源:github 链接), CrossDomainMessenger.sol/relayMessage(来源:github 链接), StandardBridge.sol//finalizeBridgeETH(来源:github 链接)
relayMessage 采用发送者的地址、提取 ETH 的地址和要提取的 ETH 金额作为参数,并调用 StandardBridge 的 finalizeBridgeETH,这是 L2 生成的提款消息的目标函数。然后,在 finalizeBridgeETH 函数中,铸造 L2 烧毁的金额并将其发送到 L1 用户地址以完成提款过程。
到目前为止,我们已经尽可能在代码级别上探索了 Bedrock 升级后存款-提款流程的流程。特别是,检索和交叉验证在提款过程中生成证明所需的组件(例如提款信息、区块详细信息和输出根值)所涉及的步骤,提出了一个具有挑战性的旅程。理解已实施的代码本身就是一个挑战,我不禁佩服为研究和开发如此复杂的过程所做的努力。
在接下来的系列中,我们将分析区块推导过程,其中提交给 L1 的 batcher 交易被转换回 L2 区块。
<https://static.optimism.io/optimism.tokenlist.json>
Proxy | Address 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed | Etherscan
optimism/packages/contracts-bedrock/contracts/universal/StandardBridge.sol
optimism/op-node/rollup/driver/state.go at develop · ethereum-optimism/optimism
optimism/op-node/rollup/derive/engine_queue.go at develop · ethereum-optimism/optimism
optimism/op-node/rollup/driver/sequencer.go at develop · ethereum-optimism/optimism
optimism/op-node/rollup/derive/attributes_queue.go at develop · ethereum-optimism/optimism
optimism/op-node/rollup/derive/engine_update.go at develop · ethereum-optimism/optimism
optimism/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol at develop ·…
- 原文链接: medium.com/tokamak-netwo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!