临界学院

临界Hashgard:读懂智能合约与虚拟机,看这一篇就够了!

七月 25, 2019 八月 19th, 2019 没人评论

什么是虚拟机,为什么在区块链的世界里,它如此重要?

本次,临界 (Hashgard) 旗下的研究机构Hashgard Labs与BKFUND研究院共同完成了一篇研究报告,对虚拟机进行了深度分析。

1. 智能合约

智能合约简单来说是一种无需中介、自我验证、自动执行合约条款的计算机交易协议或者程序。

区块链中的智能合约不仅可以实现转账,也可以描述游戏规则。

  • 狭义的智能合约可看作是运行在分布式账本上预置规则、具有状态、条件响应的,可封装、验证、执行分布式节点复杂行为, 完成信息交换、价值转移和资产管理的计算机程序.
  • 广义的智能合约则是无需中介、自我验证、自动执行合约条款的计算机交易协议.

​按照其设计目的可分为:

  • 旨在作为法律的替代和补充的智能法律合约
  • 旨在作为功能型软件的智能软件合约
  • 旨在引入新型合约关系的智能替代合约 (如在物联网中约定机器对机器商业行为的智能合约)。

2. 实现技术路线:

区块链智能合约应满足的设计要求与实现思路:

  • 智能合约应满足确定性,需要在设计时采用确定性的算法和确定性的数据来源
  • 智能合约应满足可终止性,可通过有限命令、gas模式、资源控制、准入限制等方式实现
  • 目前区块链智能合约主要有三种技术实现:脚本方式、容器化方式、虚拟机方式。

2.1 脚本方式

脚本方式可以说是最传统的实现可编程逻辑的一种方式,最早在BTC系统中被采用。BTC的UTXO模型采用了一种类似Forth语言的签名脚本体系,用于验证该笔交易的合法性。交易一般会包括输入脚本和输出脚本两个,分别用于解锁上一笔交易的输出以及设置该笔交易金额的解锁条件。

BTC脚本采用堆栈结构方式的逆波兰表达式,用户需要按照顺序将匹配的签名、公钥等提供给脚本作为执行的输入用以解锁该笔交易。因此,脚本方式的另一特点是,和UTXO方式的配合使用效果较好。

但这种方式的缺点也很明显,主要在于功能的扩展性上,脚本方式。因此,如果从功能完备性和编程可扩展性方面来看,很多人认为比特币的脚本方式能实现的编程业务非常有限。因此从狭义的角度来看,脚本方式区块链可认为是只实现了简单的可编程特性,而没有通常意义下的智能合约体系。

目前使用脚本方式来实现可编程特性的,都可认为是区块链1.0版本的系统,可对应的包括以下3大类:

1) BTC及相关分叉、竞争通证

使用脚本方式较多的是比特币及早期相关采用UTXO模型的一些通证,包括BTC的分叉Token、竞争Token等,主要包括:Litecoin、Bitcoin Cash等等。

2)匿名通证

主打匿名、隐私保护的通证也较多的使用了脚本体系。出于对隐私保护和交易追溯性的隐藏,这类通证很多都采用了抽象、简化的脚本体系,包括Monero、Dash等。

3)DAG类的区块链

还有一些会用到脚本方式来运行合约的会是基于有向无环图结构(DAG)的区块链系统。由于DAG系统的原生特性,包括交易时长不可控、不存在全局排序机制等特点,较难在DAG的分布式账本系统上实现出一个图灵完备的智能合约系统。因此为了提高并发TPS、简化交易处理过程,一部分基于DAG的分布式账本系统也会采用脚本方式来构建自己的可编程体系。

2.2 容器化方式

容器化方式,是近年来兴起的、不同于虚拟机的一种新型虚拟化技术。相对于虚拟机在用户程序和底层环境中增加的一层中间环境,容器技术只需要将应用程序所需要的依赖打包即可独立运行,而不需要一个附加的虚拟操作系统环境。

使用容器化的方式实现区块链平台的智能合约环境,相对于堆栈执行代码的虚拟机方式相对更为独立和灵活、可调用的资源也更多。

尽管容器化技术从整体系统架构来看更为轻便与灵活,但从单个应用的角度来看,则需要考虑更“重”的一些系统因素,因为在容器环境中的进程可访问包括文件、系统功能等在内的更多系统资源。这就对区块链自身的运行环境提出了较多要求,因此采用这种方式的区块链平台较为少见。

