Arbitrum 一个区块链上的区块链,我们要研究一下它
- 原文链接: https://soliditydeveloper.com/arbitrum-nitro
- 译文出自:登链翻译计划
- 译者:翻译小组 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
你听说过Arbitrum Nitro吗?新的WAVM可以实现Plasma,以一种超级高效的方式用于智能合约! 它使侧链拥有了以太坊主链的安全保证。到目前为止,Arbitrum已经是最成功的第二层之一,新的Nitro是它的一个重要升级。
让我们从头开始...
Merkle 树是这种扩容技术工作的基础。Merkle树的根是根hash。它是由所有叶子节点的原始值的哈希值创建而来。现在,两个叶子的哈希值组合起来创建一个新的哈希值, 一直向上组合,直到生成只有一个根哈希值的树。现在,一个Merkle证明是你向只知道根哈希值的人证明某个值实际上是这棵树的叶子之一的方法。
这里有一篇Merkle 树指南,如果你想更深入地了解这个话题,可以看看。
在以太坊中,一棵Merkle树是状态树,它包含所有的状态,如用户ETH余额,也包含合约存储。这使得我们可以在智能合约状态上创建Merkle证明!
所以可以用Merkle证明机制来证明一个智能合约具有某种状态。记住这一点稍后待用。
Plasma 使用智能合约和Merkle证明的组合。这些结合在一起,通过将这些交易从以太坊主区块链上转移到Plasma链上,实现快速和低廉的交易。与普通侧链相比,你不能在这里运行任何智能合约。
在Plasma中,用户以UTXO的方式在彼此之间发送交易,其中新的余额的结果作为Merkle树根在以太坊智能合约中持续更新。一旦Merkle树根在智能合约中被更新,即使Plasma链运营商是恶意的,它也能给保证用户资金的安全。Merkle树根囊括了许多笔资金交易的结果。如果Plasma运营商提交了一个无效的根,用户可以提出异议,并安全地取回他们的资金。更多细节请看这里。
但如前所述,它不能运行智能合约。所以不可能与Plasma进行Uniswap。
但这就是Arbitrum的用武之地,它是智能合约版的Plasma!
这里的核心思想其实很简单。就像在Plasma中,有一个运行所有交易的 layer2 链,及偶尔更新Layer1的Merkle根。在Arbitrum中,Merkle 根不是像普通plasma那样用于UTXO交易,而是包含智能合约的全部状态。或者说是所有正在使用的智能合约的全部状态。
是的,这意味着我们可以在Arbitrum上运行任意的智能合约! 简单归纳一下它的工作方式:
现在你可能意识到,这就是扩容的来源。你只在第1层运行有争议的、有欺诈证明的交易。所以,扩容的优势完全来自于你不会在第1层运行99.9%的交易这一事实。
Arbitrum Nitro背后的大概念是:
为了实际运行交易,我们需要原生 Geth、Geth与Wasm和Merkle证明。该架构看起来像这样。
新的交易可以通过三种方式被添加:
定序器(Sequencer)的正常打包包含
通过定序器,从L1的消息中加入
来自L1的消息被强制包含在L2上
在正常情况下,目前仍然中心化的定序器将把新的消息添加到收件箱中。这是通过调用addSequencerL2Batch完成的。函数检查发送者是否是保存的定序器,只有他被允许调用这个函数。
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里面调用桥接(bridge),将信息排队到收件箱。
bridge.enqueueSequencerMessage(
dataHash,
afterDelayedMessagesRead,
prevMessageCount,
newMessageCount
);
然后在桥接中调用 enqueueSequencerMessage,它只是向收件箱数组添加一个新的哈希值。
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);
}
消息也可以由任何人直接使用L1中的调用来添加。例如,当从L1到L2进行存款时,就很有用。
最终这将在在deliverToBridge内调用桥接的enqueueDelayedMessage。
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
);
}
第二种情况有一个问题。定序器可以从延迟的收件箱中获取消息并进行处理,但他也可以简单地忽略它们。这情况下,消息可能永远不会在L2中。而由于定序器仍然是中心化的,所以有第三个备份选项,叫做forceInclusion(强制包含)。
任何人都可以调用这个函数,如果在最小时间范围内定序器停止发布消息,它允许其他人继续发布消息。
那么,为什么会有延迟,为什么不允许用户总是立即强制包含交易呢?如果定序器有优先权,他可以给用户提供关于交易的软确认,带来更好的用户体验。如果有持续的强制包含,定序器就不能预先向用户确认将发生什么。为什么呢?好吧,一个强行加入的交易可能会使定序器计划发布的交易无效。
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的欺诈证明是如何工作的。
Arbitrum Nitro的新功能是WAVM。他们基本上重新使用Geth 以太坊节点代码,并将其编译为Wasm(或者说是Wasm的一个略微修改的版本)。Wasm是Web Assembly的缩写,是一个允许运行代码的环境,与平台无关。所以类似于EVM,但没有Gas。它也是一个网络范围的标准,所以它有更多其他语言的支持和更好的性能。因此,将用Go编写的Geth代码编译到Wasm中确实是可能的。
这个Wasm的执行对我们有什么帮助?
我们可以用它运行证明! 因为它是一个受控的执行环境,我们可以在Solidity智能合约内复制它的执行。这就是运行欺诈证明的要求。
Wasm与原生编译的代码相比,执行速度还是比较慢。但这里是Nitro的魅力所在。同样的Geth代码在证明时将被编译成Wasm,但在执行时则被编译成本地代码。这样,我们就可以得到两全其美的结果:以本地性能运行链,但仍然能够执行证明。
现在让我们来看看这些欺诈证明是如何详细工作的。我们需要什么?
最后一步是可选的,但如果我们只需要对单个的执行进行证明,则是一种性能改进。然而,它确实需要挑战者和被挑战节点之间的一些额外的交互步骤。我们不会去讨论这个细节,但你可以在这里阅读更多的内容。当然也可以直接在源代码中阅读。
但我们现在将详细介绍其他两个部分。
在交互式挑战过程中,最终挑战者会指向某单个执行的分歧。这个单一的执行有一个已知的执行前和执行后状态的Merkle根哈希值。执行后的根哈希值是被挑战的,所以最后我们会把它和我们自己执行得到的结果进行比较。执行前的哈希值没有受到挑战,因此是可信的。
它将被用于初始化WAVM 机器(Machine)。
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(模块)根,用它来验证模块的数据。
一个模块被定义为:
struct Module {
bytes32 globalsMerkleRoot;
ModuleMemory moduleMemory;
bytes32 tablesMerkleRoot;
bytes32 functionsMerkleRoot;
uint32 internalsOffset;
}
这里面持有的数据是WAVM机器数据的进一步Merkle根哈希值的形式。而挑战者也初始化了这些数据。
合约又只是验证它是否匹配之前的模块modulesRoot:
(mod, offset) = Deserialize.module(proof, offset);
(modProof, offset) = Deserialize.merkleProof(proof, offset);
require(
modProof.computeRootFromModule(mach.moduleIdx, mod) == mach.modulesRoot,
"MODULES_ROOT"
);
最后我们再对Instruction(指令)数据做同样的处理。
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机器,剩下的就是执行某个有分歧的的操作。现在这取决于我们需要运行的确切指令。
以一个简单加法为例,这是很简单的。
uint32 b = mach.valueStack.pop().assumeI32();
uint32 a = mach.valueStack.pop().assumeI32();
[...]
return (a + b, false);
基本上就是这样了。从机器堆栈中取前两个值,然后把它们加在一起。
让我们来看看另一条指令,一个本地获取指令:
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来自WAVM的初始化,在这里我们可以找到localsMerkleRoot。
struct StackFrame {
Value returnPc;
bytes32 localsMerkleRoot;
uint32 callerModule;
uint32 callerModuleInternals;
}
并通过Merkle证明,我们可以检索到该值并将其推送到堆栈。
最后,我们检查这个计算步骤产生的最终哈希值是否等于存储的哈希值。
require(
afterHash != selection.oldSegments[selection.challengePosition + 1],
"SAME_OSP_END"
);
只有当它不匹配时,证明才有效,我们继续。现在挑战者赢了,一个新的后状态将被接受。
Arbitrum完全支持Solidity,所以你可以照搬你的合约,只需注意一些问题:
blockhash(x)
返回一个加密不安全的伪随机哈希值.block.coinbase
返回0block.difficulty
返回常数2500000000000000block.number
/block.timestamp
返回L1区块的 估计值
msg.sender
的工作方式与以太坊上正常的L2到L2交易相同;对于L1到L2的 “retryable ticket(可重试票据) ”交易,它将返回触发消息的L1合约的L2地址别名。更多内容见可重试票据地址别名。这就是两个重要的Aribtrum网络。你可以使用MetaMask等支持的钱包的wallet_addEthereumChain功能指定添加网络,要不然用户需要手动添加网络。
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主网添加到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设置中激活Arbitrum。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!