• Arbitrum Nitro 是怎样扩容的以及如何使用它
  • 发布于 2个月前
  • 261 热度
    0 评论
你听说过 Arbitrum Nitro 吗?新的 WAVM 可以实现 Plasma,以一种超级高效的方式用于智能合约! 它使侧链拥有了以太坊主链的安全保证。到目前为止,Arbitrum 已经是最成功的[4]第二层之一,新的 Nitro 是它的一个重要升级。让我们从头开始!

什么是默克尔树?
Merkle 树是这种扩容技术工作的基础。Merkle 树的根是根 hash。它是由所有叶子节点的原始值的哈希值创建而来。现在,两个叶子的哈希值组合起来创建一个新的哈希值, 一直向上组合,直到生成只有一个根哈希值的树。现在,一个Merkle 证明是你向只知道根哈希值的人证明某个值实际上是这棵树的叶子之一的方法。

智能合约的状态
在以太坊中,一棵 Merkle 树是状态树,它包含所有的状态,如用户 ETH 余额,也包含合约存储。这使得我们可以在智能合约状态上创建 Merkle 证明!所以可以用 Merkle 证明机制来证明一个智能合约具有某种状态。记住这一点稍后待用。

Plasma 是如何工作的?
Plasma 使用智能合约和 Merkle 证明的组合。这些结合在一起,通过将这些交易从以太坊主区块链上转移到 Plasma 链上,实现快速和低廉的交易。与普通侧链相比,你不能在这里运行任何智能合约。

在 Plasma 中,用户以UTXO[6]的方式在彼此之间发送交易,其中新的余额的结果作为 Merkle 树根在以太坊智能合约中持续更新。一旦 Merkle 树根在智能合约中被更新,即使 Plasma 链运营商是恶意的,它也能给保证用户资金的安全。Merkle 树根囊括了许多笔资金交易的结果。如果 Plasma 运营商提交了一个无效的根,用户可以提出异议,并安全地取回他们的资金。更多细节请看这里[7]。

但如前所述,它不能运行智能合约。所以不可能与 Plasma 进行 Uniswap。

Arbitrum: 如何在区块链上运行一个区块链
但这就是 Arbitrum 的用武之地,它是智能合约版的 Plasma!这里的核心思想其实很简单。就像在 Plasma 中,有一个运行所有交易的 layer2 链,及偶尔更新 Layer1 的 Merkle 根。在 Arbitrum 中,Merkle 根不是像普通 plasma 那样用于 UTXO 交易,而是包含智能合约的全部状态。或者说是所有正在使用的智能合约的全部状态。

是的,这意味着我们可以在 Arbitrum 上运行任意的智能合约! 简单归纳一下它的工作方式:
1.将智能合约状态表现为 Merkle 树
2.只在 Arbitrum 链上运行所有交易
3.持续更新以太坊第一层的状态根
4.Arbitrum 链的安全性很低,但通过以太坊上的状态根,则可以实现欺诈证明
5.当 Layer2 的验证者提交了一个恶意的状态根并被质疑时,验证者会失去他们的保证金。
6.欺诈证明的成本很高,但通过交互式机制比 Optimism 更有效(详见下文)。
7.运行单个有争议的执行步骤,证明者提交任何需要的状态。
现在你可能意识到,这就是扩容的来源。你只在第 1 层运行有争议的、有欺诈证明的交易。所以,扩容的优势完全来自于你不会在第 1 层运行 99.9%的交易这一事实。

Arbitrum 详细概述
Arbitrum Nitro 背后的大概念是:
1.序列化
2.核心是 Geth
3.用于证明的 Wasm
4.通过交互式欺诈证明进行乐观 Rollup

为了实际运行交易,我们需要原生 Geth、Geth 与 Wasm 和 Merkle 证明。该架构看起来像这样:
1.在最上层,你有区块链节点功能。
2.ArbOS 处理 L2 功能,如批量解包和桥接。
3.核心 Geth EVM 执行合约,可以是原生合约或使用 WASM 的合约。

