以太坊是具有内置图灵完备编程语言的区块链。任何人都可以使用以太坊智能合约来创建去中心化应用程序。

“以太坊虚拟机(EVM)是以太坊处理智能合约部署和执行的一部分”(Antonopoulos and Wood,2018)。

EVM 由基于堆栈的架构组成。为了部署智能合约,必须首先将所有高级以太坊智能合约代码编译为机器可读代码(称为字节码)。然后,EVM 通过后进先出堆栈安排来处理此字节码代码(一系列单字节操作码和可选参数)。此操作类似于 Java 虚拟机(JVM),其中每条指令都以单字节操作码和参数开始,如果有参数的话,则占用后面的未对齐的字节,值按大端排序给出(Scott,2009)。

本文的首要目标是解释以太坊基于堆栈的 EVM 的内部工作原理。解释了这些 EVM 基础知识,我们将开始看到以太坊有机地遵循了基于堆栈的虚拟机二进制指令格式,使其有望过渡到 WebAssembly 的光明未来。

让我们带着了解这些的目的,首先看一下 WebAssembly。

WebAssembly(简称 Wasm)

WebAssembly(Wasm)是一种可以在现代 Web 浏览器中运行新型代码。它带来了新功能并在性能上有重大进展。Wasm 旨在成为 C,C++和 Rust 等低级源语言的有效编译目标(MDN Web Docs,2019)。

以太坊 Web Assembly(简称 Ewasm)

以太坊 Web Assembly (Ewasm)是基于现代标准 WebAssembly 虚拟机构建的确定性智能合约执行引擎。Ewasm 是替代EVM 的主要候选方案,是Ethereum 2.0 “Serenity”路线图的一部分。甚至,还有人建议在以太坊主网上采用 Ewasm (ewasm,2019)。

为什么选择 Ewasm

EVM 的当前架构是释放其原始(raw)性能的最大障碍之一(GitHub EIP48,2019)。例如,虽然 256 位字长可促进本机哈希和椭圆曲线运算(Antonopoulos and Wood,2018),但也使从 EVM 操作码转换成硬件指令的过程难度过大,远超所需。提供更接近硬件映射的架构将大大提高以太坊的性能(GitHub EIP48,2019)。

除了性能增强方面,Ewasm 项目的设计目标之一是还支持跨多种语言和工具进行智能合约开发,即把 LLVM,C,C++,Rust,JavaScript 纳入开发周期。

从理论上讲,可以编译为 Wasm 的任何语言都可以用来编写智能合约。只要实现 Ewasm 合约接口(ECI)和以太坊环境接口(EEI)(ewasm,2019)。

智能合约

通过使用原始 EVM 架构逐步创建、编译和部署以太坊智能合约,来了解一下EVM 的基本要件。

以太坊智能合约就像以太坊执行环境中的“自治代理”,当这个合约被消息或交易“触发”时,总是会执行特定的代码段,并直接控制自己的以太币余额和自己的键 / 值存储,从而跟踪持久变量(Buterin,2013)。

诸如 SolidityVyperLity 之类的每种更高级别的智能合约源编程语言都维护着自己的编译器。智能合约的源代码可以编译为各种输出。包括但不限于应用程序二进制接口(ABI),字节码流和操作码。

智能合约编译

在本地计算机上安装编译器之前,建议您检查基于Web 的新编译器,例如SecondState 的BUIDL 环境这将为您节省大量时间。

让我们使用简单的存储源代码,并使用 SecondState 的 BUIDL 环境对其进行编译,如下图所示。

单击编译按钮将立即生成智能合约的 ABI 和字节码,如下所示。

如果要查看如何在命令行中(使用本地安装的编译器)执行此编译,请参阅本文末尾的附录 A。

分析部署字节码

如果我们查看字节码中的前 4 条指令,则会看到以下内容。

608060405234

如果我们查看以太坊黄皮书第 30 页上引用的这些值的助记符表示,我们将看到第一个指令(60)为 PUSH1。

δ

助记符右侧标有δ的列表示要通过 PUSH1 指令(在本例中为 0)从堆栈中删除的 item 数。

α

下一个标记为α的列,代表要添加的其他 item 数通过 PUSH1 指令放在堆栈上。在这种情况下为 1;一个字节 0x80。

