Java智能合约实现探索
Note. “智能合约是一种特殊协议,旨在提供、验证及执行合约。具体来说,智能合约是区块链被称之为“去中心化的”重要原因,它允许我们在不需要第三方的情况下,执行可追溯、不可逆转和安全的交易。“[1]Java作为企业级应用开发最流行的开发语言,却比较少地能够用于在各类公链以及联盟链开发智能合约。本篇介绍一种基于Wasm虚拟机的Java智能合约的实现,重点介绍Java字节码转化为Wasm字节码过程,希望能够抛砖引玉,帮助大家继续发掘更多Java智能合约的实现方案。
1. Java智能合约可选实现方案
纵观当前的联盟链和公链,智能合约的实现方案大致可划分为3种[2]:
- native方式
- 容器方式
- 虚拟机方式
但是不管哪种方式,设计都需要满足以下2点基本要求:
- 智能合约的整个执行过程每一步都需要是一个确定状态转移,也就是说实现智能合约不能依赖如随机数等不确定算法、类库。否则,在不同节点上运行结果不一致导致链的分叉。
- 智能合约的执行一定要可终止,可以使用诸如可执行命令数量有限、gas计费模型、资源控制、访问限制等方式实现。
native方式:就是在区块链节点上分别运行原生的可执行文件,比如在节点上安装Jdk,直接运行编译构建好的Jar包。这种方式一般只适合内部试验,因为JVM在运行Java字节码过程中能够直接跟节点进行交互,比如执行读写文件、访问网络等系统调用,安全性很差。一旦不注意,有可能会直接导致账本被修改,甚至整个系统崩溃。
容器方式:典型例子是Hyperledger Fabric,通过把链码部署到节点上,所有相关节点都启动一个独立运行在Docker容器上链码进程,而在容器里运行的链码进程通过外部gRPC接口与外部链节点进行通信完成相关交易。每次发送调用智能合约的时候,都会启动一个容器在容器内运行合约代码。而容器启动到运行这段时间相对合约代码的运行比较长,另外容器也能够直接访问宿主机资源,安全性方面会有隐患。所以如果打算使用容器方式支持智能合约,需要考虑快速启动类型的安全容器,如kata container。
虚拟机方式:通过虚拟机方式支持智能合约也是最常见的方式,提供了一个相对透明的执行环境。可以选择发明一种新的编程语言和支持其运行的虚拟机,如EVM&solidity(以太坊)。其中WebAssembly(Wasm)又是其中虚拟机方向的一个大方向,支持的前端语言有C/C++/RUST/Go。Wasm虚拟机能够提供一个沙盒环境,在安全性方面有了很大保障,另外其又具备接近native的运行性能,也就成了区块链领域内比较流行的方案。本篇将介绍基于Wasm虚拟机的Java智能合约实现,接下来对Wasm做简单介绍。
图 1 容器和基于Wasm虚拟机的两种实现方案
2. Java字节码到Wasm字节码转化过程
2.1 WebAssembly(Wasm)简介
WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式。
上面是关于Wasm的定义,更详情的介绍请参考[3],有比较详尽的介绍。这里就不详细展开描述,简单介绍一些与后面章节关联比较密切的内容。
Wasm字节码看是一个后缀为wasm格式的二进制文件,逻辑上看是由一个模块组成,而模块又由一系列具有特定功能的“段”结构组成。读者可以尝试使用WasmFiddle进行在线编译,体验一下。以下C程序片段:
int main() {
return 42;
}
int add(int a,int b) {
return a+b;
}
通过编译生成Wasm,及对应可读格式(wat)如下:
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "main" (func $main))
(export "add" (func $add))
(func $main (; 0 ;) (result i32)
(i32.const 42)
)
(func $add (; 1 ;) (param $0 i32) (param $1 i32) (result i32)
(i32.add
(get_local $1)
(get_local $0)
)
)
)
上面是一段表示树形结构的文本(S-表达式),根节点为module,其子节点由table、memory、export、…、func等段组成。
memory段用于定义模块实例化后的可用线性内存段,(memory $0 1)
,memory表示线性内存段,而接下来的数的$0,表示使用$0作为初始容量大小,而1表示表示最大可用内存大小,这里单位为页(64KB),所以最大内存空间为64KB。
export段表示把数据从模块内导出到模块外的宿主环境,当Wasm虚拟机在执行过程中遇到export段,可以使用自身的数据或者函数替换。
func段主要用于存储在模块中定义的所有函数类型数据,func表示一个函数段,接着是函数名,如$main,而 (; 0 ;) 表示索引位置,result i32表示函数结果是一个32的有符号整数,下面是函数体,由若干指令组成,如i32.const 42表示一个32位有符号整数,值为42并压入数据栈中。
除了上述的几个段,Wasm还有import、start、global、data、elements等类型的段,感兴趣的读者可以阅读理解WebAssembly文本格式了解更多的内容。
总体来说,Wasm模块的二进制数据内容由众多不同类型的“段”组成,每一种类型的段结构都有特定的信息含义,这些不同的段结构之间又组成了Wasm模块完整的功能和逻辑。
2.2 Java字节码转化为Wasm字节码流程
与C++程序类似,Java源码先通过编译生成Java字节码,再转化为Wasm字节码。
通过调研发现,目前已有的开源方案(teavm、CheerpJ、Bytecoder、JWebAssembly)将Java源码转化为Wasm字节码的流程图2前半部分所示。
图 2 Java源码到Wasm字节码主要转化流程
-
源码先编译成Java字节码,再使用asm[4]字节码操作库读取主函数所在的类的字节码(对于智能合约来说,指定为运行入口所在的类)。但是即便是Java虚拟机,在运行的过程也不是一次性的把jar包里所有的class加载到内存里。而是需要通过主动或者被动地把主类依赖的其他类加载到Java虚拟机。但是要翻译成对应的Wasm字节码,就必须转化所有从入口函数可达的Java字节码。因此,需要作依赖分析。
-
另外可以通过消除不可达代码,减少Wasm字节码体积。
-
经过上面步骤,Java源码就变成一棵比较大的抽象语法树,再将这颗语法树每一条Statement一一翻译成对应的Wasm指令即可得到整个Wasm模块。
下面将一个具体的例子来说明整个过程。
2.3 示例说明
while(k<7){
foo(k);
k=k+1;
Object obj = new Object();
}
上面程序1是一个Java代码示例片段,在一个while循环里调用函数foo以及创建对象obj。对应的抽象语法树如图3所示:
图 3 程序1对应的抽象语法树
实际中,AST对应的每个节点几乎都能够一一翻译成Wasm的指令,如图4所示。比如Wasm通过loop
、br
即可实现while语句。
图 4 程序1对应的Wasm S-表达式
2.4 关于内存
我们知道,Java是本身具备自动垃圾回收机制的,像上面的例子中,即便不断循环创建新对象,背后也会有线程定期地回收可释放的内存空间。但是Wasm的MVP标准里面是没有自动垃圾回收机制的,所以在处理Java的构造函数(new函数)的时候,会涉及到Wasm线性内存空间的指令。如图5所示,Wasm中调用new函数的时候,可能会对Wasm的线性内存空间使用标记清除
垃圾回收算法。
图 5 Wasm线程内存空间的"标记清除"过程
3. 基于Wasm虚拟机的Java智能合约实现
关于这部分的内容,需要设计到Wasm虚拟机,以及虚拟机和区块链节点间的通讯,后续将通过实现一个Wasm虚拟机原型同时再进行更详细的介绍,整体流程如图6所示。
图 6 Java智能合约的实现方案
-
可以通过低代码、DSL、合约模板等方式降低智能合约开发门槛,生成Java源码。
-
再通过maven插件,解析Java源码的抽象语法树,做预处理。生成满足区块链Wasm虚拟机执行规范的Java源码。
-
再编译生成Java字节码。
-
按上面内容,将Java字节码转化为Wasm字节码。
-
将合约接口信息与Wasm字节码进行整合,再部署到区块链上,最后进行调用生成交易。
最后,关于Wasm虚拟机部分的内容将再后续单独进行介绍,到时候会实现一个Wasm虚拟机来加深对Wasm的理解,希望以上内容能给到大家关于Java智能合约方面的一些启发。
4. 参考资料
[1] 什么是区块链的“智能合约”
[2] Smart Contract and Virtual Machine
[3] WebAssembly完全入门——了解wasm的前世今身
[4] 史上最通俗易懂的ASM教程