迅雷链在支持 wasm 虚拟机上的实践细节

阅读数:7871 2019 年 8 月 19 日 10:53

迅雷链在支持wasm虚拟机上的实践细节

为了给开发者提供更大范围的开发自由度和更强大的功能,迅雷链于 18 年底开始支持了 WASM 虚拟机。基于 WASM 虚拟机和 C/C++ 智能合约编程语言,迅雷链能方便的扩展智能合约功能,例如要为智能合约提供新接口以实现新功能,迅雷链只要提供一个新的库函数即可;并且,迅雷链还提供了合约原地升级功能,使用起来非常方便。

由于用户需要,迅雷链在前天开源了 wasm 虚拟机,相关 github 项目包括 3 个部分:

  • wagon 是 wasm 的字节码解释器,在 go-interpreter/wagon 项目的基础上改造而来,例如内存管理、系统调用接口、gas 收费机制等。
  • tc-wasm 迅雷链 wasm 虚拟机代码,包括字节码解决执行和 SDK 库接口的实现,方便集成到其它区块链平台,支持 c/c++ 编写的智能合约。mock 目录的代码提供了集成到以太坊的示例。
  • tcvm-cdt 是迅雷链 wasm 合约编译工具,提供本地编译工具以及合约 SDK 库文件源代码。

一、背景介绍

在长期基于 EVM 的 solidity 智能合约的开发使用过程中,迅雷链发现 EVM 存在一些令人难以忍受的问题,例如:
1)强耦合,难扩展: 区块链与 EVM 之间到处充斥着相 - 互依赖的关系,例如:

  • 虚拟机指令集中包含大量用于访问账本数据的指令;
  • 虚拟机直接提供以区块链账本作为载体的持久化存储指令;
  • 功能函数实现为虚拟机指令,功能难以扩展,若要新增一个功能,需要修改编译器和 EVM;

2)缺少标准库:Solidity 中根本就没有标准库。如果你想比较两个字符串,Solidity 中根本就没有类似 strcmp 或 memcmp 的标准库函数供你调用,你必须自己用代码实现或在网上拷贝代码来实现。

经过一翻调研后,迅雷链选择了 WASM 作为第二个智能合约虚拟机,解决了上述 EVM 的问题。

WebAssembly(简称 wasm)是为了提供比 JavaScript 更快的编译和运行速度,让高性能的 Web 应用在浏览器上运行成为可能。wasm 本身是一种面向 Web 的低级语言(不是直接的机器语言,与 IR 差不多),理论上来说,开发者能够将任何语言编译成 wasm 字节码,并运行在 web 上,而不再局限于 JavaScript。目前编译器已经支持将 C/C++/Rust 编译为 wasm 字节码,后续还会支持更多的主流语言。

相比于 JavaScript,wasm 具有如下优点:
1)快速、高效:wasm 的二进制格式可以被原生的解码,比 JS 的解析要快很多;而且 wasm 的二进制格式更加紧凑,文件更小;
2)可移植:wasm 代码可以运行在不同的平台上,以接近本地的速度执行;

以太坊的智能合约是运行在图灵完备的 EVM 虚拟机上,不过社区已经计划把 EVM 替换为 eWASM,最重要的一个原因是 wasm 的指令更加接近实际的 cpu 指令,有更显著的运行性能。

二、设计目标

区块链系统与智能合约的关系如同操作系统与应用程序的关系,操作系统内核为应用程序提供系统调用接口,方便应用程序访问内核资源。同理在区块链系统中,虚拟机作为智能合约的运行环境,会为智能合约提供区块链的系统调用接口,方便智能合约与区块链系统的交互,以及对区块链资源的访问。

但是,区块链系统有其特殊性,因此虚拟机除了要提供必要的系统调用接口外,还需要满足如下要求:
1)确定性:同样的合约调用参数,要求区块链中的每个节点在运行完合约调用之后的状态必须是确定的、一致的,否则区块链的共识就无法达成一致;
2)可终止性:合约的运行不能是无限循环 unstopped,这样会导致区块链系统无法继续构建区块;
3)可嵌套:虚拟机需要支持在一个合约代码中嵌套执行另一个合约的代码;
4)可撤销:合约在执行的过程中发生了失败或者异常,虚拟机需要撤销该合约在当前执行过程中的状态修改,恢复到执行前的状态。

由于迅雷链最初是支持 EVM 的,在引入 wasm 时,除了上述必须满足的目标,还要求做到:
1)与 EVM 共存,同时支持 EVM 和 WASM 双虚拟机,通过运行时识别字节码来决定是调用 EVM 还是 WASM;
2)支持合约原地升级;
3)封装 c++ SDK,隐藏区块链底层细节,方便开发者进行合约开发;