典型使用容器化方式的区块链项目是Hyperledger Fabric。其智能合约的运行方式是在节点部署一个链上代码后,所有相关节点均会启动一个在Docker容器中独立运行的链码进程。链码通过容器中对外的gRPC接口完成与节点的交互。

目前对于链码的运行,Fabric采用的仍然是一种较为手动和底层的方式来管理维护。因为是联盟链的环境,相当于是默认所有被许可加入网络的节点均可以较为自觉的使用系统资源,即准入限制方式。

这一思想在对于链码生命周期的维护方面就体现的比较明显:在Fabric 1.3版本中,只提供了package(打包)、install(安装)、instantiate(实例化)、upgrade(升级)等4个命令,在未来才会考虑提供一个在不用实际删除链码情况下,禁用(stop)和重新启用(start)链码的命令。

因此若想在公有链环境下用容器化的方式来构建智能合约环境,则需要采用例如资源控制等更多的方法来适应改造。

2.3 虚拟机方式

目前实现智能合约方式的最多的一种是虚拟机方式。它可以为程序提供一个完全对底层透明的执行环境。

这种思路的典型应用可追溯到传统IT技术中Java的JVM虚拟机。其目的是为了实现“一次编写,到处运行”的特性,而不是让程序开发人员为兼容每个不同的服务器编写不同版本的程序。而这一特点正是分布式部署与运行的智能合约所需要的:屏蔽区块链节点自身执行环境的区别,在所有节点上运行均一致,实现上文所述智能合约需要满足的确定性特点。

目前区块链虚拟机主要有6个方向:

1.基于 neo VM(AVM,编译器核心是基于 .net CLR的MSIL),目前支持.NET系列的语言(C#、VB.NET、F#)、Java系列语言(Java、Kotlin)、Python。

2.基于WebAssembly(Wasm),目前支持C/C++/RUST/Go。

3.基于脚本语言虚拟机(Chrome V8引擎、Lua虚拟机等),目前支持 js。

4.基于RISC-V(一种开源的CPU 指令集架构),目前支持Solidity。

5. 创造一种新虚拟机和语言,类似于EVM。EVM & solidity (ethereum),Move VM & move (libra)。

6.支持多虚拟机和多语言。本体支持neo VM & Wasm, 迅雷链支持 EVM & Wasm, 芯链支持 neo VM & EVM。

虚拟机 VS 容器

2.3.1 虚拟机基础知识总结

虚拟机的核心:内存管理,程序集加载,安全性,异常处理和线程同步。虚拟机从实现上主要分为基于Stack的和基于Register的两种虚拟机。

(1) 解释器 – Interprete

一条一条的解释执行源语言(php,postscritp,javascript)等。

利弊

解释器启动和执行的更快,不需要等待整个编译过程完成就可以运行代码。从第一行开始翻译,就可以依次继续执行了。解释器看起来更加适合 JavaScript。对于一个 Web 开发人员来讲,能够快速执行代码并看到结果是非常重要的。这就是为什么最开始的浏览器都是用 JavaScript 解释器的原因。

可是当运行同样的代码一次以上的时候,解释器的弊处就显现出来了。比如执行一个循环,那解释器就不得不一次又一次的进行翻译,这是一种效率低下的表现。

(2) 编译器 – Compiler

把源代码整个编译成目标代码,执行时不在需要编译器,直接在支持目标代码的平台上运行,这样执行效率比解释执行快很多。

利弊

编译器的问题和解释器相反,它需要花一些时间对整个源代码进行编译,然后生成目标文件才能在机器上执行。对于有循环的代码执行的很快,因为它不需要重复的去翻译每一次循环。

另外一个不同是,编译器可以用更多的时间对代码进行优化,以使代码执行的更快。而解释器是在 runtime 时进行这一步骤的,这就决定了它不可能在翻译的时候用很多时间进行优化。

(3) GCC

GNU(Gnu’s Not Unix)编译器套装(GNU Compiler Collection,GCC),指一套编程语言编译器,以GPL及LGPL许可证所发行的自由软件,也是GNU项目的关键部分,也是GNU工具链的主要组成部分之一。是跨平台软件的编译器首选。GCC在所有平台上都使用同一个前端处理程序,产生一样的中介码,因此此中介码在各个其他平台上使用GCC编译,有很大的机会可得到正确无误的输出程序。

支持的语言:C、C++、Fortran、Pascal、Objective-C、Java、Ada,Go等

支持的主要处理器架构:ARM、x86、x86-64、MIPS、PowerPC等。

(4) Clang

是一个C、C++、Objective-C和Objective-C++编程语言的编译器前端。它采用了底层虚拟机(LLVM)作为其后端。它的目标是提供一个GNU编译器套装(GCC)的替代品。

(5) JIT

JIT(Just-in-time 编译器):综合了解释器和编译器的优点,为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。

不同的浏览器实现这一功能的方式不同,不过其基本思想是一致的。在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次,如何运行的等信息。

JIT是使 JavaScript 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 JavaScript 应用的性能提升很多倍。

为了使执行速度变快,JIT 会增加很多多余的开销,这些开销包括:

  • 优化和去优化开销;
  • 监视器记录信息对内存的开销;
  • 发生去优化情况时恢复信息的记录对内存的开销;
  • 对基线版本和优化后版本记录的内存开销。

这里还有很大的提升空间:即消除开销。通过消除开销使得性能上有进一步地提升,这也是 WebAssembly 所要做的事之一。

(6) JVM

依赖于JVM(Java Virtual Machine) 的语言(Java、Scala、Groovy、Kotlin等)程序经过一次JIT(just in time,即时编译技术)编译之后,将程序代码编译为字节码也就是class文件,然后在不同的操作系统上依靠不同的JVM进行解释,最后再转换为不同平台的机器码,最终得到执行。

(7) NET CLR

.NET CLR  (公共语言运行库,Common Language Runtime)一个可由多种编程语言(C++/CLI,C#,Visual Basic,F#,Iron Python,Iron Ruby,中间语言(IL)等)使用的运行环境,是.NET Framework的主要执行引擎。

(8) GraalVM

支持非宿主型语言(JavaScript、Ruby、R、Python、LLVM二进制码)和基于JVM的宿主型语言

(9) LLVM

LLVM(Low Level Virtual Machine)是一套可重用的编译工具链,提供了介于高级编程语言和机器语言之间的IR中间语言。LLVM本身可以作为多种语言的后端,提供与语言本身无关的优化和对多种CPU的代码生成功能。(LLVM由UIUC主持开发,最初LLVM (low level virtual machine)的意义已经被超越)

LLVM针对不同语言的前端,对应生成不同平台的机器码。

LLVM的编译流程如下:源码被编译成LLVM中间格式的文件,然后使用LLVM Linker 链接,并进行优化,得到的LLVM code 最终被翻译成特定平台的机器码,另外LLVM支持JIT,会在代码生成过程中插入一些轻量级的操作指令来手机运行的信息,例如识别hot region,另外收集的信息可以支持离线优化(offline optimizer),实现profile-driven 等优化策略,调整native code 以适应特定的架构。

LLVM IR(Intermediate representative):

由上面可以看出LLVM编译器是先将源语言翻译成“中间语言”,不同语言有不同的IR,再通过后端程序翻译为目标平台的编译语言。LLVM IR提供三种格式,分别是:内存里的IR格式,存储在磁盘上二进制格式,存储在磁盘上的文本格式。除此以外,和IR相关的还有一些文件格式,罗列如下:

  • bc 结尾, LLVM IR文件,二进制,可通过lli 命令执行
  • ll 结尾,LLVM IR文件, 文本格式,可以通过 lli 执行
  • s 结尾,本地汇编文件
  • out 后缀,本地可执行文件下图显示集中文件的转化:

 LLVM的前端编译器可采用各种解析编译器,通常是clang用的比较多,不过在EOS是用wasm替代了clang。

LLVM JIT(just-in-time) :

JIT是将原本编译器要生成机器码的部分直接写入当前内存,通过函数指针的转换,找到相应机器码并执行,常用于处理内存管理,符号重定向,处理外部符号等问题上。

(10) Wasm

WebAssembly是一种新的字节码格式,是除了 JavaScript (JS于 1995 年问世, 2008 年”性能大战”打响。许多浏览器引入了 Just-in-time ( JIT)编译器。基于 JIT 的模式,JS代码的运行执行速度快了 10 倍)以外,另一种可以在浏览器中执行的编程语言。

它的缩写是”.wasm”, .wasm 为文件名后缀,是一种新的底层安全的二进制语法。它可以抽象地理解成是概念机器的机器语言,比 JavaScript 代码更直接地映射到机器码,它也代表了“如何能在通用的硬件上更有效地执行代码”的一种理念。所以它并不直接映射成特定硬件的机器码。

浏览器把 WebAssembly 下载下来后,可以迅速地将其转换成机器汇编代码。它被定义为“精简、加载时间短的格式和执行模型”,并且被设计为Web 多编程语言目标文件格式。这意味着浏览器端的性能会得到极大提升,它也使得我们能够实现一个底层构建模块的集合。支持WebAssembly的浏览器可以识别二进制格式的文本,它有能力编译比JS文本小得多的二进制包,解码速度比JS快很多。

Wasm允许用户采用自己熟悉的语言书写(目前支持C/C++/Rust),再在虚拟机引擎在浏览器上运行。它支持沙盒模式,即先用高级语言编写wasm模块,再在JS中以库函数加载。

WebAssembly 使用基于栈的虚拟机,但是并不是说在实际的物理机器上它就是这么生效的。当浏览器翻译 WebAssembly 到机器码时,浏览器会使用寄存器,而 WebAssembly 代码并不指定用哪些寄存器,这样做的好处是给浏览器最大的自由度,让其自己来进行寄存器的最佳分配。

WASM允许C/C++等语言编写运行在WEB中的程序,WASM其实是一种字节码格式,是底层二进制语法,加载时间段以及高速执行,是为WEB多语言编程设计的目标文件格式。

WebAssembly提供两种格式:可读的文本格式wast 和二进制格式 wasm, 通过工具wast2wasm 完成 wast 到 wasm 的格式转换,同理,wasm2wast 实现逆转换。

WebAssembly 模块的组成部分

必须部分:

Type:在模块中定义的函数的函数声明和所有引入函数的函数声明。

Function:给出模块中每个函数一个索引。

Code:模块中每个函数的实际函数体。

可选部分:

Export:使函数、内存、表(tables)、全局变量等对其他 WebAssembly 或 JavaScript 可见,允许动态链接一些分开编译的组件,即 .dll 的WebAssembly 版本。

Import:允许从其他 WebAssembly 或者 JavaScript 中导入指定的函数、内存、表或者全局变量。

Start:当 WebAssembly 模块加载进来的时候,可以自动运行的函数(类似于 main 函数)。

Global:声明模块的全局变量。Memory:定义模块用到的内存。

Table:使得可以映射到 WebAssembly 模块以外的值,如映射到 JavaScript 的对象。这在间接函数调用时很有用。

Data:初始化导入的或者局部内存。

Element:初始化导入的或者局部的表。

Wasm VS JS

  • 当前的WebAssembly 只能使用数字(整型或者浮点型)作为参数或者返回值
  • 对于任何其他的复杂类型,比如 string,就必须得用  WebAssembly 的内存操作。如果是经常使用 JavaScript,对直接操作内存不是很熟悉的话,可以回想一下 C、C++ 和 Rust 这些语言,它们都是手动操作内存。
  • WebAssembly 的内存操作和这些语言的内存操作很像。为了实现这个功能,它使用了 JavaScript 中称为 ArrayBuffer 的数据结构。ArrayBuffer 是一个字节数组,所以它的索引(index)就相当于内存地址了。如果你想在 JavaScript 和 WebAssembly 之间传递字符串,可以利用 ArrayBuffer 将其写入内存中,这时候 ArrayBuffer 的索引就是整型了,可以把它传递给 WebAssembly 函数。此时,第一个字符的索引就可以当做指针来使用。

Wasm VS asm.js

asm.js 是一个JavaScript的一个严格的子集,可以被用来作为一个底层的、高效的编译器目标语言。asm.js提供了一个类似于C/C++虚拟机的抽象实现,包括一个可有效负载和存储的大型二进制堆、整型和浮点运算、高阶函数定义、函数指针等。

asm.js的思想是使用它所规定的方法来编写JavaScript代码,支持asm.js的引擎会将代码转变为十分高效的机器码。如果你是将C++代码编译为asm.js,将在浏览器端获得极大的性能提升。

Web Assembly 比 asm.js 要激进很多。Web Assembly 连标注 Js 这种事情都懒得做了,不是要 AOT 吗?我直接给字节码好不好?(后来改成 AST 树)。对于不支持 Web Assembly 的浏览器, 会有一段 Javascript 把 Web Assembly 重新翻译为 Javascript 运行, 这个技术叫 polyfill, HTML5 刚出来的时候很常用的一个技术。使用 AST 的原因是因为 AST 比字节码更容易压缩,也更容易翻译。Javascript 先编译为 AST, 然后到 Bytecode. AST 的抽象程度比 Bytecode 要高一级。

与asm.js相比,它减少了大约25%的代码量,WebAssembly的加载速度比asm.js快了20倍,这主要是因为相比解析 asm.js 代码,JavaScript引擎破译二进制格式的速度要快得多。精简的代码,更好的性能,更少的bug。

Wasm的技术优势

  • 性能高效:WASM采用二进制编码,在程序执行过程中的性能优越;
  • 存储成本低:相对于文本格式,二进制编码的文本占用的存储空间更小;
  • 多语言支持:用户可以使用 C/C++/RUST/Go等多种语言编写智能合约并编译成WASM格式的字节码;

Wasm 比 JS 执行更快的原因:

  • 文件抓取阶段,WebAssembly 比 JavaScript 抓取文件更快。即使 JavaScript 进行了压缩,WebAssembly 文件的体积也比 JavaScript 更小;
  • 解析阶段,WebAssembly 的解码时间比 JavaScript 的解析时间更短;
  • 编译和优化阶段,WebAssembly 更具优势,因为 WebAssembly 的代码更接近机器码,而 JavaScript 要先通过服务器端进行代码优化。
  • 重优化阶段,WebAssembly 不会发生重优化现象。而 JS 引擎的优化假设则可能会发生“抛弃优化代码<->重优化”现象。
  • 执行阶段,WebAssembly 更快是因为开发人员不需要懂太多的编译器技巧,而这在 JavaScript 中是需要的。WebAssembly 代码也更适合生成机器执行效率更高的指令。
  • 垃圾回收阶段,WebAssembly 垃圾回收都是手动控制的,效率比自动回收更高。

与区块链的结合点

Web端DApp

JS的时代下Web端编译语言多半是解释性语言,虽然它们易于被使用者解释并理解,但运行效率并不高,特别是JS。Node.js固然给了一个框架可以编写本地和服务端应用,但对于加密计算、图像处理等效率过于低下。HTML5解决了很多浏览器的功能和性能标准问题,但是H5仍然沿用了JS作为主要语言,没有本质上解决问题。在诸多Web开发端在各行其是自己搞编译性语言无法得到统一下,Wasm应运而生。

在 Web 平台的很多项目中, 对于原生新功能的支持需要 Web 浏览器或者 Runtime 提供复杂的标准化的 API 来实现, 但是 Java API 往往较慢。使用 WebAssembly, 这些标准 API 可以更简单, 并且在底层进行操作。例如, 对于一个面部识别的 Web 项目, 对于访问数据流我们可以由简单的 Java API 实现, 而把面部识别原生 SDK 做的事情交由 WebAssembly 实现。

那么对于区块链DApp,它的意义非常明确:

1.允许开发者以其他语言开发,再加载在JS上。

2.提升程序性能,允许大型区块链DApp的开发。

这是ETH、EOS等项目想要使用wasm这个技术的原因。

由于EVM需要预编译,同时需要付出gas作为代价,实际上在EVM上编程成本很高。同时对于EVM的臃肿毫无帮助。最后,Solidity相比其语言基础C比较难学。

而Wasm是是内存安全的、平台独立的,并且可以有效地映射到所有类型的CPU架构。其指令集效率高,同时保有足够的可移植性。此外,Wasm指令集可以很容易地通过移除浮点指令来确定化,这将使它适合于替换EVM语言。

同时,Wasm在不增加内存消耗的情况下,可以达成无信任编程。可以通过在Wasm上进行堆栈分析与计量进行精确计算。

目前ETH打算将DApp也用上基于eth-WASM的智能合约,而EOS、Polkadot和Ontology则是用于虚拟机。