有了理论基础,我们现在开始进入实际的例子。我们以Corda项目(最近贡献给了Hyperledger项目)的确定性类加载器为例。Corda为编写确定性智能契约提供了一个JVM框架,用于操作分布式总账的状态。
示例代码可以在Corda项目的Github主页上找到。如果有人想参与到项目中,或者想参与讨论、拉取分支,随时欢迎。
乍一看,把JVM看作一个确定性执行平台有点奇怪。JVM的很多东西都是动态的,例如多线程Java程序、垃圾回收、JIT编译的竟态条件所导致的方法运行差异性,等等。
不过,JVM有如下优势。
JVM字节码语义易于理解和管理
Java的安全模型被证明是健壮的
JVM字节码工具已经很成熟
类加载机制为确定性代码分析提供了一个便利的平台
Corda沙箱使用类加载机制和运行时组件来达成如下目的。
拒绝明显的非确定性程序
在加载类时插入资源追踪代码
提供了一个系统,用于终结那些试图违反资源约束的程序(回滚事务)
WhitelistClassLoader是Corda框架里的一个类加载器。它会拒绝加载直接或间接创建线程的代码,因为所有多线程程序都是非确定性的。
这个类加载器包含了一个白名单,名单里列出了所有已知的安全方法,比如Object::getClass(),这个方法是允许被调用的(包含了native代码的方法会被踢出名单)。
这个类加载器会往加载的类里面注入运行时追踪代码,用于检测类的资源使用情况。
这个类加载器系统已经知道JRE包里面的哪些方法是确定性的,这些方法可以被用户代码自由调用。有了这些类加载器组件,就可以为智能契约代码构建一个确定性执行系统。
类加载器客户端在加载类时会调用它的loadClass()方法。这个方法先尝试从缓存里查找类,如果找不到,会委托给父加载器去加载。如果父加载器也无法加载,它会委托findClass()方法去加载。
这个过程遵循标准的Java类加载机制。自定义类加载器一般会通过覆盖findClass()方法来实现自定义的类加载功能。
也就是说,真正实现类加载的是findClass()方法。在WhitelistClassLoader里,findClass()方法会扫描被加载的类是否是确定性的。它使用ASM来读取和检查被加载的类。扫描代码看起来是这样的(经过少许的简化):
public boolean scan() throws IOException { try (final InputStream in = Files.newInputStream( classDir.resolve(classInternalName + ".class"))) { try { // 使用输入流创建一个ASM ClassReader对象 final ClassReader classReader = new ClassReader(in); // 创建一个ClassVistor,用于分析加载的类是否是确定性的。 // 我们使用CandidacyStatus对象持续跟踪在扫描过程中遇到的方法, // 并标记它们是否为确定性的。 ClassVisitor whitelistCheckingClassVisitor = new WhitelistCheckingClassVisitor(classInternalName, candidacyStatus); // 使用ASM的reader和vistor模式来读取要加载的类 classReader.accept(whitelistCheckingClassVisitor, ClassReader.SKIP_DEBUG); } catch (Exception ex) { // ... 这里忽略异常处理部分 } } return candidacyStatus.isLoadable(); }类加载过程最关键的部分是class visitor,它负责给代码添加特定的约束。
例如,关键字strictfp在Java语言里很少被用到,它要求强制遵循IEEE 754浮点数标准。一般来说,很少类会使用这个关键字。不过如果不使用这个关键字,JVM一般会选择在硬件层面进行浮点数运算,其精确度完全取决于硬件。
运行在现代CPU上的JVM一般会给出比IEEE 754更高精确度的结果,不过这也意味着不同的硬件和不同的实现会产生不同的结果,从而造成了不确定性。
确定性类加载器会简单粗暴地拒绝加载没有使用strictfp关键字的程序代码,但很少程序员会用到这个关键字,所以这个让人感到沮丧。不过,WhitelistClassLoader会为所有加载的类打开strictfp开关,以确保浮点数运算行为的一致性。
class visitor在进入主要的分析阶段之前会处理strictfp关键字问题和其他与确定性有关的问题(比如不允许使用finalizer),每个方法对应一个WhitelistCheckingMethodVisitor对象。
在扫描过程中,类加载器会用到CandidacyStatus,它知道某些方法的确定性状态(一般指单类的方法)。从它的名字我们就可以看出,这些方法会被认为是可加载的候选方法。