在下面的示例中,我们现在可以看到第一条指令(0x60) PUSH1 将值 0x80 压入了堆栈,第二条指令(0x60) PUSH1 将值 0x40 压入了堆栈。

60806040…

PUSH1 0x80 PUSH1 0x40…

如果沿字节码移动,则会遇到值为 0x52 的指令。正如您在下面的黄皮书中所见,该指令具有 MSTORE 的助记符表示形式。

608060405234

如果再次查看助记符右侧的δ列,我们可以看到 MSTORE 将消耗堆栈顶部的两个 item。总计,MSTORE 将消耗堆栈中的前 2 个 item,但将 0 个 item 放回堆栈中。

在此阶段必须指出,MSTORE 是一项内存操作,其任务是将一个单词保存到内存中。请不要对这里使用“word”这个说法感到困惑。

EVM 的字长为 256 位(Antonopoulos and Wood,2018)。

这个“word”本身不是单词。比如,它可以是账户地址等。

MSTORE 通过首先从堆栈顶部消耗当前条目来开始其操作;即一个地址会被指定在 word 在存储器中的存储位置。在这种情况下,地址位置为 0x60。然后,MSTORE 使用堆栈中的下一个条目并将其(0x80)保存到预先指定的地址(0x60)。在此阶段,堆栈上没有剩余的条目。

下一条指令是(0x34)。它具有 CALLVALUE 的助记符表示形式。

608060405234

如果我们密切注意标记为α的列,作为其标准操作的一部分,我们将看到 CALL VALUE 将在堆栈中放置一项。但是,我们刚才提到堆栈当前为空,因此这引发了以下问题。CALL VALUE 如何获取要放在堆栈中的数据?

从黄皮书中可以看到,所有值在 30s (0x30 至 0x3e)中的指令都与环境信息有关。在这种情况下,CALLVALUE 从负责执行此字节码的消息调用中获取其所需的数据。与环境信息有关的另一个示例是指令 0x33,它具有 CALLER 的助记符表示。CALLER 指令能够自动获取启动字节码执行的以太坊帐户的地址。

部署与运行时字节码

在此刻,区分部署字节码和运行时字节码非常重要。如果查看附录 A.1 (获取部署字节码)和附录 A.2 (获取运行时字节码),您会注意到返回的字节码结果并不相同。

运行时字节码是调用已部署的智能合约的功能时执行的字节码。

部署字节码包含其他指令,这些指令仅与部署有关。

有趣的是,运行时字节码始终可以看作是代码的子集,它逐字地驻留在部署字节码内。如下图所示。

分析运行时字节码

每个智能合约功能都可以标识为 4 字节功能签名(在运行时字节码内部)。要计算功能签名,我们要先获取功能名称。在我们的例子中,让我们从功能“ set”开始。

除了函数名称“ set”之外,我们还采用了函数的输入参数数据类型(用逗号分隔并用括号括起来)。例如,在我们的简单情况下,我们以文本集(uint256)结尾。

注意:创建函数选择器文本时,请勿使用任何空格。

获得此信息后,我们将创建 sha3 哈希的十六进制表示,并将其截断为仅 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 实现细节。

Ewasm 实施

之前我们提到过,智能合约的源代码可以编译成各种输出。当然,从高级智能合约代码到 Ewasm 的路径是一项复杂的任务,可以通过不同的工具链采用多种多样的编译路径。

Second State 的开发人员最近建立了一个 Solidity 到 Ewasm 的编译器,称为 Soll

在以下以太坊重要的会议召开前,Second State 已完成了 demo 的原型,这一具有挑战性的任务:

  • Crosslink-在台北举行的全球领先的区块链研究人员和开发者会议

  • Devcon5-以太坊开发者国际会议,在日本大阪举行

Crosslink(2019 年 10 月)

Second State 的开发 Hung-Ying-Ying 因为Soll 编译器项目得到以太坊基金会奖金(与 Vitalik Buterin 一起在台北 Crosslink 活动中合照)。

Devcon5

如果您没有参加 Devcon5,以下视频概述了 Soll 的操作。即在新的以太坊 Ewasm 测试网上部署以太坊 ERC20 Solidity 智能合约,并与之交互。

https://www.youtube.com/watch?v=X-A6sP_HTy0

Devcon5 大会非常适合分享 SecondState 团队的最新 Ewasm 开发。在会议的第一天正式介绍 Second State 提出的以太坊 1.X 与以太坊 2.0 的解决方案之后,又进行了一次非正式的公开 demo 和讨论。

