{"content":{"title":"Arbitrum Nitro 是怎样扩容的以及如何使用它","body":"> * 原文链接： https://soliditydeveloper.com/arbitrum-nitro\r\n> * 译文出自：[登链翻译计划](https://github.com/lbc-team/Pioneer)\r\n> * 译者：[翻译小组](https://learnblockchain.cn/people/412)  校对：[Tiny 熊](https://learnblockchain.cn/people/15)\r\n> * 本文永久链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/5322)\r\n\r\n\r\n\r\n\r\n你听说过Arbitrum Nitro吗？新的WAVM可以实现Plasma，以一种超级高效的方式用于智能合约! 它使侧链拥有了以太坊主链的安全保证。到目前为止，Arbitrum已经是[最成功的](https://twitter.com/JackNiewold/status/1564374766441611266)第二层之一，新的Nitro是它的一个重要升级。\r\n\r\n让我们从头开始...\r\n\r\n\r\n\r\n### 什么是默克尔树？\r\n\r\n**Merkle 树**是这种扩容技术工作的基础。Merkle树的根是根hash。它是由所有叶子节点的原始值的哈希值创建而来。现在，两个叶子的哈希值组合起来创建一个新的哈希值， 一直向上组合，直到生成只有一个根哈希值的树。现在，一个**Merkle证明**是你向**只知道根哈希值的人证明**某个值实际上是这棵树的叶子之一的方法。\r\n\r\n这里有一篇[Merkle 树指南](https://learnblockchain.cn/article/5297)，如果你想更深入地了解这个话题，可以看看。\r\n\r\n\r\n\r\n## 智能合约的状态\r\n\r\n在以太坊中，一棵Merkle树是状态树，它包含所有的状态，如用户ETH余额，也包含合约存储。这使得我们可以在智能合约状态上创建Merkle证明!\r\n\r\n所以可以用Merkle证明机制来证明一个智能合约具有某种状态。记住这一点稍后待用。\r\n\r\n\r\n\r\n## Plasma是如何工作的？\r\n\r\nPlasma 使用智能合约和Merkle证明的组合。这些结合在一起，通过将这些交易从以太坊主区块链上转移到Plasma链上，实现快速和低廉的交易。与普通侧链相比，你不能在这里运行任何智能合约。\r\n\r\n在Plasma中，用户以[UTXO](https://www.investopedia.com/terms/u/utxo.asp)的方式在彼此之间发送交易，其中新的余额的结果作为Merkle树根在以太坊智能合约中持续更新。一旦Merkle树根在智能合约中被更新，即使Plasma链运营商是恶意的，它也能给保证用户资金的安全。Merkle树根囊括了许多笔资金交易的结果。如果Plasma运营商提交了一个无效的根，用户可以提出异议，并安全地取回他们的资金。更多细节请看[这里](https://www.learnplasma.org/en/learn/mvp.html#plasma-mvp)。\r\n\r\n但如前所述，它不能运行智能合约。所以不可能与Plasma进行Uniswap。\r\n\r\n\r\n\r\n### Arbitrum: 如何在区块链上运行一个区块链\r\n\r\n但这就是Arbitrum的用武之地，它是智能合约版的Plasma!\r\n\r\n![Yo Dawg Optimism](https://img.learnblockchain.cn/2023/01/12/yo-dawg-blockchain.jpg)\r\n\r\n这里的核心思想其实很简单。就像在Plasma中，有一个运行所有交易的 layer2 链，及偶尔更新Layer1的Merkle根。在Arbitrum中，Merkle 根不是像普通plasma那样用于UTXO交易，而是包含智能合约的全部状态。或者说是所有正在使用的智能合约的全部状态。\r\n\r\n是的，这意味着我们可以在Arbitrum上运行任意的智能合约! 简单归纳一下它的工作方式：\r\n\r\n- 将智能合约状态表现为Merkle树\r\n- 只在Arbitrum链上运行所有交易\r\n- 持续更新以太坊第一层的状态根\r\n- Arbitrum链的安全性很低，但通过以太坊上的状态根，则可以实现欺诈证明\r\n- 当Layer2的验证者提交了一个恶意的状态根并被质疑时，验证者会失去他们的保证金。\r\n- 欺诈证明的成本很高，但通过交互式机制比Optimism更有效（详见下文）。\r\n- 运行单个有争议的执行步骤，证明者提交任何需要的状态。\r\n\r\n现在你可能意识到，这就是扩容的来源。你只在第1层运行有争议的、有欺诈证明的交易。所以，扩容的优势完全来自于你不会在第1层运行99.9%的交易这一事实。\r\n\r\n\r\n\r\n## Arbitrum 详细概述\r\n\r\nArbitrum Nitro背后的大概念是：\r\n\r\n1. 序列化 \r\n2. 核心是Geth\r\n3. 用于证明的Wasm\r\n4. 通过交互式欺诈证明进行乐观 Rollup\r\n\r\n![Arbitrrum Sequencing](https://img.learnblockchain.cn/2023/01/12/seq-arbitrum.png)\r\n\r\n为了实际运行交易，我们需要原生 Geth、Geth与Wasm和Merkle证明。该架构看起来像这样。\r\n\r\n- 在最上层，你有区块链节点功能。\r\n- ArbOS处理L2功能，如批量解包和桥接。\r\n- 核心Geth EVM 执行合约，可以是原生合约或使用 WASM 的合约。\r\n\r\n![Geth at core](https://img.learnblockchain.cn/2023/01/12/geth-at-core.png)\r\n\r\n\r\n\r\n## 交易是如何被包含的？\r\n\r\n新的交易可以通过三种方式被添加：\r\n\r\n1. 定序器（Sequencer）的正常打包包含\r\n2. 通过定序器，从L1的消息中加入\r\n\r\n3. 来自L1的消息被强制包含在L2上\r\n\r\n\r\n\r\n### 1. 定序器的正常打包包含\r\n\r\n在正常情况下，目前仍然中心化的定序器将把新的消息添加到收件箱中。这是通过调用[addSequencerL2Batch](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L235)完成的。函数检查发送者是否是保存的定序器，只有他被允许调用这个函数。\r\n\r\n```solidity\r\nfunction addSequencerL2Batch(\r\n    uint256 sequenceNumber,\r\n    bytes calldata data,\r\n    uint256 afterDelayedMessagesRead,\r\n    IGasRefunder gasRefunder,\r\n    uint256 prevMessageCount,\r\n    uint256 newMessageCount\r\n) external override refundsGas(gasRefunder) {\r\n    if (\r\n        !isBatchPoster[msg.sender]\r\n        && msg.sender != address(rollup)\r\n    ) revert NotBatchPoster();\r\n\r\n    [...]\r\n    addSequencerL2BatchImpl(\r\n        dataHash_,\r\n        afterDelayedMessagesRead_,\r\n        0,\r\n        prevMessageCount_,\r\n        newMessageCount_\r\n    );\r\n    [...]\r\n}\r\n```\r\n\r\n然后在[addSequencerL2BatchImpl](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L338-L383)里面调用桥接（bridge），将信息排队到收件箱。\r\n\r\n```solidity\r\nbridge.enqueueSequencerMessage(\r\n    dataHash,\r\n    afterDelayedMessagesRead,\r\n    prevMessageCount,\r\n    newMessageCount\r\n);\r\n```\r\n\r\n然后在桥接中调用 [enqueueSequencerMessage](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Bridge.sol#L100-L132)，它只是向收件箱数组添加一个新的哈希值。\r\n\r\n```solidity\r\nbytes32[] public sequencerInboxAccs;\r\n\r\nfunction enqueueSequencerMessage(\r\n    bytes32 dataHash,\r\n    uint256 afterDelayedMessagesRead,\r\n    uint256 prevMessageCount,\r\n    uint256 newMessageCount\r\n)\r\n    external\r\n    onlySequencerInbox\r\n    returns (\r\n        uint256 seqMessageIndex,\r\n        bytes32 beforeAcc,\r\n        bytes32 delayedAcc,\r\n        bytes32 acc\r\n    )\r\n{\r\n    [...]\r\n    acc = keccak256(abi.encodePacked(beforeAcc, dataHash, delayedAcc));\r\n    sequencerInboxAccs.push(acc);\r\n}\r\n```\r\n\r\n\r\n\r\n### 2. 通过定序器，从L1的消息中加入\r\n\r\n消息也可以由任何人直接使用L1中的调用来添加。例如，当从L1到L2进行存款时，就很有用。\r\n\r\n最终这将在在[deliverToBridge](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Inbox.sol#L419-L430)内调用桥接的[enqueueDelayedMessage](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Bridge.sol#L152-L167)。\r\n\r\n```solidity\r\nbytes32[] public delayedInboxAccs;\r\n\r\nfunction enqueueDelayedMessage(\r\n    uint8 kind,\r\n    address sender,\r\n    bytes32 messageDataHash\r\n) external payable returns (uint256) {\r\n    [...]\r\n    delayedInboxAccs.push(\r\n        Messages.accumulateInboxMessage(\r\n            prevAcc,\r\n            messageHash\r\n        )\r\n    );\r\n    [...]\r\n}\r\nfunction deliverToBridge(\r\n    uint8 kind,\r\n    address sender,\r\n    bytes32 messageDataHash\r\n) internal returns (uint256) {\r\n   return\r\n       bridge.enqueueDelayedMessage{value: msg.value}(\r\n           kind,\r\n           AddressAliasHelper.applyL1ToL2Alias(sender),\r\n           messageDataHash\r\n       );\r\n}\r\n```\r\n\r\n\r\n\r\n### 3. 来自L1的消息被强制包含在L2上\r\n\r\n第二种情况有一个问题。定序器可以从延迟的收件箱中获取消息并进行处理，但他也可以简单地忽略它们。这情况下，消息可能永远不会在L2中。而由于定序器仍然是中心化的，所以有第三个备份选项，叫做[forceInclusion（强制包含）](https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L89-L152)。\r\n\r\n任何人都可以调用这个函数，如果在最小时间范围内定序器停止发布消息，它允许其他人继续发布消息。\r\n\r\n\r\n\r\n**那么，为什么会有延迟，为什么不允许用户总是立即强制包含交易呢？**如果定序器有优先权，他可以给用户提供关于交易的软确认，带来更好的用户体验。如果有持续的强制包含，定序器就不能预先向用户确认将发生什么。为什么呢？好吧，一个强行加入的交易可能会使定序器计划发布的交易无效。\r\n\r\n```solidity\r\nfunction forceInclusion(\r\n    uint256 _totalDelayedMessagesRead,\r\n    uint8 kind,\r\n    uint64[2] calldata l1BlockAndTime,\r\n    uint256 baseFeeL1,\r\n    address sender,\r\n    bytes32 messageDataHash\r\n) external {\r\n    [...]\r\n\r\n    if (l1BlockAndTime[0] + maxTimeVariation.delayBlocks >= block.number)\r\n        revert ForceIncludeBlockTooSoon();\r\n    if (l1BlockAndTime[1] + maxTimeVariation.delaySeconds >= block.timestamp)\r\n        revert ForceIncludeTimeTooSoon();\r\n\r\n    [...]\r\n\r\n    addSequencerL2BatchImpl(\r\n            dataHash,\r\n            __totalDelayedMessagesRead,\r\n            0,\r\n            prevSeqMsgCount,\r\n            newSeqMsgCount\r\n        );\r\n    [...]\r\n}\r\n```\r\n\r\n\r\n\r\n## 欺诈证明是如何工作的？\r\n\r\n让我们详细探讨一下Arbitrum Nitro的欺诈证明是如何工作的。\r\n\r\n\r\n\r\n### 1. WAVM\r\n\r\nArbitrum Nitro的新功能是WAVM。他们基本上重新使用Geth 以太坊节点代码，并将其编译为Wasm（或者说是Wasm的一个略微修改的版本）。Wasm是Web Assembly的缩写，是一个允许运行代码的环境，与平台无关。所以类似于EVM，但没有Gas。它也是一个网络范围的标准，所以它有更多其他语言的支持和更好的性能。因此，将用Go编写的Geth代码编译到Wasm中确实是可能的。\r\n\r\n这个Wasm的执行对我们有什么帮助？\r\n\r\n![数学证明Meme](https://img.learnblockchain.cn/2023/01/12/math-proof-meme.png)\r\n\r\n我们可以用它运行证明! 因为它是一个受控的执行环境，我们可以在Solidity智能合约内复制它的执行。这就是运行欺诈证明的要求。\r\n\r\nWasm与原生编译的代码相比，执行速度还是比较慢。但这里是Nitro的魅力所在。同样的Geth代码在证明时将被编译成Wasm，但在执行时则被编译成本地代码。这样，我们就可以得到两全其美的结果：以本地性能运行链，但仍然能够执行证明。\r\n\r\n\r\n\r\n### 2. 欺诈证明\r\n\r\n![欺诈性备忘录](https://img.learnblockchain.cn/2023/01/12/fraud-meme.jpg)\r\n\r\n现在让我们来看看这些欺诈证明是如何详细工作的。我们需要什么？\r\n\r\n1. 我们需要一种机制来获得执行的前状态和后状态。\r\n2. 我们需要能够在Solidity合约中运行 WAVM执行（ WAVM execution）。\r\n3. 我们需要一个交互式的机制来决定哪一个执行步骤需要证明。\r\n\r\n\r\n\r\n最后一步是可选的，但如果我们只需要对单个的执行进行证明，则是一种性能改进。然而，它确实需要挑战者和被挑战节点之间的一些额外的交互步骤。我们不会去讨论这个细节，但你可以在[这里](https://developer.offchainlabs.com/inside-arbitrum-nitro#dissection-protocol-simplified-version)阅读更多的内容。当然也可以直接在[源代码](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/challenge/ChallengeManager.sol#L171)中阅读。\r\n\r\n但我们现在将详细介绍其他两个部分。\r\n\r\n\r\n\r\n### 3. 获取一个执行的前状态和后状态\r\n\r\n\r\n在交互式挑战过程中，最终挑战者会指向某单个执行的分歧。这个单一的执行有一个已知的执行前和执行后状态的Merkle根哈希值。执行后的根哈希值是被挑战的，所以最后我们会把它和我们自己执行得到的结果进行比较。执行前的哈希值没有受到挑战，因此是可信的。\r\n\r\n它将被用于初始化[WAVM 机器（Machine）](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Machine.sol#L18-L28)。\r\n\r\n```solidity\r\nstruct Machine {\r\n    MachineStatus status;\r\n    ValueStack valueStack;\r\n    ValueStack internalStack;\r\n    StackFrameWindow frameStack;\r\n    bytes32 globalStateHash;\r\n    uint32 moduleIdx;\r\n    uint32 functionIdx;\r\n    uint32 functionPc;\r\n    bytes32 modulesRoot;\r\n}\r\n```\r\n\r\n挑战者将用所有的数据初始化这个机器Machine。\r\n\r\n在合约中，我们只需要再次检查这些数据是否代表了存储的Merkle根哈希值。\r\n\r\n```solidity\r\nrequire(mach.hash() == beforeHash, \"MACHINE_BEFORE_HASH\")\r\n```\r\n\r\n现在我们可以信任Module（模块）根，用它来验证模块的数据。\r\n\r\n一个[模块](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Module.sol#L9)被定义为：\r\n\r\n```solidity\r\nstruct Module {\r\n    bytes32 globalsMerkleRoot;\r\n    ModuleMemory moduleMemory;\r\n    bytes32 tablesMerkleRoot;\r\n    bytes32 functionsMerkleRoot;\r\n    uint32 internalsOffset;\r\n}\r\n```\r\n\r\n这里面持有的数据是WAVM机器数据的进一步Merkle根哈希值的形式。而挑战者也初始化了这些数据。\r\n\r\n合约又只是[验证它](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProofEntry.sol#L60)是否匹配之前的模块modulesRoot：\r\n\r\n```solidity\r\n(mod, offset) = Deserialize.module(proof, offset);\r\n(modProof, offset) = Deserialize.merkleProof(proof, offset);\r\nrequire(\r\n    modProof.computeRootFromModule(mach.moduleIdx, mod) == mach.modulesRoot,\r\n    \"MODULES_ROOT\"\r\n);\r\n```\r\n\r\n最后我们再对[Instruction（指令）](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Instructions.sol#L7)数据做同样的处理。\r\n\r\n```solidity\r\nstruct Instruction {\r\n    uint16 opcode;\r\n    uint256 argumentData;\r\n}\r\n```\r\n\r\n并且将通过函数MerkleRoot进行验证：\r\n\r\n```solidity\r\nMerkleProof memory instProof;\r\nMerkleProof memory funcProof;\r\n(inst, offset) = Deserialize.instruction(proof, offset);\r\n(instProof, offset) = Deserialize.merkleProof(proof, offset);\r\n(funcProof, offset) = Deserialize.merkleProof(proof, offset);\r\nbytes32 codeHash = instProof.computeRootFromInstruction(mach.functionPc, inst);\r\nbytes32 recomputedRoot = funcProof.computeRootFromFunction(\r\n    mach.functionIdx,\r\n    codeHash\r\n);\r\nrequire(recomputedRoot == mod.functionsMerkleRoot, \"BAD_FUNCTIONS_ROOT\");\r\n```\r\n\r\n所以现在我们有一个初始化的WAVM机器，剩下的就是执行某个有分歧的的操作。现在这取决于我们需要运行的确切指令。\r\n\r\n以一个[简单加法](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProverMath.sol#L217)为例，这是很简单的。\r\n\r\n```solidity\r\nuint32 b = mach.valueStack.pop().assumeI32();\r\nuint32 a = mach.valueStack.pop().assumeI32();\r\n[...]\r\nreturn (a + b, false);\r\n```\r\n\r\n![堆栈](https://img.learnblockchain.cn/2023/01/12/Lifo_stack.svg.png)\r\n\r\n基本上就是这样了。从机器堆栈中取前两个值，然后把它们加在一起。\r\n\r\n让我们来看看另一条指令，一个[本地获取](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProver0.sol#L334)指令：\r\n\r\n```solidity\r\nfunction executeLocalGet(\r\n    Machine memory mach,\r\n    Module memory,\r\n    Instruction calldata inst,\r\n    bytes calldata proof\r\n) internal pure {\r\n    StackFrame memory frame = mach.frameStack.peek();\r\n    Value memory val = merkleProveGetValue(frame.localsMerkleRoot, inst.argumentData, proof);\r\n    mach.valueStack.push(val);\r\n}\r\n```\r\n\r\n这个[StackFrame](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/StackFrame.sol#L9)来自WAVM的初始化，在这里我们可以找到localsMerkleRoot。\r\n\r\n```solidity\r\nstruct StackFrame {\r\n    Value returnPc;\r\n    bytes32 localsMerkleRoot;\r\n    uint32 callerModule;\r\n    uint32 callerModuleInternals;\r\n}\r\n```\r\n\r\n并通过Merkle证明，我们可以检索到该值并将其推送到堆栈。\r\n\r\n最后，我们[检查](https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/challenge/ChallengeManager.sol#L262)这个计算步骤产生的最终哈希值是否等于存储的哈希值。\r\n\r\n```solidity\r\nrequire(\r\n    afterHash != selection.oldSegments[selection.challengePosition + 1],\r\n    \"SAME_OSP_END\"\r\n);\r\n```\r\n\r\n只有当它不匹配时，证明才有效，我们继续。现在挑战者赢了，一个新的后状态将被接受。\r\n\r\n\r\n\r\n## 如何在Arbitrum上开发\r\n\r\nArbitrum完全[支持Solidity](https://developer.offchainlabs.com/solidity-support)，所以你可以照搬你的合约，只需注意一些问题：\r\n\r\n- `blockhash(x)`返回一个加密不安全的伪随机哈希值.\r\n- `block.coinbase`返回0\r\n- `block.difficulty`返回常数2500000000000000\r\n- `block.number`/`block.timestamp`返回L1区块的 `估计值`\r\n- `msg.sender`的工作方式与以太坊上正常的L2到L2交易相同；对于L1到L2的 “retryable ticket（可重试票据） ”交易，它将返回触发消息的L1合约的L2地址别名。更多内容见[可重试票据地址别名](https://developer.offchainlabs.com/arbos/l1-to-l2-messaging#address-aliasing)。\r\n\r\n\r\n\r\n### 如何使用 Arbitrum 网络\r\n\r\n这就是两个重要的Aribtrum网络。你可以使用MetaMask等支持的钱包的[wallet_addEthereumChain](https://docs.metamask.io/guide/rpc-api.html#wallet-addethereumchain)功能指定添加网络，要不然用户需要手动添加网络。\r\n\r\n\r\n\r\n- [点击这里连接到 Arbitrum One](https://chainlist.org/chain/42161)\r\n- [点击这里连接到 Arbitrrum Nitro Rinkeby](https://chainlist.org/chain/421611)\r\n- [点击这里连接到 Aribitrum Nitro Goerli](https://chainlist.org/chain/421613)\r\n\r\n```javascript\r\nconst params = [{\r\n  \"chainId\": \"42161\", // testnet: \"421611\"\r\n  \"chainName\": \"Arbitrum\",\r\n  \"rpcUrls\": [\r\n    \"https://arb1.arbitrum.io/rpc\"\r\n    // rinkeby: \"https://rinkeby.arbitrum.io/rpc\"\r\n    // goerli: \"https://goerli-rollup.arbitrum.io/rpc\"\r\n  ],\r\n  \"nativeCurrency\": {\r\n    \"name\": \"Ether\",\r\n    \"symbol\": \"ETH\",\r\n    \"decimals\": 18\r\n  },\r\n  \"blockExplorerUrls\": [\r\n    \"https://explorer.arbitrum.io\"\r\n    // rinkeby: \"https://rinkeby-explorer.arbitrum.io\"\r\n    // goerli: \"https://goerli-rollup-explorer.arbitrum.io\"\r\n  ]\r\n}]\r\n\r\ntry {\r\n    await ethereum.request({\r\n        method: 'wallet_addEthereumChain',\r\n        params,\r\n    })\r\n} catch (error) {\r\n    // something failed, e.g., user denied request\r\n}\r\n\r\n```\r\n\r\n要在Arbitrum获得资金，可使用 https://bridge.arbitrum.io/ 提供的桥。\r\n\r\n\r\n\r\n### 如何部署到Arbitrum网络中\r\n\r\n现在你可以将Arbitrum主网添加到Truffle或Hardhat中，如下所示：\r\n```json\r\n{\r\n    arbitrum_mainnet: {\r\n        provider: function () {\r\n          return new HDWalletProvider(\r\n            mnemonic,\r\n            \"https://arbitrum-mainnet.infura.io/v3/\"\r\n                + infuraKey,\r\n            0,\r\n            1\r\n          );\r\n        },\r\n    },\r\n    arbitrum_rinkeby: {\r\n        provider: function () {\r\n          return new HDWalletProvider(\r\n            mnemonic,\r\n            \"https://rinkeby.arbitrum.io/rpc\",\r\n            0,\r\n            1\r\n          );\r\n        },\r\n    },\r\n    arbitrum_goerli: {\r\n        provider: function () {\r\n          return new HDWalletProvider(\r\n            mnemonic,\r\n            \"https://goerli-rollup.arbitrum.io/rpc\",\r\n            0,\r\n            1\r\n          );\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n\r\n推荐的一个好的做法是用Hardhat编写测试，用常规的本地配置，这样你可以快速运行测试，并有console.log/stacktraces 功能可用。\r\n\r\n如果需要，可以在[Infura](https://infura.io)设置中激活Arbitrum。"},"author":{"user":"https://learnblockchain.cn/people/412","address":null},"history":"QmPyheh42WNXXT5nBdPNRqBAHCu5mFGrPGXFZAmVVXDenb","timestamp":1674209842,"version":1}