交易是如何被包含的?
新的交易可以通过三种方式被添加:
1.定序器(Sequencer)的正常打包包含
2.通过定序器,从 L1 的消息中加入
3.来自 L1 的消息被强制包含在 L2 上

1. 定序器的正常打包包含
在正常情况下,目前仍然中心化的定序器将把新的消息添加到收件箱中。这是通过调用addSequencerL2Batch[8]完成的。函数检查发送者是否是保存的定序器,只有他被允许调用这个函数。
 // 堆代码 www.duidaima.com
function addSequencerL2Batch(
    uint256 sequenceNumber,
    bytes calldata data,
    uint256 afterDelayedMessagesRead,
    IGasRefunder gasRefunder,
    uint256 prevMessageCount,
    uint256 newMessageCount
) external override refundsGas(gasRefunder) {
    if (
        !isBatchPoster[msg.sender]
        && msg.sender != address(rollup)
    ) revert NotBatchPoster();

    [...]
    addSequencerL2BatchImpl(
        dataHash_,
        afterDelayedMessagesRead_,
        0,
        prevMessageCount_,
        newMessageCount_
    );
    [...]
}
然后在addSequencerL2BatchImpl[9]里面调用桥接(bridge),将信息排队到收件箱。
bridge.enqueueSequencerMessage(
    dataHash,
    afterDelayedMessagesRead,
    prevMessageCount,
    newMessageCount
);
然后在桥接中调用 enqueueSequencerMessage[10],它只是向收件箱数组添加一个新的哈希值。
bytes32[] public sequencerInboxAccs;

function enqueueSequencerMessage(
    bytes32 dataHash,
    uint256 afterDelayedMessagesRead,
    uint256 prevMessageCount,
    uint256 newMessageCount
)
    external
    onlySequencerInbox
    returns (
        uint256 seqMessageIndex,
        bytes32 beforeAcc,
        bytes32 delayedAcc,
        bytes32 acc
    )
{
    [...]
    acc = keccak256(abi.encodePacked(beforeAcc, dataHash, delayedAcc));
    sequencerInboxAccs.push(acc);
}

2. 通过定序器,从 L1 的消息中加入
消息也可以由任何人直接使用 L1 中的调用来添加。例如,当从 L1 到 L2 进行存款时,就很有用。最终这将在在deliverToBridge[11]内调用桥接的enqueueDelayedMessage[12]。
bytes32[] public delayedInboxAccs;

function enqueueDelayedMessage(
    uint8 kind,
    address sender,
    bytes32 messageDataHash
) external payable returns (uint256) {
    [...]
    delayedInboxAccs.push(
        Messages.accumulateInboxMessage(
            prevAcc,
            messageHash
        )
    );
    [...]
}
function deliverToBridge(
    uint8 kind,
    address sender,
    bytes32 messageDataHash
) internal returns (uint256) {
   return
       bridge.enqueueDelayedMessage{value: msg.value}(
           kind,
           AddressAliasHelper.applyL1ToL2Alias(sender),
           messageDataHash
       );
}

3. 来自 L1 的消息被强制包含在 L2 上
第二种情况有一个问题。定序器可以从延迟的收件箱中获取消息并进行处理,但他也可以简单地忽略它们。这情况下,消息可能永远不会在 L2 中。而由于定序器仍然是中心化的,所以有第三个备份选项,叫做forceInclusion(强制包含)[13]。任何人都可以调用这个函数,如果在最小时间范围内定序器停止发布消息,它允许其他人继续发布消息。

那么,为什么会有延迟,为什么不允许用户总是立即强制包含交易呢?如果定序器有优先权,他可以给用户提供关于交易的软确认,带来更好的用户体验。如果有持续的强制包含,定序器就不能预先向用户确认将发生什么。为什么呢?好吧,一个强行加入的交易可能会使定序器计划发布的交易无效。
// 堆代码 www.duidaima.com
function forceInclusion(
    uint256 _totalDelayedMessagesRead,
    uint8 kind,
    uint64[2] calldata l1BlockAndTime,
    uint256 baseFeeL1,
    address sender,
    bytes32 messageDataHash
) external {
    [...]

    if (l1BlockAndTime[0] + maxTimeVariation.delayBlocks >= block.number)
        revert ForceIncludeBlockTooSoon();
    if (l1BlockAndTime[1] + maxTimeVariation.delaySeconds >= block.timestamp)
        revert ForceIncludeTimeTooSoon();

    [...]

    addSequencerL2BatchImpl(
            dataHash,
            __totalDelayedMessagesRead,
            0,
            prevSeqMsgCount,
            newSeqMsgCount
        );
    [...]
}