在 demo 后,与 Solidity 团队的 Christian Reitwiessner 以及其他人进行的讨论中,展现了 Second State 在最佳协作以及减少未来 Ewasm 领域里不同开发人员和软件项目之间重复工作方面的最佳发展方向。

在成功完成了从 Solidity 到 LLVM 到 Ewasm 的原型编译之后,Second State 参考 Christian 的宝贵意见,将努力实现执行 Yul 到 llvm 到 Ewasm 的编译路径。

Yul

Yul 是以太坊特定的中间语言。以太坊 Solidity 编译器(可能还有 Vyper 编译器)将来的版本会全面支持 Yul 作为中间语言。

Yul 被设计为 EVM 1.0,EVM 1.5 和 Ewasm 都能使用。Yul 的核心组件是函数、区块、变量、literals,for 循环,if 语句,switch 语句,表达式以及对变量的赋值(Solidity-Yul,2019)。

后端或目标是从 Yul 到特定字节码的转换器。每个后端都可以公开以后端名称为前缀的函数。Yul 为提出的两个后端保留 evm_和 ewasm_前缀(Solidity-Yul,2019)。

在这个领域已经完成了许多工作,因此,让我们简要了解一下从 Solidity 到 Yul 到 Ewasm 之路的已经熟知的部分。

为 Yul 编译 Solidity

Solidity 编译器具有一个特殊的 flag,可用于将 Solidity 源代码编译为 Yul 的中间表示(IR)。

为了提供此功能的示例,我们在命令行中使用以太坊的 Solidity 编译器,从上方将简单存储示例编译为这种 YUL IR 格式。

您将从下面的代码中看到我们可以使用 IR flag 执行此任务。

为 Ewasm 编译 Yul

从 Yul 出发的路径的简单概述包括以下步骤。

从 EVM-Yul 代码开始,我们需要:

将每个 256 位(32 字节)变量拆分为 4 个单独的 64 位(8 字节)变量。注意字节序差异。

创建一个库,该库使用内置的等效 Ewasm 作为用户定义的函数来实现每个 EVM 操作码,并使用常规的优化器。

更多 Solidity 编译示例

如果把上述 YUL IR 代码交给 Solidity 编译器处理,Solidity 可以生成漂亮的打印格式、二进制表达格式和文本表达格式。

这可以通过以下命令实现

solc / solc --strict-assembly --optimize〜/ simple_storage / simple_storage_yul_ir.txt

各个输出如下:

漂亮的打印格式

二进制表达格式

文本表达格式

内部工作机制的一个例子

Christian 在 Devcon5 演示的代码示例,展示了 Ewasm 风格的编译过程的内部工作原理。更具体地说,如此处显示,产生等同于 MSTORE 函数所要求的工作。如上面的 demo,使用原始 EVM,MSTORE 接受 2 个参数(首先是标准的 32 字节地址,其次是 256 位(32 字节)的单词)。但是,如您所见,以下代码显示了原始的 Solidity 智能合约 256 位变量被拆分为单独的 64 位变量。您还将注意到发生了字节序交换。

1
2
3
4
5
6
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))}

可在此网址获得:http://chriseth.github.io/notes/talks/yul_devcon5/#/11

字节序

出于以下原因,在为 Ewasm 进行编译时,交换字节序(存储和从内存中检索字节的字节顺序)至关重要。以太坊虚拟机规范采用大端字节排序(Wood,2014)。但是,Web Assembly 规范要求所有值都必须以 Little Endian 读写(webassembly.github.io,2019)。

随着这项重要工作的进展,目前正在考虑 / 讨论其他细节。为了简洁起见,以下的附录 B 中概述了其中的一些细节。

结论

智能合约代码和 Ewasm 之间有许多的路径。Yul 的使用将为当前的以太坊编译器提供目标终端(target endpoints),并为 llvm 到 Ewasm 编译器提供入口。从 Yul 到 llvm 到 Ewasm 的编译器将为任何与 Yul 兼容的智能合约语言(如 Solidity 和 Vyper)带来 Wasm / Ewasm 的基本优势。

使用 Yul 是一项重大胜利,因为它可以重复使用几乎所有的优化器组件(Reitwiessner,2019)。

