作者:杨奕辉 创新平台实验室
项目参与者: 杨奕辉 强宇周 丁天宇
Damoclis-VM是创新平台实验室的旗舰项目Damoclis的子项目,是对智能合约和虚拟机的设计与实现。整个过程充满未知与挑战,我们从零开始调研,到合约语言和虚拟机方案的选型,到env api的设计和虚拟机实现,最后形成一个MVP。收获良多,也对区块链及智能合约的理解更为深刻,因此有必要为它写一系列文章,记录一下它的诞生。
《Damoclis-VM 诞生记》系列总共包含三篇:
- 《Damoclis-VM 诞生记(一)—— 关于智能合约的设计与思考》
- 《Damoclis-VM 诞生记(二)—— 字节码的选型与WebAssembly》
- 《Damoclis-VM 诞生记(三)—— 虚拟机的设计与思考》
智能合约字节码
字节码是一种已经经过编译,但与特定机器代码无关,需要解释器转移后才能成为机器代码的中间代码。每一个操作指令OP均用一个字节常量来表示。字节码的一个显著好处是可以跨平台,即只需要实现对应平台的解释器,即可执行字节码。
由于区块链本身是一个软件,而智能合约又是建立在区块链之上的应用层,为了让区块链系统能够执行智能合约,通常会想到以下三个方案:
- 区块链虚拟机直接解释执行合约代码。
- 合约代码编译成中间字节码,再由虚拟机执行。
- 合约代码直接编译至区块链系统的二进制中。
方案三显然是不可行的,这会丧失合约部署的灵活性。方案一相比方案二来说,减少了开发者编译代码的工序,但也牺牲了合约执行的效率,同时智能合约的开发语言固定,使得智能合约开发不具备扩展性。方案二则最具优势的:编译成字节码后,虚拟机可以针对字节码进行专门的优化,提升性能;合约开发语言不受限制,只需要实现能编译到同种字节码的编译器,便可作为智能合约的编程语言,具备扩展性。
字节码方案对比
EWASM设计文档针对目前主流的字节码技术做了比较好的对比总结,在此我们再进行一下归纳梳理——
WebAssembly
WebAssembly是一套可移植、体积小、加载快且兼容web的二进制字节码格式。它专为栈式虚拟机设计,开发者能够将高阶语言如C/C++, Rust编译成WebAssembly,从而可以部署在Web客户端或虚拟机中。
优势
- 轻量
- 性能优秀(接近原生速度)
- 可以被大规模部署
- AST式的字节码可以将metering操作从VM中解耦
劣势
目前只release了MVP版本,特性还没有stable。
LLVM IR
LLVM IR是著名的编译器工具链开源项目LLVM的中间语言,便于实现编译器优化,以及方便表示高阶语言如C/C++所需的构建和语义。
优势
- 经过充分测试
- 社区庞大
- 被Google的PNaCI使用。PNaCI是Google提出的一种可以在浏览器中执行native code的技术。Chrome在支持WebAssembly后,宣布放弃对PNaCI的支持。
- 被广泛部署
劣势
- 不轻量
- 不稳定
- 虚拟机实现需要处理大量接口(ISA)
JVM
所有权归Oracle。
RISC-V
RISC-V是基于精简指令集计算(RISC)原理建立的开放指令集架构(ISA),该项目始于2010年,由加州大学伯克利分销的David Patterson教授带领,目前已获得社区大力支持。在x86和ARM垄断的当下,RISC-V指令集完全开源,设计简单,易于移植于Unix系统。RISC-V可以满足从低功耗小型微处理器,到高性能数据中心(DC)处理器的实现要求。
优势
- 免费开源
- 相比x86和ARM,指令集非常精简(核心指令集只有41条指令)
- 支持广泛——GCC和LLVM已支持
劣势
- 不是为编译为x86和ARM而设计
Nervos是第一家基于RISC-V实现智能合约虚拟机的项目。详细可以阅读基于RISC-V打造的区块链虚拟机——CKB-VM诞生记(一)。笔者认为,Nervos CKB选用RISC-V的原因,可能主要是因为Nervos CKB是一条POW公链,本身须要矿机来运行全节点,可能希望在未来能让智能合约运行在专门设计的硬件上。
WebAssembly
通过对市面已有智能合约字节码方案的调研,以及对团队自身能力的考量,我们最终选择WebAssembly作为Damoclis-VM的合约中间字节码。
WebAssembly入门在这里不展开,MDN已经有了非常详细的文档。本章将主要介绍WebAssembly在设计虚拟机中所涉及核心知识点。
核心知识点
函数(func)
WASM当前仅支持四种类型: i32
,i64
,f32
,f64
。用于智能合约后只剩下i32
和i64
(不支持浮点数)。
一个正常的函数签名如下,它接受两个i32
参数,返回一个i64
整数:
(func (param i32)(param i32)(result i64)...)
内存(memory)
由于WebAssembly起源于网络编程,其内存模型被设计成线性数组(从javascript角度来看就是ArrayBuffer
)。这种设计提供了三个特性:
- 通过共享内存,在虚拟机环境和wasm中传递数值。
- 内存管理交由虚拟机,虚拟机需要实现垃圾回收机制。例如,在浏览器中使用wasm,由于内存本身是js对象
ArrayBuffer
,当wasm实例离开作用域时,其对象会被GC。 - 内存数组带有边界控制,即天然不允许越界访问,实现内存资源的隔离。
表格(table)
WASM中的表格实际是一个指针数组,用于存放函数指针,其作用是实现运行时动态调用函数。目前仅支持anyfunc
类型的元素。table类型使我们能够在避免遭受各种攻击的方式下使用函数指针特性。
table实际是一个位于Wasm内存之外的数组,它的元素值就是对函数的引用。
在底层,引用就是内存地址。但它不在WebAssembly的内存中,因此WebAssembly无法看到这些地址,但可以访问到数组索引。
call_indirect
指令即是通过数组索引去调用对应函数。
Wast——WebAssembly的文本格式
理解WebAssembly的文本格式语法非常重要,这使得你不需要反编译就能快速理解wasm字节码的含义。文档
WASM编译器
官方
社区
- AssemblyScript - TypeScript编译至WebAssembly [ √ ]
二次开发
智能合约的编译器不仅仅须要将合约代码编译成解释器可执行的字节码,还须要在编译过程中添加一些特性:
- 添加入口函数,即根据开发者编写的合约方法,须在入口函数中自动添加合约方法的dispatcher。
- 自动生成ABI,便于虚拟机在执行字节码前对参数进行验证。
- 字节码优化,剔除部分无用的运行时代码,减少体积。
添加入口函数
在Damoclis-VM中设计了apply
函数作为wasm字节码模块的入口函数。虚拟机将用户发送的合约方法和合约参数传入相应合约的apply
函数,它包裹了一个dispatcher
,帮助找到并调用相应合约方法。一个apply
函数案例如下:
export function apply(){
actionName = get_action_name() // get action name from vm
actionData = get_aciton_data() // get parameters from vm
switch(acitonName){
case "func1": // if call "func1"
call func1(actionData);
break;
case "func2": // if call "func2"
call func2(actionData);
break;
case "func3":
...
default:
throw new Error()
}
}
ABI生成
ABI(Application Binary Interface)实际是合约的接口说明,描述了合约的字段名称、字段类型、方法名称、参数名称、参数类型、方法返回值类型等。
ABI可由开发者自行编写,也可通过编译器自动生成。我们推荐使用编译器生成的ABI,能够保证正确性。ABI与合约字节码同时部署,可以单独更新,不要求ABI与合约字节码强一致。ABI的用途主要有两个:
- 供用户参考的接口文档。
- 虚拟机执行合约前,对调用方法和传参进行验证。
在Damoclis-VM中,我们参照了EOS的ABI设计。一个ABI样例如下:
{
"version":"damoclis-0.1",
"types":[ // 自定义数据类型
{
"new_type_name":"account_name",
"type":"name"
}
],
"structs":[ // 声明各action需要传入的参数,以及各table的数据结构
{
"name": "transfer",
"base": "",
"fields": [
{"name":"from", "type":"account_name"},
{"name":"to", "type":"account_name"},
{"name":"quantity", "type":"uint64"},
{"name":"memo", "type":"string"}
]
},
{
"name": "account",
"base": "",
"fields": [
{"name":"balance", "type":"uint64"},
{"name":"frozen", "type":"uint8"},
{"name":"whitelist", "type":"uint8"}
]
}
]
"actions":[{
"name": "transfer", // 在智能合约中定义的操作
"type": "transfer", // 在structs中声明的数据结构名
"ricardian_contract": "" // 可选参数,理查德合约
}],
"tables":[{ // 列出合约建立的数据表名称、主键名称与类型。在structs中定义了字段名称与类型
"name": "accounts",
"type": "account"
}]
}
WASM解释器
WebAssembly要在区块链虚拟机中运行,离不开解释器。自行实现一个Wasm解释器成本过高,因此复用社区已有的方案是我们的首选。
项目名 | 实现语言 |
---|---|
WAVM | C++ |
wasmer | Rust |
lucet | Rust |
wagon | Go |
life | Go |
go-ext-wasm | Go |
Damoclis区块链采用Go语言编写,因此解释器自然也须要采用基于Go语言的方案。
- wagon:最早的wasm解释器方案,700+stars, 在公开的issue中没有发现未解决的bug。且API暴露非常充分,能够用于虚拟机实现。
- life:1000+stars,(官方声称)性能优于wagon,但在公开的issue中有两个重要bug迟迟未解决。
- go-ext-wasm:著名wasm解释器wasmer的子项目,6月份刚开源,API很简陋,需要较大的改造成本。
经过综合考虑,我们决定选用成熟稳定、API完备的wagon作为wasm解释器的方案。
推荐阅读
[1] 使用Javascript创建WebAssembly模块实例