欺诈证明是如何工作的?
让我们详细探讨一下 Arbitrum Nitro 的欺诈证明是如何工作的。

1. WAVM
Arbitrum Nitro 的新功能是 WAVM。他们基本上重新使用 Geth 以太坊节点代码,并将其编译为 Wasm(或者说是 Wasm 的一个略微修改的版本)。Wasm 是 Web Assembly 的缩写,是一个允许运行代码的环境,与平台无关。所以类似于 EVM,但没有 Gas。它也是一个网络范围的标准,所以它有更多其他语言的支持和更好的性能。因此,将用 Go 编写的 Geth 代码编译到 Wasm 中确实是可能的。

这个 Wasm 的执行对我们有什么帮助?

我们可以用它运行证明! 因为它是一个受控的执行环境,我们可以在 Solidity 智能合约内复制它的执行。这就是运行欺诈证明的要求。

Wasm 与原生编译的代码相比,执行速度还是比较慢。但这里是 Nitro 的魅力所在。同样的 Geth 代码在证明时将被编译成 Wasm,但在执行时则被编译成本地代码。这样,我们就可以得到两全其美的结果:以本地性能运行链,但仍然能够执行证明。

2. 欺诈证明
现在让我们来看看这些欺诈证明是如何详细工作的。我们需要什么?
1.我们需要一种机制来获得执行的前状态和后状态。
2.我们需要能够在 Solidity 合约中运行 WAVM 执行( WAVM execution)。
3.我们需要一个交互式的机制来决定哪一个执行步骤需要证明。
最后一步是可选的,但如果我们只需要对单个的执行进行证明,则是一种性能改进。然而,它确实需要挑战者和被挑战节点之间的一些额外的交互步骤。我们不会去讨论这个细节,但你可以在这里[14]阅读更多的内容。当然也可以直接在源代码[15]中阅读。

但我们现在将详细介绍其他两个部分。

3. 获取一个执行的前状态和后状态
在交互式挑战过程中,最终挑战者会指向某单个执行的分歧。这个单一的执行有一个已知的执行前和执行后状态的 Merkle 根哈希值。执行后的根哈希值是被挑战的,所以最后我们会把它和我们自己执行得到的结果进行比较。执行前的哈希值没有受到挑战,因此是可信的。

它将被用于初始化WAVM 机器(Machine)[16]。
struct Machine {
    MachineStatus status;
    ValueStack valueStack;
    ValueStack internalStack;
    StackFrameWindow frameStack;
    bytes32 globalStateHash;
    uint32 moduleIdx;
    uint32 functionIdx;
    uint32 functionPc;
    bytes32 modulesRoot;
}
挑战者将用所有的数据初始化这个机器 Machine。在合约中,我们只需要再次检查这些数据是否代表了存储的 Merkle 根哈希值。
require(mach.hash() == beforeHash, "MACHINE_BEFORE_HASH")
现在我们可以信任 Module(模块)根,用它来验证模块的数据。
一个模块[17]被定义为:
struct Module {
    bytes32 globalsMerkleRoot;
    ModuleMemory moduleMemory;
    bytes32 tablesMerkleRoot;
    bytes32 functionsMerkleRoot;
    uint32 internalsOffset;
}
这里面持有的数据是 WAVM 机器数据的进一步 Merkle 根哈希值的形式。而挑战者也初始化了这些数据。
合约又只是验证它[18]是否匹配之前的模块 modulesRoot:
(mod, offset) = Deserialize.module(proof, offset);
(modProof, offset) = Deserialize.merkleProof(proof, offset);
require(
    modProof.computeRootFromModule(mach.moduleIdx, mod) == mach.modulesRoot,
    "MODULES_ROOT"
);
最后我们再对Instruction(指令)[19]数据做同样的处理。
struct Instruction {
    uint16 opcode;
    uint256 argumentData;
}
并且将通过函数 MerkleRoot 进行验证:
MerkleProof memory instProof;
MerkleProof memory funcProof;
(inst, offset) = Deserialize.instruction(proof, offset);
(instProof, offset) = Deserialize.merkleProof(proof, offset);
(funcProof, offset) = Deserialize.merkleProof(proof, offset);
bytes32 codeHash = instProof.computeRootFromInstruction(mach.functionPc, inst);
bytes32 recomputedRoot = funcProof.computeRootFromFunction(
    mach.functionIdx,
    codeHash
);
require(recomputedRoot == mod.functionsMerkleRoot, "BAD_FUNCTIONS_ROOT");
所以现在我们有一个初始化的 WAVM 机器,剩下的就是执行某个有分歧的的操作。现在这取决于我们需要运行的确切指令。
以一个简单加法[20]为例,这是很简单的。
uint32 b = mach.valueStack.pop().assumeI32();
uint32 a = mach.valueStack.pop().assumeI32();
[...]
return (a + b, false);