除此之外,鉴于 Rust 等其他语言都将 LLVM 作为其主要的代码源后端(Rust-lang.github.io,2019),上述工具链路径将为其他编程语言成为以太坊的 Ewasm 智能合约生态系统的一部分打开大门 。

附录 A 在命令行中编译简单存储智能合约

如以上文章所述,可以在命令行中手动执行编译。例如,在安装 Solidity,Vyper 或 Lity 之后,您将可以访问适当的本地编译环境。

这是一个示例,说明了如何使用 SecondState 的 Lity 编译器来编译简单的存储智能合约,以获得各种不同的输出。

A.1 获取部署字节码

tpmccallum$ lityc/lityc --bin ~/simple_storage/simple_storage.sol

608060405234801561001057600080fd5b5060ec8061001f6000396000f3fe6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146085575b600080fd5b348015605957600080fd5b50608360048036036020811015606e57600080fd5b810190808035906020019092919050505060ad565b005b348015609057600080fd5b50609760b7565b6040518082815260200191505060405180910390f35b8060008190555050565b6000805490509056fea165627a7a7230582037a6182517eb7335095ca48cb7b895cbcbbdb824f48911f3f69fdc1869c7263e0029

A.2 获取运行时字节码

tpmccallum$ lityc/lityc --bin-runtime ~/simple_storage/simple_storage.sol

6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146085575b600080fd5b348015605957600080fd5b50608360048036036020811015606e57600080fd5b810190808035906020019092919050505060ad565b005b348015609057600080fd5b50609760b7565b6040518082815260200191505060405180910390f35b8060008190555050565b6000805490509056fea165627a7a7230582037a6182517eb7335095ca48cb7b895cbcbbdb824f48911f3f69fdc1869c7263e0029

A.3 获取操作码

tpmccallum$ lityc/lityc --opcodes ~/simple_storage/simple_storage.sol

PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xec DUP1 PUSH2 0x1f PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x49 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xffffffff AND DUP1 PUSH4 0x60fe47b1 EQ PUSH1 0x4e JUMPI DUP1 PUSH4 0x6d4ce63c EQ PUSH1 0x85 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x59 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x83 PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH1 0x6e JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0xad JUMP JUMPDEST STOP JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x90 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x97 PUSH1 0xb7 JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP3 DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH1 0x0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP INVALID LOG1 PUSH6 0x627a7a723058 KECCAK256 CALLDATACOPY 0xa6 XOR 0x25 OR 0xeb PUSH20 0x35095ca48cb7b895cbcbbdb824f48911f3f69fdc XOR PUSH10 0xc7263e00290000000000

A.4 获取 ABI

tpmccallum$ lityc/lityc --abi ~/simple_storage/simple_storage.sol

[{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

附录 B 待考虑 / 讨论的议题:

1.与 1.x EVM 的其余部分交互

除了典型的字节码操作,即堆栈,内存和存储访问之外,原始 EVM 还可以访问帐户信息,即地址和余额以及区块信息和当前 gas 价格(Antonopoulos 和 Wood,2018 年)。对存储的访问有助于正确操作。例如,有效交易的执行始于对状态的不可撤销的更改:发件人帐户的 nonce (Wood,2014 年)。

根据以太坊 1.x 路线图,关于 Ewasm 将如何与 EVM 状态的其余部分(即合约存储,以太坊余额等)相互作用的问题尚待解决。有一种方法是排除 Ewasm 代码直接访问 EVM 状态,但允许 Ewasm 在被调用时交换输入 / 输出(Docs.ethhub.io,2019)。

2.确定性行为

众所周知,Ewasm 是 Wasm 的子集,而 Wasm 具有一些不确定的特征。随着 Ewasm 的发展,需要一种方法来拒绝具有不确定性特征的任何合约(Docs.ethhub.io,2019)。目前看来,这可以通过一份定点合约来实现。前哨是系统合约(已启用创世纪或硬叉 Ewasm 的一部分)。前哨合约具有原始接口,该接口用于在部署期间进行合约验证。在发生意外问题时,前哨合约执行无效操作(调用常规故障)或在曾经检测到无效输入的情况下,前哨合约执行还原操作。

3.Gas 计量

Ewasm 合约部署和交互的 gas 的计算尚未确定。目前提出使用自动上限估计。可以对字节码执行静态分析,并且对于代码的子集,还可以计算已执行指令(虚拟气体)的上限(Docs.ethhub.io,2019)。

参考文献