三、细节说明

围绕上述的设计目标,下面将会详细地介绍迅雷链的一些内部实现细节:

1. EVM 和 WASM 双虚拟机支持:

不管是 EVM 合约还是 WASM 合约,部署成功后,在迅雷链内部都是统一由 Account 表示:

复制代码
type Account struct {
    Nonce uint64
    Balance *big.Int
    Root common.Hash // merkle root of the contract storage
    CodeHash []byte
}

每一个 Account 都会有一个对应的地址 Address,合约账户的地址和普通账户的地址是一样的,统一由 20 个字节表示。也就是说从地址上直观的观察,是无法辨别出该地址代表的是普通用户,还是合约账户。

在创建虚拟机执行合约代码之前,会根据 CodeHash 从 db 读取合约的二进制字节码,判断合约的前 4 个字节是否为 wasm 合约的标识符(wasm 合约字节码的前 4 个字节为固定的”.asm”标识符),从而创建对应的虚拟机来执行合约调用,实现双虚拟机的支持。

2.WASM 合约原地升级:

之所以要支持合约原地升级功能,最主要的原因是当合约存在安全漏洞、逻辑错误时,可以直接通过原地升级来解决,而不是只能关闭合约功能,停止对外服务。

EVM 合约要支持原地升级功能,对合约开发者有较高的开发门槛,体现在如下两方面:
1)首先,合约升级的权限管理,需要在开发者在合约代码中进行严格的权限检查与签名校验;
2)其次,合约数据持久化存储与 solidity 代码的变量布局有关系,变量的布局改变了,原先存储的数据可能就无法读取到了。因此,基于 solidity 编写的智能合约要支持升级,需要开发者采用代理合约的策略方式,将合约的逻辑与数据分离成不同的合约,升级的时候只升级逻辑合约,不能够升级数据合约。

对于合约升级的权限管理,迅雷链为了降低开发的难度,直接在区块链系统底层给予支持,不需要开发者而外实现。具体来说,迅雷链在区块链系统底层实现多重签名支持,合约在部署的时候,指定合约的所有者列表 owners,后续如果需要升级合约,迅雷链底层会严格按照 owners 列表来检查合约升级交易的多重签名是否正确,若 owners 列表为空,自动拒绝合约升级交易的执行。

关于合约的数据存储,迅雷链在迅雷链底层提供系统调用接口,由合约开发者显式地指定数据的 key,这样当合约升级后,迅雷链依然可以根据给定的 key 读取到之前的数据。

3.WASM 虚拟机改造:

1)Gas 手续费的收取:
迅雷链是由普通机器作为出块节点的,不像 EOS 是由超级服务器节点组成,若简单的规定合约运行的最大时间为 1 毫秒,那么稍微功能复杂点的合约,都无法在规定的 1 毫秒内完成调用,限制太强。因此迅雷链借鉴以太坊的做法,对 WASM 约执行的每条指令进行按需收费,当合约执行的手续费达到了调用者给定的手续费上限时,WASM 虚拟机自动终止合约的继续执行。

2)系统调用接口:
wasm 模块支持引入其他的 wasm 模块以调用其中的函数,如果本模块内调用的函数没有具体实现,就会默认的认为是从 env 模块导入的。因此,迅雷链把所有的系统调用接口封装到一个 env 模块中,在执行一个具体的 wasm 合约之前,会先加载并分析其导入的 env 模块的每一个函数,将这些函数与迅雷链的系统调用接口关联起来,从而实现 wasm 合约对 native 函数的调用。

具体来说,迅雷链提供了如下几大类的系统调用接口:

  • Libc 标准库部分接口:主要是与内存操作相关的函数,如 malloc 和 free。因为 wasm 虚拟机作为 wasm 合约执行的宿主环境,需要对合约运行的虚拟内存进行分配与管理;
  • 区块链相关接口:为方便 wasm 合约方便访问区块链相关信息,迅雷链提供了存储读写、账户转账、事件触发、交易信息等接口;
  • Library:主要是提供了 BigInt 和 json 操作函数,方便合约开发者的编写;

3)内存管理:
为了处理字符串及其他复杂数据类型,WebAssembly 提供了内存管理。按照 wasm 的定义,内存就是一个随着时间增长的字节数组。所以,迅雷链的 wasm 虚拟机在执行合约前,会根据合约导入的内存,为其创建内存实例,并初始化对应的 data 数据段内容。具体实现上,迅雷链采用了 Buddy 伙伴算法来管理 wasm 合约的运行内存,默认的初始内存大小为 64KB,最大可增长到 256KB。