基本上就是这样了。从机器堆栈中取前两个值,然后把它们加在一起。

让我们来看看另一条指令,一个本地获取[21]指令:
// 堆代码 www.duidaima.com
function executeLocalGet(
    Machine memory mach,
    Module memory,
    Instruction calldata inst,
    bytes calldata proof
) internal pure {
    StackFrame memory frame = mach.frameStack.peek();
    Value memory val = merkleProveGetValue(frame.localsMerkleRoot, inst.argumentData, proof);
    mach.valueStack.push(val);
}
这个StackFrame[22]来自 WAVM 的初始化,在这里我们可以找到 localsMerkleRoot。
struct StackFrame {
    Value returnPc;
    bytes32 localsMerkleRoot;
    uint32 callerModule;
    uint32 callerModuleInternals;
}
并通过 Merkle 证明,我们可以检索到该值并将其推送到堆栈。最后,我们检查[23]这个计算步骤产生的最终哈希值是否等于存储的哈希值。
require(
    afterHash != selection.oldSegments[selection.challengePosition + 1],
    "SAME_OSP_END"
);
只有当它不匹配时,证明才有效,我们继续。现在挑战者赢了,一个新的后状态将被接受。

如何在 Arbitrum 上开发
Arbitrum 完全支持 Solidity[24],所以你可以照搬你的合约,只需注意一些问题:
1.blockhash(x)返回一个加密不安全的伪随机哈希值.
2.block.coinbase返回 0
3.block.difficulty返回常数 2500000000000000
4.block.number/block.timestamp返回 L1 区块的 估计值

5.msg.sender的工作方式与以太坊上正常的 L2 到 L2 交易相同;对于 L1 到 L2 的 “retryable ticket(可重试票据) ”交易,它将返回触发消息的 L1 合约的 L2 地址别名。更多内容见可重试票据地址别名[25]。


如何使用 Arbitrum 网络
这就是两个重要的 Aribtrum 网络。你可以使用 MetaMask 等支持的钱包的wallet_addEthereumChain[26]功能指定添加网络,要不然用户需要手动添加网络。
.点击这里连接到 Arbitrum One[27]
.点击这里连接到 Arbitrrum Nitro Rinkeby[28]
.点击这里连接到 Aribitrum Nitro Goerli[29]
const params = [{
  "chainId": "42161", // testnet: "421611"
  "chainName": "Arbitrum",
  "rpcUrls": [
    "https://arb1.arbitrum.io/rpc"
    // rinkeby: "https://rinkeby.arbitrum.io/rpc"
    // goerli: "https://goerli-rollup.arbitrum.io/rpc"
  ],
  "nativeCurrency": {
    "name": "Ether",
    "symbol": "ETH",
    "decimals": 18
  },
  "blockExplorerUrls": [
    "https://explorer.arbitrum.io"
    // rinkeby: "https://rinkeby-explorer.arbitrum.io"
    // goerli: "https://goerli-rollup-explorer.arbitrum.io"
  ]
}]

