Ewasm 是以太坊2.0时代用于替换evm虚拟机的一个新的虚拟机实现,它汲取了webAssembly 的优势,将其应用到以太坊2.0虚拟机的实现,可以方便开发者利用多种语言实现智能合约并且运行在以太坊平台
在介绍Ewasm之前,还是得先介绍下WebAssembly,
WebAssembly(或Wasm)是一种新的,可移植的,节省大小和加载时间有效的字节码格式。WebAssembly旨在通过利用各种平台上可用的通用硬件功能以本机速度执行。WebAssembly目前被W3C社区组织设计为开放标准。
WebAssembly介绍、WebAssembly设计、 WebAssembly指令集
WebAssembly定义了一个指令集,以及 中间源格式(wast) wast其实是一个wat 的改进版 ,这个是人类可以读的文件,并且可以进行更改,方便进行调试,和生成二进制编码格式(wasm)。
WebAssembly具有一些更高级别的功能,import export module,它的moudle导出后续应该也可以被javascript 可以使用,这样扩展性也就变得更强。
LLVM 包含一个WebAssembly后端来生成Wasm输出,这意味着只要能被llvm编译成wasm的语言,即可在浏览器等其他地方大放异彩。
wasm是可以在栈式虚拟机上运行的一种二进制指令格式,设计的首要目标是性能,很适合用在区块链项目中。
可扩展性好
32/64位的操作符
支持更多的高级语言(C,C++, Rust,Go等)
由W3C领导,Google,Apple,MicroSoft等大公司支持,更广阔的社区
主流浏览器支持,已有项目支持在非浏览器环境运行(wabt,wavm等)
EVM 一直是被当做eth1.0存在的, 暂且抛开各种代码逻辑层面的问题和solidity的问题不说,就光是解释速度就能被无数人诟病,而Ewsam 则采用的 更加高效的wasm 指令集,它更加接近cpu指令集。Ewasm 是Eth2的一个重要模块,它将负责改变以太坊交易执行,交易合约语言独立的现状。它将变得更加高效,执行速度更快,而且有丰富的语言和社区支持。 Ewasm 其实是wasm 的一个子集,因为它限制了合约的执行环境必须在以太坊标准环境中,为了达到这样的要求,以及为了兼容EVM之前的合约,Ewasm目前的设计如下:(此设计后续会不断变换)
为了兼容EVM1的合约,Ewasm必须做到以下几点
无论什么时候从state 加载合约,必须检验合约签名。如果合约是当前的(新的),则执行当做Ewasm 合约执行,否则按照EVM1 合约执行
如果当前客户端不支持EVM1 ,则使用EVM Transcompiler
工具来将EVM1合约转换为Ewasm2合约,目前solidity还没有这样的成熟工具出现,之前有过一个evm2wasm
项目,现在代码也一年多没更新了。因为官方在设计的时候发现的问题太多。社区里面有人提到说有可能会先转为YUL 代码,然后从YUL 转为wasm代码。
部署wasm合约时,要有一个预编译内置合约(合约名称Sentinel Contract)对合约进行检查,要求合约符合EVM2.0的要求,比如不能包括float类型,不能有限定外的操作。它还会对wasm 字节码进行注入gas消耗,然后将注入gas后的字节码返回。
Ewasm 合约也是遵循wasm字节码规范的,可以认为它是wasm的一个子集
ethereum
命名空间,并且具有与EEI中指定的函数直接对应的函数签名和名称,这确保了以太坊合约执行始终是一个沙盒环境。debug
名称空间,可以进行信息打印用来合约调试,但在生产系统部署时必须去掉debug 命名空间。EEI 将以太坊环境的核心api暴露给Ewasm环境,这样Ewasm环境就可以通过api进行状态存取。以太坊的module 将以以太坊客户端的nvtive 语言实现。所有参数和返回值必须采用32或者64位 integers. 不允许出现浮点数。
EEI 定义了下面的数据类型和API,数据类型又分为以太坊数据类型和webAssembly数据类型
以太坊数据类型
bytes
: 不定长bytes 数组
bytes32
: 32字节定长数组
address
: 20字节定长数组
u128
: 128位无符号整数,小端存储
u256
: 256位无符号整数,小端存储
webAssembly 数据类型
i32
: 等同于webAssembly 中的i32i32ptr
: 等同于webAssembly 中的i32,但是是一个指针,指向webAssembly的内存偏移量i64
: 等同于webAssembly 中的i64API
useGas
减掉消耗的gas
getAddress
获取正在执行合约的账户地址,并将其存储在给定的内存偏移量里面
getExternalBalance
获取给定地址余额,并将其存储在给定的内存偏移量中
getBlockHash
获取指定区块hash,并将其存储在给定的内存偏移量中
call
给定gas ,address, value, dataoffset, datalenght 进行函数调用
callDataCopy
将call的data拷贝进内存
getCallDataSize
获取call 的 data的大小
callCode
功能类似codedelegate,目前已经接近弃用
callDelegate
使用被调用者的代码进行调用其他合约,但是保留原始的调用者和value值
callStaic
将包含任意数据的消息发送到给定的地址路径,但不允许状态修改。这包括log
,create
,selfdestruct
和call
具有非零值
storageStore
将内存中256位的一个值持久化进storage
storageLoad
从storage 中将256位的值load进内存
getCallValue
获取call 中的value
codeCopy
拷贝当前环境中的code进内存
getCodeSize
获取当前环境中的code的size
getBlockCoinbase
获取区块受益者地址,并加载进内存
create
根据指定值创建新合约
getBlockDifficulty
获取区块难度
externalCodeCopy
获取账户中的code
getExternalCodeSize
获取账户中的code size
getGasLeft
返回当前剩余gas
getBlockGasLimit
获取区块gas 限制
getTxGasPrice
获取当前环境交易gas price
log
在当前环境下创建log
getBlockNumber
获取区块号
getTxOrigin
获取交易最开始账户地址,永远不可能是合约账户地址
finish
设置执行完成之后的返回数据,这将会立即终止执行
revert
设置执行完成之后的返回数据。这将立即停止执行并将执行结果设置为“恢复”。
getReturnDataSize
获取当前返回数据缓冲区的大小到内存。这包含从执行的最后一个返回的数据call
,callCode
,callDelegate
,callStatic
或create
。
returnDataCopy
将当前返回数据缓冲区复制到内存。这包含从去年执行的返回据call
,
callCode
,callDelegate
,callStatic
或create
。
selfDestruct
将帐户标记为以后删除,并将剩余余额提供给指定的受益人地址。这将导致webAssmebly trap,执行将立即中止。
getBlockTimestamp
获取区块时间戳
系统合约是定义为合约的接口,ewasm VM实现可以选择本地实现这些接口,也可以依赖于使用ewasm编写的实现。这些合约中的每一个都有一个预定义的地址,可以合约调用来执行。
哨兵合约
地址: 0x000000000000000000000000000000000000000a
前面提到每个新部署的ewasm合约必须经过哨兵合约进行处理,哨兵将执行三个非常重要的处理步骤:
验证ewasm语义
注入gas计量代码
将处理后的代码返回给部署者
Evm 代码转换合约
地址:0x000000000000000000000000000000000000000b
它的主要作用就是将evm1.0的合约的bytecode转换为ewasm 合约的bytecode
以太坊预编译合约
以太坊黄皮书里面定义的EVM1.0的预编译合约也将同样适用于ewasm
给定一组操作和每个操作的相应成本集合,可以通过总计每个操作的执行成本来确定地运行任意数量的成本单位的计算。我们将成本单位称为gas,它作为计算时间的估计。
在wasm 中进行gas注入,这里有个专门项目进行实现
计量主要包括分支计量和内存消耗计量。
分支计量会提前定义好导致分支的一些操作,例如
const branching_ops = new Set(['end', 'br', 'br_table', 'br_if', 'if', 'else', 'return', 'loop']
还有一个 cost_table 记录了与之相对应的gas 消耗
cost_table = {
'op': cost
}
内存计量:
内存计量主要是为初始分配内存以及分配增加内存进行计费
关于Metring 的详细设计可以看Metring
提供ewasm合约语义和以太坊接口的规范
提供EVM到ewasm合约的转换器,最好是由一个ewasm合约来完成
提供gas 计量,最好也是一个ewasm 合约来实现
提供用于执行ewasm合同的VM实现
在Solidity编译器中实现ewasm后端
提供在Rust中编写合约的库和说明
提供用C编写合约的库和说明
定义可移植,大小和加载时间有效的二进制格式作为编译目标,可以通过利用各种平台(包括移动和物联网)上的常用硬件功能,编译为以本机速度执行。
为了读者更方便的理解Ewasm示例合约代码,这里首先介绍WebAssembly 相关的知识点
WebAssembly 文本格式是以S-表达式表示的,S-表达式是用于描述树状结构的一种简单文本形式,其特征是树的每个结点均被一对圆括号“(...)” 包围,结点可以包含子结点。
最简单的WebAssembly 模块如下:
(module)
在S-表达式中,这表示一颗根结点为module的树,尽管module什么都没有,但是这仍然是一个合法的WebAssembly 模块。
在WebAssembly文本格式中,左括号“(” 后紧跟的是结点的类型,如刚才例子中的module, 随后是由分隔符(空格、换行符等)分割的属性和子结点列表。下面的代码描述了一个WebAssmebly 模块,该模块包含了一个什么也没做空函数,以及属性为1的memory:
(module
(func)
(memory 1)
)
在WebAssembly文本格式中,结点类型如下表
结点 | 类型 |
---|---|
module | WebAssembly模块根结点,即module |
memory | Memory |
data | Memory 初始值 |
table | Table |
elem | Table 元素初始值 |
import | 导入对象 |
export | 导出对象 |
type | 函数签名 |
global | 全局变量 |
func | 函数 |
param | 函数参数 |
result | 函数返回值 |
local | 局部变量 |
start | 开始函数 |
下面展示了一段可以正常在以太坊测试网络执行的wasm合约,我们将它转为下面wast的可读形式进行分析
(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(import "ethereum" "storageStore" (func (;0;) (type 0)))
(func (;1;) (type 1)
i32.const 0
i32.const 32
call 0)
(memory (;0;) 1)
(export "memory" (memory 0))
(export "main" (func 1))
(data (i32.const 32) "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\01"))
首先是定义了一个webAssembly模块,接下来定义了两种函数签名, 第一个函数签名索引为0 , 该函数接受两个 i32 类型的参数, 没有返回值。 第二个函数签名索引为1, 没有参数,也没有返回值。
接下来导入了一个以太坊EEI模块中的storageStore, 用于账本数据存储,该函数在定义的函数列表索引为0, 函数签名类型为之前定义的type 0,该函数函数体在以太坊代码里面用navite code实现。
然后定义了一个函数索引为1 的,函数签名类型为 type 1 的 函数。 这个函数的函数体 先使用 i32.const 0
将 内存地址偏移量为0 压栈。 然后 使用i32.const 32 将从内存中偏移量0开始,长度为32字节的数据压入栈。
然后调用 函数0, 函数0就是 storageStore 的具体实现,这个代码会通过wasm虚拟机 执行调用, 该函数会从栈上取出内存偏移量和数据长度,并将数据写入账本。
(memory (;0;) 1)
定义了一段在webAssmebly中使用的内存, 这段内存的索引为0, 初始大小为1页, 也就是64KB = 65535字节。
然后将memory 导出, 导出的内容索引为0的memory
然后将main 导出,导出的函数为 索引为1 的函数。
data 部分是 memory 初始化后存储的值, 代码段表示 在 内存的偏移量为32字节处存储i32类型的值为1。
Ewasm项目的设计目标之一是使用一系列工具支持各种语言的智能合约开发,包括现有的成熟工具链,如LLVM(C / C ++,Kotlin,Rust)和JavaScript /TypeScript。理论上,任何可以编译为Wasm的语言都可以用来编写智能合约(只要它实现了EEI和ECI)。
Ewasm testnet支持执行EVM 1.0(拜占庭)字节码和 Ewasm字节码。链ID设置为0x42(十进制66)
还有两个技术差异:
以太坊目前有个简单的测试网络,可以供开发人员进行测试ewasm合约。但是目前针对c和c++相关的资料较少,大都是一些个人项目,而且不一定能测试通过。
随着Eth2.0的发展以及为了支持Ewasm, 以太坊的客户端逐渐和VM 划分开来,通过EEI 和ECI 就可以实现这种划分,这样也是得客户端和VM更加丰富多样化,只要实现了相应的API 就可以进行相应的组合。
现在主流的本地测试方法如下,这里只介绍可能跟我们有关系的项目,像Rust python 之类的客户端,不在这里一一列举,感兴趣的读者可以自己探索。
Hera 虚拟机与客户端组合图:
Geth 客户端要选择特定的go-ethereum 分支,这里需要选择ewasm项目下的testnet里面fork的这版本,官方的go-ethereum目前并不支持。
EVMC是以太坊虚拟机(EVM)和以太坊客户端之间的低级别ABI。在EVM方面,它支持经典的EVM1和Ewasm。在客户端,它定义了EVM实现访问以太坊环境和状态的接口。EVM-C的作用是分离客户端和EVM,客户端在执行合约中只负责与链上信息的交互,把对合约的解析和执行任务交给EVM,他们之间的通信接口就是EVM-C。
Hera 正是实现了EVMC 的相关接口的一个虚拟机。Hera内部包含了wasm VM,目前wasm的VM有多个实现版本,Hera目前完全支持Bineryen,WABT,WAVM(EOS也使用WAVM)。
不管是哪个版本的客户端,只要是使用EVMC接口就可以与Hera进行交互。总之只要执行指令过程中有与链上的信息交互就通过接口与客户端通信,其他low level 的 wasm指令由内置的VM执行。
目前在geth启动参数里面配置hera的动态链接库路径,就可以实现 这种模式。
hera工作流程:
这个Geth 客户端跟 上面是一样的,都不是官方的go-ethereum 客户端,github上有个项目已经实现了Geth 调用Wagon 解释器的geth,这两个是可以无缝结合的,因为都是golang实现的。
wagon 是一个golang 实现的解释器,它可以实现下面两个功能:
wasm
二进制文件wagon
不关心wasm
二进制文件的生成; 这些文件应该用另一工具来制造(例如WABT或binaryen) wagon
可提供一个实用程序来产生wasm
从文件wast
或wat
文件(并且反之亦然)。
wagon
主要目标是提供构建块,以便能够为Go代码构建解释器,可以嵌入任何Go程序中。
geth+wagon 工作流程
wasm合约的执行过程在没有真正到达wasm vm执行代码代码之前都是与evm的种种行为保持一致的,所以它对外的接口也是两个大类,一个是create
一个是call
,接下来分别详细解释wasm合约的执行流程
Geth-wagon 优缺点
优点:
wagon解释器成熟度高,代码容易实现,而且采用wagon解释器的项目也比较多
后续可能会有以太坊官方实现版本,可以随时更新跟进
缺点:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!