从EVM到Ewasm,硬核对比以太坊虚拟机……
来源 | Hackernoon
编译 | Shawn
责编 | Carol
出品 | 区块链大本营(blockchain_camp)
以太坊是一种内置图灵完备编程语言的区块链。任何人都可以利用以太坊的智能合约创造去中心化应用。
以太坊虚拟机由基于栈的体系结构组成。为了部署智能合同,所有高级的以太坊智能合同代码必须首先编译成机器可读的代码(称为字节码)。
这个字节码代码(一系列单字节操作码和可选参数)然后由EVM通过后进先出的栈安排进行处理。该操作类似于Java虚拟机(JVM) ,其中每条指令都以单字节操作码和参数开始,如果有参数的话,则占用后面的未对齐的字节,值按大端排序给出。
本文将解释以太坊基于栈的 EVM 的内部工作原理。解释了这些 EVM 的基本原理后,我会开始看到以太坊对基于栈的虚拟机的二进制指令格式的有机整合,它已经准备好迎接Web Assembly的光明未来了。
首先,让我们先来探讨一下Web Assembly。
WebAssembly (Wasm)是一种可以在现代网络浏览器中运行的新型代码。它带来了新的特性和巨大的性能提升。Wasm旨在为 C、C++ 和Rust等低级源代码语言提供有效的编译目标。
而Ethereum Web Assembly (Ewasm)则是一种建立在现代、标准的 WebAssembly 虚拟机上的确定性智能合同执行引擎。EWasm 最有潜力取代EVM,成为以太网2.0 “Serenity” 路线图的一部分。而且,还有人建议在 以太坊主网上采用Ewasm。
EVM vs. Ewasm之EVM
Evm 的当前架构是释放其原始性能的最大阻碍之一。例如,虽然256位字节大小有利于原生的哈希和椭圆曲线算法,但也使得从 EVM 操作码到硬件指令的转换变得更为困难;一种更易于向硬件指令转换的架构将大大提高以太坊的性能。
除了性能增强方面,Ewasm 项目的设计目标之一是支持更广泛的语言和工具,进行智能合同开发,例如将LLVM、C、C++、Rust和JavaScript整合到开发周期中。
理论上,任何可以编译成 Wasm 的语言都可以用来编写智能合同。只要它实现了Ewasm合约接口(ECI)和以太坊合约接口(EEI)。
举个例子,让我们先看看如何使用原始的EVM架构来创建、编译和部署以太坊智能合约,来理解 EVM 的基础知识。
以太坊智能合约就像生活在以太坊执行环境中的「自主代理」 ,在被某种消息或事务「触发」时总是执行一段特定的代码,并直接控制它们自己的以太平衡和键值存储,以跟踪持久变量。
高级智能合同的源编程语言,如Solidity、Vyper和Lity都有自己的编译器。智能合约的源代码可以被编译成各种输出,包括但不限于应用二进制接口(ABI)、字节码流和操作码。
EVM下的智能合约编译
在本地计算机上安装编译器之前,我建议你先检查一下新的基于Web的编译器,如SecondState的BUIDL环境。这将为你节省大量的时间。
使用SecondState的BUIDL环境来编译下图中这段简单的存储源代码。
单击编译按钮将立即生成智能合约的 ABI 和字节码,如下所示。
部署字节码分析
查看字节码的前4条指令,我们会看到以下内容:
608060405234
如果查看这些值的助记符表,请参阅《以太坊黄皮书》第30页,我们将看到第一条指令(60)是 PUSH1。
在助记符右边列头为δ的列,表示用 PUSH1指令从栈中移除的条目数(在本例中为0)。
后面列头为α的列,表示通过 PUSH1指令要放置在栈上的其他条目的数量。在本例中为1; 单个字节0x80。
在下面的示例中,我们现在可以看到第一条指令(0x60) PUSH1将0x80的值推入栈,第二条名为 PUSH1的指令(0x60),将0x40的值推入栈。
60806040…
PUSH1 0x80 PUSH1 0x40…
如果我们沿着字节码移动,就会看到值为0x52的指令。正如您在下面的黄皮书中看到的,此指令的助记符表示形式为MSTORE。
608060405234
再看助记符右侧δ 那列,我们可以看到MSTORE将取走栈顶部的两个条目。总的来说,MSTORE 将取走栈顶部的2条目,将0个条目放回栈。
重要的是,在这个阶段,MSTORE 是一个内存操作,它的任务是保存一个字节到内存中。请不要对“字节“这个词感到困惑。
Evm 的字节大小为256位(Antonopoulos 和 Wood,2018)。
“字节”本身不是一个词。例如,它可以是一个帐户地址等。
Mstore 首先从栈顶部取走当前条目,即一个指定字节存储在内存中的位置的地址。在本例中,地址位置为0x60。然后 MSTORE 取走栈中的下一条目,并将其(0x80)保存到预先指定的地址(0x60)。此时,栈上就已经没有任何条目了。
下一条指令是(0x34)。它的的助记符表示形式是 CALLVALUE。
608060405234
仔细观察 α 那一列,可以看到 CALLVALUE 将在栈上放置一个条目,这是其标准操作的一部分。但是,我们刚才提到堆栈当前是空的,因此这就有了以下问题。CALLVALUE如何获取它要放在堆栈上的数据?
正如《以太坊黄皮书》中所说,值在30s(0x30到0x3e)范围中的所有指令都与环境信息相关。在这种情况下,CALLVALUE 从负责执行这个字节码的消息调用中获取所需的数据。
另一个与环境信息有关的例子是指令0x33,它的助记符表示为CALLER。CALLER指令能够自动获得启动字节码执行的以太坊帐户的地址
部署 vs 运行时字节码
到这里,区分部署字节码和运行时字节码非常重要。运行时字节码是在调用已部署的智能合约的函数时执行的字节码,部署字节码则包含额外的指令,这些指令只与部署有关。
有趣的是,运行时字节码始终可以被视为代码的一个子集,它驻留在部署字节码中。下图说明了这一点。
运行时字节码分析
每个智能合约函数都可以(在运行时字节码中)标识为一个4字节的函数签名。要计算函数签名,我们首先要获取函数的名称。在本例中,我们从 “ set” 函数开始。
除了函数名“ set”之外,我们还将获取函数的输入参数数据类型(用逗号分隔,用括号括起来)。例如,在这个简单示例中,我们最终得到了文本集(uint256)。
有了这些信息后,我们就可以创建sha3 hash的十六进制表示,并将其截到只剩4字节。以下是web3.js 和 web3.py的示例。
selectorHash = "0x" + str(web3.toHex(web3.sha3(text="set(uint256)")))[2:10]
var selectorHash = web3.sha3("set(uint256)").substring(0,10)
上述两个命令都将返回以下签名: 0x60fe47b1,我们可以很容易地在部署和运行时字节码数据中找到它。
在本小节中,我们了解了字节码中的每条指令是如何执行的。接下来,让我们再来看看以太坊Ewasm是如何实现的。
EVM vs. Ewasm之Ewasm
上文提到,智能合约的源代码可以编译成各种输出。当然,从高级智能合约代码到 Ewasm是一个复杂的任务,可能会涉及到不同的toolchains,以及很多不同的编译方法。
最近,SecondState 的开发者们一种叫做Soll的语言编译器.
SecondState团队在刚刚结束的Devcon5会议上分享了Ewasm的最新进展。在会议的第一天的正式演讲之后,又进行了一次非正式的、公开的演示和讨论。
在演示完毕后,SecondState团队与Soliity维护者Christian Reitwiessner等人进行了讨论,提出了 SecondState 未来Ewasm开发工作的计划,旨在实现最好的合作,减少不同开发者和软件项目之间的重复工作。
成功地进行了Solidity到LLVM再到Ewasm的原型编译后,SecondState期望利用Yul实现从Yul到llvm再到Ewasm的编译。
这里有必要说一下,Yul是一种只适用于以太坊的中间语言。未来版本的以太坊Solidity编译器(可能还有以太坊Vype 编译器)将全面支持Yul作为一种中间语言编译器。
后端或终端作为翻译器,将Yul转换为特定字节码。每个后端都可以暴露前缀为后端名称的函数。Yul为EVM和Ewasm这两个后端保留evm_ 和ewasm_这两个前缀。
在从Solidiity到Yul再到Ewasm的编译上,我们已经取得了很多成果,接下来让我们简要地了解一下这些成果。
从Solidity到Yul的编译
Solidity编译器有一个特性,可用于将Solidity源代码编译成Yul的中间表示。
为了展示关于这个特性的示例,我们在命令行中使用以太坊的 Solidity编译器将上文中的存储示例代码编译成这种 YUL IR 格式
以下代码,展示了如何使用IR 标识执行这个任务。
从Yul到Ewasm的编译
从 Yul到Ewasm的编译可以简单概述为以下步骤:
首先要处理EVM-Yul代码;
将每个256位(32字节)变量拆分为4个64位(8字节)的变量,处理字节顺序偏差;
创建一个库,使用对应的Ewasm内置函数,将每个EVM opcode 作为用户自定义函数实现;
使用Regular Optimizer。
当然,还有其他用Solidity编译的方法,比如:如果输入上面的Yul IR,Solidatity 还可以生成漂亮的打印源、二进制表示和文本表示。通过以下命令实现。
solc/solc --strict-assembly --optimize ~/simple_storage/simple_storage_yul_ir.txt
输出如下:
Christian还在Devcon5会议上进行的代码演示揭示了Ewasm编译过程的内部原理。具体来说,如下图所示,该过程需要生成相当于MSTORE函数的对等函数。
如上文所述,使用原始 EVM,MSTORE 接收2个参数。然而,从下面的代码可以看出,原始的Solidity智能合约的256位变量被分割成单独的64位变量。另外,还发生了字节顺序互换。
function mstore(x1, x2, x3, x4, y1, y2, y3, y4) {
let pos := u256_to_i32ptr(x1, x2, x3, x4)
i64.store(pos, endian_swap(x1))
i64.store(i64.add(pos, 8), endian_swap(x2))
i64.store(i64.add(pos, 16), endian_swap(x3))
i64.store(i64.add(pos, 24), endian_swap(x4))
}
关于字节顺序互换
在向 Ewasm 编译时发生的字节顺序互换(从内存中存储和检索字节的字节顺序)是至关重要的。原因如下:以太坊虚拟机规范使用big-endian(高位优先)字节排序。然而,Web Assembly规范要求所有的值都以little endian(低位优先)进行读写。
总结
在智能合约代码到Ewasm,还有许多潜在的实现方法。Yul的使用将为当前的以太坊编译器提供一个目标端点,还将为llvm到Ewasm编译器提供一个切入点。Yul到 llvm到Ewasm编译器将为所有与Yul兼容的智能合约语言带来Wasm和Ewasm的基本优势,比如Solidity和Vyper。
因此,使用Yul是一个很大的优势,因为它可以再利用几乎所有的优化器组件。
此外,考虑到Rust等其他语言使用LLVM作为其主要代码源后端,上文中的工具链(toolchain)路径将为其他编程语言打开大门,成为Ethereum智能契约生态系统的一部分。
猛戳"阅读原文"有惊喜哟
老铁在看了吗?👇