try {
    await ethereum.request({
        method: 'wallet_addEthereumChain',
        params,
    })
} catch (error) {
    // something failed, e.g., user denied request
}
要在 Arbitrum 获得资金,可使用 https://bridge.arbitrum.io/ 提供的桥。

如何部署到 Arbitrum 网络中
现在你可以将 Arbitrum 主网添加到 Truffle 或 Hardhat 中,如下所示:
{
    arbitrum_mainnet: {
        provider: function () {
          return new HDWalletProvider(
            mnemonic,
            "https://arbitrum-mainnet.infura.io/v3/"
                + infuraKey,
            0,
            1
          );
        },
    },
    arbitrum_rinkeby: {
        provider: function () {
          return new HDWalletProvider(
            mnemonic,
            "https://rinkeby.arbitrum.io/rpc",
            0,
            1
          );
        },
    },
    arbitrum_goerli: {
        provider: function () {
          return new HDWalletProvider(
            mnemonic,
            "https://goerli-rollup.arbitrum.io/rpc",
            0,
            1
          );
        }
    }
}
推荐的一个好的做法是用 Hardhat 编写测试,用常规的本地配置,这样你可以快速运行测试,并有 console.log/stacktraces 功能可用。如果需要,可以在Infura[30]设置中激活 Arbitrum。

参考资料
[1]登链翻译计划: https://github.com/lbc-team/Pioneer
[2]翻译小组: https://learnblockchain.cn/people/412
[3]Tiny 熊: https://learnblockchain.cn/people/15
[4]最成功的: https://twitter.com/JackNiewold/status/1564374766441611266
[5]Merkle 树指南: https://learnblockchain.cn/article/5297
[6]UTXO: https://www.investopedia.com/terms/u/utxo.asp
[7]这里: https://www.learnplasma.org/en/learn/mvp.html#plasma-mvp
[8]addSequencerL2Batch: https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L235
[9]addSequencerL2BatchImpl: https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L338-L383
[10]enqueueSequencerMessage: https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Bridge.sol#L100-L132
[11]deliverToBridge: https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Inbox.sol#L419-L430
[12]enqueueDelayedMessage: https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/Bridge.sol#L152-L167
[13]forceInclusion(强制包含): https://github.com/OffchainLabs/nitro/blob/2678e0b57abfbcda8f21e844a94368eea5389348/contracts/src/bridge/SequencerInbox.sol#L89-L152
[14]这里: https://developer.offchainlabs.com/inside-arbitrum-nitro#dissection-protocol-simplified-version
[15]源代码: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/challenge/ChallengeManager.sol#L171
[16]WAVM 机器(Machine): https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Machine.sol#L18-L28
[17]模块: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Module.sol#L9
[18]验证它: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProofEntry.sol#L60
[19]Instruction(指令): https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/Instructions.sol#L7
[20]简单加法: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProverMath.sol#L217
[21]本地获取: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/osp/OneStepProver0.sol#L334
[22]StackFrame: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/state/StackFrame.sol#L9
[23]检查: https://github.com/OffchainLabs/nitro/blob/c191708c7847bf9e92c3c0a5263d31e4876d9e18/contracts/src/challenge/ChallengeManager.sol#L262
[24]支持Solidity: https://developer.offchainlabs.com/solidity-support
[25]可重试票据地址别名: https://developer.offchainlabs.com/arbos/l1-to-l2-messaging#address-aliasing
[26]wallet_addEthereumChain: https://docs.metamask.io/guide/rpc-api.html#wallet-addethereumchain
[27]点击这里连接到 Arbitrum One: https://chainlist.org/chain/42161
[28]点击这里连接到 Arbitrrum Nitro Rinkeby: https://chainlist.org/chain/421611
[29]点击这里连接到 Aribitrum Nitro Goerli: https://chainlist.org/chain/421613
[30]Infura: https://infura.io

用户评论