使用 OP Stack 创建你自己的 L2 Rollup 测试网:分步指南
本文档详细介绍了如何使用 OP Stack 框架搭建一个以太坊等效的 Layer 2 Rollup。

1. 介绍
在快速增长的以太坊生态系统中,Layer 2 rollups(二层Rollup) 已经成为扩展的最有效方法——减少 gas 费用、提高吞吐量,并为 dApp 释放新的可能性。
但对于大多数开发者来说,建立自己的 rollup 总是让人望而却步。
大量的样板代码。
太多的活动部件。
很少有关于底层发生情况的可视性。
这正是 OP Stack 所解决的问题。
OP Stack 是一个 模块化和生产级别的框架,用于构建与以太坊等效的 rollups。它是 Optimism 主网的动力——但它也适用于 任何人 使用和 自定义。
如果你曾经想:
- 了解像 Optimism 这样的 rollups 真正是如何工作的,
- 部署你自己的 L2 链用于测试或实验,
- 或者只是更深入地研究以太坊基础设施。
本指南就是为你准备的。
2. 快速概览:OP Stack 组件
在我们开始实践之前,让我们花点时间了解 OP Stack rollup 的核心构建块。
正如以太坊有一个共识客户端(例如 Lighthouse、Prysm)和一个执行客户端(例如 Geth、Nethermind)一样,OP Stack 也有类似的组件——但专为运行 Layer 2 链而设计。
以下是一个快速概览:
1. 执行客户端 → op-geth
执行客户端负责:
- 执行智能合约
- 维护链的状态
- 处理以太坊 JSON-RPC 接口
在 OP Stack 中,执行客户端是 Geth 的一个分支,称为 op-geth。
可以将其视为你的 L2 链的“EVM 大脑”。
2. 共识客户端 → op-node
共识客户端负责:
- 跟踪 L1 区块(在我们的例子中是以太坊 Sepolia)
- 使用 Engine API 与执行客户端协调
- 根据 L1 的输入确定规范的 L2 链
这由 op-node 处理——你的 rollup 的核心驱动程序。
它监视以太坊 L1、处理 L2 输入,并驱动 op-geth 生成 L2 区块。
3. Batcher → op-batcher
一旦你的 rollup 产生区块,你需要将 这些区块发布到以太坊 L1。
这就是 batcher 的工作。
batcher:
- 从
op-geth收集 L2 交易数据 - 将其发布到 L1 的一个特殊合约中
- 确保数据的可用性和可验证性
这就是使 rollups 安全的原因:数据存在于以太坊上。
4. Proposer → op-proposer
最后一个组件是 proposer。
它负责:
- 将你的 L1 链的 状态根 提交到 L1
- 更新 Output Oracle 智能合约
- 允许从 L2 提款到 L1
没有 proposer,你的链无法最终确定。
对于测试来说,它是可选的——但对于一条活跃的链来说,它是必不可少的。
它们如何协同工作
这是一个简化的工作流程:
op-node监视 Sepolia 上的新 L1 区块- 它指示
op-geth产生 L2 区块 op-batcher收集这些区块并将它们发布到 L1op-proposer将最终状态发布到 L1 以进行提款
所有这些组件协同工作,以保持你的 L2 链运行——去中心化、可扩展和 trust-minimised。
3. 设置环境
既然你了解了主要组件,那么是时候卷起袖子,设置你启动自己的 L2 链所需的一切了。
我们将首先克隆相关的存储库、安装依赖项并准备核心二进制文件。
克隆 Optimism Monorepo
首先,克隆主要的 Optimism Monorepo,其中包含 OP Stack 源代码。
git clone <https://github.com/ethereum-optimism/optimism.git>
进入克隆的目录:
cd optimism
安装 Node.js 依赖项
Optimism Monorepo 使用 yarn 来管理 Node.js 包。
安装所有必要的依赖项:
yarn install
注意:在继续之前,请确保你已安装 Node.js 和 yarn。你可以使用以下命令安装 yarn:
npm install --global yarn
构建核心二进制文件
安装依赖项后,你需要构建一些重要的二进制文件:
make op-node op-batcher op-proposer
这将编译并准备:
op-node(共识客户端)op-batcher(Batcher 服务)op-proposer(Proposer 服务)
所有这些二进制文件都将在你的 Optimism 目录中的 ./bin 文件夹中可用。
克隆执行客户端(op-geth)
执行客户端(修改后的 Geth)位于一个 单独的存储库 中。
克隆它:
git clone <https://github.com/ethereum-optimism/op-geth.git>
进入 op-geth 目录:
cd op-geth
并构建 geth 二进制文件:
make geth
这将生成一个 build/bin/geth 二进制文件,你将使用它来运行你的 L2 执行客户端。
4. 设置钱包和环境变量
在我们启动链的任何部分之前,我们需要用于不同角色的 特殊钱包——Admin、Batcher、Proposer 和 Sequencer。
我们还将准备一个 .envrc 文件,以便轻松地将这些凭据加载到我们的 shell 中。
让我们一步一步地进行。
生成钱包
在克隆的 optimism 目录中,运行钱包生成脚本:
./packages/contracts-bedrock/scripts/getting-started/wallets.sh
此脚本将生成:
- Admin 账户(用于部署)
- Batcher 账户(用于发布交易批次)
- Proposer 账户(用于发布状态根)
- Sequencer 账户(用于区块生产)
复制钱包详细信息
输出将如下所示:
Copy the following into your .envrc file:
## Admin address
export GS_ADMIN_ADDRESS=0x...
export GS_ADMIN_PRIVATE_KEY=0x...
## Batcher address
export GS_BATCHER_ADDRESS=0x...
export GS_BATCHER_PRIVATE_KEY=0x...
## Proposer address
export GS_PROPOSER_ADDRESS=0x...
export GS_PROPOSER_PRIVATE_KEY=0x...
## Sequencer address
export GS_SEQUENCER_ADDRESS=0x...
export GS_SEQUENCER_PRIVATE_KEY=0x...
资助你的钱包
由于我们要在 Sepolia 测试网上部署,你需要一些 Sepolia ETH 来资助你的钱包。
建议的资助金额:
- Admin — 0.5 Sepolia ETH
- Batcher — 0.1 Sepolia ETH
- Proposer — 0.2 Sepolia ETH
- Sequencer — 不需要 ETH(它不发送交易)
你可以从公共水龙头获取 Sepolia ETH,或者向朋友/测试网提供商询问。
安装和设置 direnv
为了自动加载这些钱包变量,我们将使用一个名为 direnv 的工具。
安装它:
brew install direnv
创建 .envrc 文件
在你的 optimism 文件夹中,创建一个名为 .envrc 的文件。
将导出的环境变量粘贴到其中,如下所示:
## Admin account
export GS_ADMIN_ADDRESS=0xYourAdminAddress
export GS_ADMIN_PRIVATE_KEY=0xYourAdminPrivateKey
## Batcher account
export GS_BATCHER_ADDRESS=0xYourBatcherAddress
export GS_BATCHER_PRIVATE_KEY=0xYourBatcherPrivateKey
## Proposer account
export GS_PROPOSER_ADDRESS=0xYourProposerAddress
export GS_PROPOSER_PRIVATE_KEY=0xYourProposerPrivateKey
## Sequencer account
export GS_SEQUENCER_ADDRESS=0xYourSequencerAddress
export GS_SEQUENCER_PRIVATE_KEY=0xYourSequencerPrivateKey
## L1 Sepolia RPC URL
export L1_RPC_URL="<https://eth-sepolia.g.alchemy.com/v2/your-api-key>"
## Chain IDs and Block Times
export L1_CHAIN_ID="11155111"
export L2_CHAIN_ID="420"
export L1_BLOCK_TIME=12
export L2_BLOCK_TIME=2
确保将 your-api-key 替换为你的真实的 alchemy 或 infura endpoint for sepolia。
加载环境
一旦 .envrc 准备就绪,告诉 direnv 加载它:
direnv allow
如果一切正常,你应该看到类似以下的输出:
direnv: loading .envrc
direnv: export +GS_ADMIN_ADDRESS +GS_ADMIN_PRIVATE_KEY +... (and so on)
如果你没有看到这个,请确保你的 .envrc 文件格式正确,然后再次尝试 direnv allow。
5. 配置 Rollup
现在你已经准备好了你的环境和钱包,接下来是设置你的 L2 rollup 的实际配置。
这个配置定义了重要的参数,例如区块时间、链 ID、地址和其他系统设置。
我们将自动生成一个基本配置,你可以在以后根据需要自定义它。
导航到 contracts-bedrock 包
从你的 optimism monorepo 根目录,移动到 contracts-bedrock 包:
cd packages/contracts-bedrock
安装 Foundry 依赖项
如果你还没有安装 Foundry,请先安装它:
curl -L <https://foundry.paradigm.xyz> | bash
foundryup
在 contracts-bedrock 包中,安装依赖项:
forge install
这将安装智能合约部署和测试所需的所有必要库。
生成 getting-started.json 配置
运行提供的配置脚本:
./scripts/getting-started/config.sh
这将创建一个新文件:
deploy-config/getting-started.json
此文件将包含基于你的环境变量的所有基本链配置。
示例:getting-started.json 的样子
这是一个简化的例子:
{
"l1ChainID": 11155111,
"l2ChainID": 420,
"l2BlockTime": 2,
"l1BlockTime": 12,
"maxSequencerDrift": 600,
"sequencerWindowSize": 3600,
"channelTimeout": 300,
"p2pSequencerAddress": "0xYourSequencerAddress",
"batchSenderAddress": "0xYourBatcherAddress",
"l2OutputOracleProposer": "0xYourProposerAddress",
"l2OutputOracleChallenger": "0xYourAdminAddress",
...
}
6. 使用 op-deployer 部署 L1 合约
现在你的 rollup 配置已准备就绪,是时候将 ** essential OP Stack 合约** 部署到 Sepolia 测试网 了。
这是一个关键步骤——这些合约构成了你的 rollup 系统的支柱,包括桥、预言机和系统配置。
我们将使用 op-deployer 工具来处理这个过程。
什么是 op-deployer?
op-deployer 是 Optimism 团队构建的命令行工具,使部署 OP Stack 合约变得简单且可重复。
它处理:
- 合约部署
- Genesis 和 rollup 配置生成
- 链初始化步骤
使用 op-deployer 还可以确保你的部署遵循 OP Stack 标准,如果你想稍后加入 Superchain(超级链),这将使其具有面向未来的能力。
步骤 1:安装 op-deployer
如果你尚未安装它:
克隆 repo:
git clone <https://github.com/ethereum-optimism/optimism.git>
(如果你之前已经克隆了它,则无需再次克隆。)
在你的 optimism 目录中,当你运行 make 时,应该已经构建了 op-deployer 二进制文件。
如果没有,你可以手动构建它:
cd optimsim/op-deployer
make
确保 bin/op-deployer 存在。
步骤 2:初始化 .deployer 目录
现在,我们将创建一个工作目录,op-deployer 将在其中管理部署状态。
从 optimism 根目录 中,运行:
./bin/op-deployer init --l1-chain-id 11155111 --l2-chain-ids 420 --workdir .deployer
11155111→ Sepolia L1 链 ID420→ 我们的 rollup 的 L2 链 ID.deployer→ 将存储部署配置和状态文件的目录
此命令创建:
.deployer/intent.json(你的部署配置).deployer/state.json(部署后填充)
步骤 3:自定义你的意图文件
你可以打开 .deployer/intent.json 来调整设置,例如:
- 所有者地址
- 费用接收者地址
- 治理设置
目前,默认设置对于测试网 rollup 来说是可以的。
步骤 4:将合约部署到 L1
现在是真正的操作:
运行此命令以将合约部署到 Sepolia:
./bin/op-deployer apply --workdir .deployer \\
--l1-rpc-url $L1_RPC_URL \\
--private-key $GS_ADMIN_PRIVATE_KEY
$L1_RPC_URL→ 你的 Sepolia RPC URL(Alchemy、Infura 等)$GS_ADMIN_PRIVATE_KEY→ 你之前生成的 Admin 钱包的私钥
此步骤中发生了什么?
- 诸如 L1StandardBridge、SystemConfig、OutputOracle 等合约已部署在 Sepolia 上
- 部署程序将所有已部署的合约地址记录到
.deployer/state.json中 - 部署流程遵循你先前创建的配置
如果一切正常,你将看到带有已部署地址的成功消息。
如果出现问题(例如“gas 不足”错误),请检查:
- Admin 账户上的资金余额
- 已加载正确的环境变量
步骤 5:结果——已部署状态
成功部署后,你将拥有:
.deployer/state.json——合约地址、部署数据.deployer/目录 ——完整的部署快照
我们将在后续步骤中使用此数据来 生成 genesis 并 初始化 L2 节点。
7. 生成 Genesis 和 Rollup 配置文件
现在你的智能合约已部署在 Sepolia 上,是时候 生成你的链实际运行所需的核心配置文件 了:
genesis.json→ 用于执行客户端(op-geth)rollup.json→ 用于共识客户端(op-node)
这两个文件都 至关重要。
如果没有它们,你的客户端将不知道从哪个链开始。
让我们创建它们。
步骤 1:生成 genesis.json
genesis.json 文件定义了你的执行客户端的 初始状态。
它告诉 op-geth:
- 第一个区块是什么样的
- 系统账户是谁
- 启动时应用什么设置
要生成它,请运行:
./bin/op-deployer inspect genesis --workdir .deployer <L2_CHAIN_ID> > .deployer/genesis.json
在我们的例子中:
./bin/op-deployer inspect genesis --workdir .deployer 420 > .deployer/genesis.json
此命令从 .deployer/state.json 读取并生成 .deployer/genesis.json。
步骤 2:生成 rollup.json
rollup.json 文件定义了你的共识客户端的 rollup 配置。
它告诉 op-node:
- 要在 L1 上监视哪些合约
- 如何验证区块
- 时间参数和安全假设
要生成它,请运行:
./bin/op-deployer inspect rollup --workdir .deployer <L2_CHAIN_ID> > .deployer/rollup.json
在我们的例子中:
./bin/op-deployer inspect rollup --workdir .deployer 420 > .deployer/rollup.json
这将创建 .deployer/rollup.json。
这些文件现在在哪里?
在这两个命令之后,你应该拥有:
.deployer/genesis.json.deployer/rollup.json
在后续步骤中初始化 op-geth 和 op-node 时,我们将把这些文件 复制 到正确的位置。
8. 运行核心客户端
部署合约并生成配置文件后,是时候 启动你的rollup的两个主要引擎 了:
- 执行客户端 (
op-geth) - 共识客户端 (
op-node)
这两个客户端将 协同工作 以驱动你的 L2 rollup。
让我们逐步进行。
步骤 1:初始化 op-geth
首先,我们需要使用我们创建的 genesis.json 初始化执行客户端 (op-geth)。
导航到你的 op-geth 目录:
cd ~/op-geth
创建一个用于区块链数据的目录:
mkdir datadir
初始化 op-geth:
build/bin/geth init --state.scheme=hash --datadir=datadir ../optimism/.deployer/genesis.json
这告诉 op-geth:
- 使用
genesis.json中指定的起始块 - 将区块链数据存储在
datadir/文件夹中
步骤 2:启动 op-geth
现在,启动执行客户端:
./build/bin/geth \\
--datadir ./datadir \\
--http \\
--http.corsdomain="*" \\
--http.vhosts="*" \\
--http.addr=0.0.0.0 \\
--http.api=web3,debug,eth,txpool,net,engine \\
--ws \\
--ws.addr=0.0.0.0 \\
--ws.port=8546 \\
--ws.origins="*" \\
--ws.api=debug,eth,txpool,net,engine \\
--syncmode=full \\
--gcmode=archive \\
--nodiscover \\
--maxpeers=0 \\
--networkid=42069 \\
--authrpc.vhosts="*" \\
--authrpc.addr=0.0.0.0 \\
--authrpc.port=8551 \\
--authrpc.jwtsecret=./jwt.txt \\
--rollup.disabletxpoolgossip=true
重要设置:
-gcmode=archive→ 对于 Sequencer 来说很重要,因为op-proposer需要完整状态。-authrpc.jwtsecret=./jwt.txt→ 此 JWT 密钥用于在op-geth和op-node之间进行身份验证。
如果这成功运行,则你的执行客户端已启动!
步骤 3:启动 op-node
打开一个
新的终端窗口
cd ~/optimism/op-node
启动共识客户端:
./bin/op-node \\
--l2=http://localhost:8551 \\
--l2.jwt-secret=./jwt.txt \\
--sequencer.enabled \\
--sequencer.l1-confs=5 \\
--verifier.l1-confs=4 \\
--rollup.config=../.deployer/rollup.json \\
--rpc.addr=0.0.0.0 \\
--rpc.port=9545 \\
--p2p.disable \\
--rpc.enable-admin \\
--p2p.sequencer.key=$GS_SEQUENCER_PRIVATE_KEY \\
--l1=$L1_RPC_URL \\
--l1.rpckind=$L1_RPC_KIND
这将:
- 将
op-node连接到你的op-geth - 开始生成 L2 区块
- 监视 L1 (Sepolia) 的更新
此时,你应该开始在日志中实时看到 L2 区块的生产!
如何知道它是否正常工作
如果一切设置正确:
op-geth将显示正在导入的新区块op-node将显示正在提议和最终确定的新 L2 区块
你现在拥有一个可正常工作的本地 Sequencer 节点!
9. 运行支持服务(op-batcher 和 op-proposer)
现在你的执行客户端(op-geth)和共识客户端(op-node)已启动并生成 L2 区块,
你仍然需要 两个关键的后台服务 来完成完整的 rollup 生命周期:
- Batcher (
op-batcher) → 将交易数据发布到 L1 - Proposer (
op-proposer) → 将状态根发布到 L1(用于提款和最终确定)
让我们启动它们!
1. 启动 Batcher (op-batcher)
Batcher 从你的 L2 链收集区块和交易数据,将它们捆绑到批次中,并将它们发送到 Sepolia 上的 Batch Inbox 合约。
没有 batcher,你的 rollup 不会向以太坊发布任何数据。
打开一个新的终端窗口。
导航到 batcher 目录:
cd ~/optimism/op-batcher
启动 batcher:
./bin/op-batcher \\
--l2-eth-rpc=http://localhost:8545 \\
--rollup-rpc=http://localhost:9545 \\
--poll-interval=1s \\
--sub-safety-margin=6 \\
--num-confirmations=1 \\
--safe-abort-nonce-too-low-count=3 \\
--resubmission-timeout=30s \\
--rpc.addr=0.0.0.0 \\
--rpc.port=8548 \\
--rpc.enable-admin \\
--max-channel-duration=25 \\
--l1-eth-rpc=$L1_RPC_URL \\
--private-key=$GS_BATCHER_PRIVATE_KEY
现在,你的 batcher 正在运行!
它将:
- 监视你的
op-geth - 将批处理的 L2 交易提交到 Sepolia (L1)
2. 启动 Proposer (op-proposer)
Proposer 将 L2 状态根 提交到 Sepolia 上的 L2OutputOracle 合约。
这使用户最终能够 将资金提取回 L1。
打开另一个终端窗口。
导航到 proposer 目录:
cd ~/optimism/op-proposer
启动 proposer:
./bin/op-proposer \\
--poll-interval=12s \\
--rpc.port=8560 \\
--rollup-rpc=http://localhost:9545 \\
--l2oo-address=$(cat ../packages/contracts-bedrock/deployments/getting-started/.deploy | jq -r .L2OutputOracleProxy) \\
--private-key=$GS_PROPOSER_PRIVATE_KEY \\
--l1-eth-rpc=$L1_RPC_URL
现在,你的 proposer 已经启动!
它将:
- 监视你的 L2 链
- 将最终确定的 L2 输出发布到 L1
10. 将你的钱包连接到你的 Rollup
现在你的 rollup 正在完全运行——生成区块、发布批次和最终确定状态——
是时候 连接你的钱包(如 MetaMask)并 直接与你的 L2 网络交互 了。
这就是它开始感觉 真实 的地方——发送交易、部署合约,并在你自己的链上看到活动!
步骤 1:在 MetaMask 中将你的 Rollup 添加为自定义网络
打开 MetaMask 并手动添加一个新的自定义网络。
以下是你需要的信息:
网络名称 → OP Stack Rollup(或你喜欢的任何名称)
新 RPC URL → http:localhost:8545
链 ID → 420(或你在 .envrc 中配置的任何内容)
货币符号 → ETH
区块浏览器 URL →(暂时留空)
保存网络。
你的 MetaMask 现在应该指向你位于 localhost:8545 的本地 Rollup。
步骤 2:确认你已连接
添加后:
- 你应该看到你的钱包已连接到你的 L2 链。
- 你的钱包地址应与 Sepolia 上的地址完全相同(因为私钥相同)。
- 但是你的 L2 上的余额可能为 0 ETH(因为尚未发生桥接)。
不用担心!
我们将在下一节中解决这个问题。
11. 将 ETH 桥接到你的 Rollup
现在你的钱包已连接到你的 L2 rollup,你将注意到你的 rollup 网络上有 0 ETH。
要发送交易或部署合约,你需要 L2 链上的 ETH。
你如何将 ETH 放到你的 rollup 上?
通过 **通过你先前部署的 L1 Standard Bridge 合约进行 桥接。
让我们一步一步地进行。
步骤 1:找到 L1 Bridge 合约地址
导航到你的 contracts-bedrock 包:
cd ~/optimism/packages/contracts-bedrock
获取运行 op-deployer 时部署的 L1StandardBridgeProxy 的地址:
cat deployments/getting-started/.deploy | jq -r .L1StandardBridgeProxy
这将输出你需要将 ETH 发送到的地址。
保存此地址。
步骤 2:将 Sepolia ETH 发送到 Bridge 合约
打开你的 MetaMask(或任何连接到 Sepolia 的钱包)并将少量 Sepolia ETH(例如 0.05 或 0.1 ETH)发送到你刚刚获得的 L1StandardBridgeProxy 地址。
这将触发向你的 rollup 的 存款。
重要提示:
- 在测试期间不要发送大量资金。
- 等待 Sepolia 交易被确认。
步骤 3:等待桥接完成
将 ETH 发送到桥后:
- 你的 L1 存款必须由 rollup 最终确定。
op-node将观察 L1 存款。- 几个区块后(通常为 2-5 分钟),你将在 L2 上看到 ETH 被铸造。
你可以查看你的 op-node 和 op-geth 终端中的日志 — 你应该看到“存款已最终确定”或类似日志。
完成后,你的 Rollup 网络上的钱包将显示桥接的 ETH。
12. 测试你的 Rollup
现在你已经将 ETH 桥接到你的 rollup 上,你终于可以像使用任何其他 EVM 链一样 使用 你的新区块链了!
你现在可以 发送交易、部署智能合约 并 观察你的 rollup 的运行情况。
让我们来演练一下你应该做的基础测试。
步骤 1:发送测试交易
使用你的 MetaMask(或任何连接到你的 L2 网络的钱包)并:
- 将少量 ETH 发送到另一个地址(甚至发回给你自己)。
- 在 MetaMask 中确认交易。
几秒钟内,你应该看到:
- 交易在你的钱包中得到确认。
- 显示交易正在执行的日志出现在你的
op-geth和op-node终端中。
如果这有效,则意味着:
- 你的 RPC 端点(
localhost:8545)已启动 - 你的执行和共识客户端正在处理新的交易
- 你的 rollup 运行状况良好
步骤 2:部署智能合约
为了进行全面测试,我们还部署一个简单的智能合约!
你可以使用 Remix IDE、Hardhat 或 Foundry 等工具。
这是一个使用 Remix 的基本方法:
- 打开 Remix。
- 将环境设置为“Injected Web3”→ 连接到 MetaMask。
- 确保 MetaMask 已连接到你的 OP Stack Rollup (localhost:8545)。
- 编写一个非常简单的合约,例如:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
string public message = "Hello, Rollup!";
}
- 编译并部署它。
✅ 你应该看到:
- 部署交易在你的 L2 上得到确认
- 合约地址可用
- 能够立即调用函数(
message)
你在这里测试什么
你正在验证:
- 你的 rollup 可以接受和处理 用户发起的交易
- 你的链支持 智能合约部署
- Gas 计量、状态更新和存储工作正常
这是完整的 EVM 功能——在你自己的 L2 rollup 上!
13. 最后想法和接下来你可以构建的内容
恭喜你——你已经从头开始 创建了你自己的 OP Stack L2 Rollup 测试网!
这是一项巨大的成就。
你现在了解了现代 rollup 的 完整堆栈:
- 共识 和 执行 分离
- batcher 和 proposer 的角色
- 在 L1 和 L2 之间 桥接 ETH
- 你自己运行 排序器节点
- 在你自己的链上 部署智能合约
这不仅仅是理论知识——你实际上 做到了。
- 原文链接: medium.com/@tomarpari90/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~