4)简化合约开发:
合约作为区块链系统的业务承载体,承载着区块链的落地应用价值,因此迅雷链在合约开发这方面,一直在努力降低合约开发的门槛,简化合约的开发过程,让更多的企业可以将其业务应用迁移到迅雷链系统中来。

  • C++ SDK 提供:

迅雷链知道,一般的业务处理逻辑流程,大致是:解析输入参数,根据参数执行相应的操作(比如持久化存储相关对象),返回处理结果。其中参数解析、对象存储读写、结果返回等,经常需要涉及到对象的序列化与反序列化操作,为此,迅雷链的 C++ SDK 提供了基于模版的静态反射库,开发者只需要声明对象的相关字段,不需要手写逻辑代码,就能达到与手写序列化操作一样的运行效率。

除此之外,SDK 还提供了常用的 Address、Hash、Token、BigInt 等操作函数,简化合约的编写。

  • ABI 工具:

迅雷链提供配套的 ABI 工具,通过对合约的 ABI 分析,自动生成 go/python 客户端代码,屏蔽合约方法的参数编码细节,方便外部对合约方法的调用。

引入 wasm 过程中发现的问题及解决方法

迅雷链在引入了 wasm 虚拟机后,在内部的合约开发测试过程中,还发现了几个其他问题:

1)字节码解析耗时:
根据 wasm 规范,wasm 的字节码是以 Module 的形式描述的,一个合约编译之后就是一个独立的 Module,一个 Module 是由不同的 Section 组成的,其中 SectionCode 才是迅雷链需要真正执行的指令。对于同一个合约,若每次执行前都需要做一次解析操作,无疑是一种多余的浪费,这种浪费会严重影响到迅雷链合约执行性能,因此底层将会缓存同一个合约字节码的解析结果,这样同一个合约每次运行时,只需要重新初始化内存即可。

2)字节码过大:
当迅雷链在合约代码中使用了 C++ 标准库的容器,发现编译后的字节码偏大,功能简单的合约字节码一下子由几 KB 增大到了几十 KB,更别说功能复杂的合约了。为此,迅雷链在 C++ SDK 中,重新简化实现了常用的标准库代码,防止合约字节码膨胀过大,同时也有利于降低字节码解析的耗时。

3)容易内存泄漏:
迅雷链底层使用 go 语言实现的,当合约通过系统调用接口访问底层 native 方法时,底层方法返回的数据保存在 wasm 的线性内存中,由于这个内存并不是开发者通过 malloc 分配的,所以会经常忘记调用 free 来释放,造成内存泄漏。加上底层限制了一个合约可以使用的内存上限是 256KB,所以会造成功能复杂的合约,最后由于超出内存上限而执行失败。

因此,为了避免内存泄漏问题,在 SDK 中采用 RAII 和 SharedPtr 方式,不再暴露原始的内存地址,而是返回栈上的临时对象。

合约示例

示例目录提供了一些合约示例,包括链上接口用法、数据存取、合约调用合约、json 序列化、token 发行等。

一个最简单的合约代码示例:

复制代码
#include "tcmethod.hpp"
class Hello : public TCBaseContract{
public:
// 合约类公有成员函数
const char* SayHello (){
return "Hello World!";
}
};
//TC_ABI 声明合约外部接口
TC_ABI(Hello, (SayHello))

合约类 Hello 继承自 TCBaseContract。
TCBaseContract 定义如下:

复制代码
class TCBaseContract : boost::noncopyable {
public:
void Init(){}
void Callback(){}
};

TCBaseContract 类只定义了两个空函数,需要的话可以在自己的合约类中编写自己的覆盖(override)函数。

Init() 函数用于定义合约初始化逻辑,会在合约调用时被自动调用一次。

Callback() 函数功能类似于 solidity 的 fallback 函数,当合约被调用,但没有匹配的函数名时,Callback() 函数就会被调用。

TC_ABI 宏用于将合约类的成员函数自动构造成合约入口函数 thunderchain_main,支持结构体入参,支持多个返回值,生成相关的 ABI 文件,详见说明文档

四、总结

本文主要探讨了迅雷链在支持 wasm 虚拟机上的实践细节,希望能对相关的区块链从业者提供一些可借鉴的经验。推荐参考迅雷链 github 开源代码

作者简介:
周茂青,网心科技软件工程师,现从事迅雷链底层技术研发工作。曾负责过广域网优化产品、分布式文件系统、CDN 缓存系统等技术设计和开发等工作,在 Linux 内核开发、网络协议优化、分布式系统等方面有着丰富的从业经验。

相关文章:
《区块链助力中国版权保护:迅雷链三大核心技术解读》

评论

发布