如果一个程序在不同的计算机、或者在同一台计算机上的不同时刻多次运行,对于相同的输入能够保证产生相同的输出,则称该程序的行为是确定性的,反之则称该程序的行为是非确定性的。使程序产生非确定性的因素有很多,总结起来有以下几种:
调用了非确定性的系统函数
一般在编写程序的时候,开发者或多或少会调用一些系统提供的函数和功能以减少开发的工作量。这些系统函数中可能会存在一些非确定性的函数,比如生成随机数、获取系统时间等。一旦程序调用了另一个非确定性的程序并使用了它们输出的内容,那么该程序自身的行为也可能会变为非确定性的。
使用了非确定性的数据来源
如果一个程序在运行时获取数据,而数据源提供的是非确定性的数据,那么该程序也可能会变成非确定性的程序。例如,通过搜索引擎来获取某个关键词的前10条搜索结果——搜索引擎针对不同的IP地址来源可能会返回不同的排序结果。
动态调用
动态调用是指,一个程序在调用另一个程序时,如果必须在运行时才能确定被调用的目标,则称该调用为动态调用;反之,如果在运行前即可确定被调用的目标,且在运行时无法变更该目标,则称该调用为静态调用。由于动态调用的目标在运行时决定,因此其行为是非确定的。
对于区块链上的智能合约,我们一般要求它的行为必须是确定性的,因为非确定性的合约可能会破坏系统的一致性。区块链的作者必须考虑到这个问题,并在设计智能合约系统的时候,就想办法把非确定性因素排除在外。那么我们来看看现有的各个区块链是如何解决这个问题的。
比特币
比特币内置了一套脚本引擎,用于执行鉴权脚本,它是区块链智能合约的雏形。开发者可以基于这套脚本系统来开发一些简单的应用,但由于其指令集非常简单且非图灵完备,能够实现功能相当有限。这套系统既没有提供任何系统函数,也没有提供任何访问数据的能力,更没有动态调用的功能,甚至连静态调用也没有提供,因此比特币的智能合约一定是确定性的。
以太坊
以太坊的主要设计思想,就是提供一个图灵完备的智能合约平台,让用户可以编写任意逻辑的程序。它专门开发了一个用于执行合约代码的虚拟机EVM,并设计了一种类似于JavaScript的高级语言Solidity,以方便用户进行开发。以太坊智能合约没有提供任何非确定性的系统函数,可访问的数据也仅限于链内数据,外部数据需要通过交易来发送到合约。但是,以太坊的CALL和CALLCODE指令的目标地址通过栈来传递,使得合约可以在运行时动态调用其它的合约代码,使合约的调用路径变为非确定性。好在合约可以访问到的数据都是确定性的,使得所有节点在动态调用目标代码时一定会获得相同的目标地址,保证了系统的一致性。但是调用路径的非确定性,会导致一个可扩展性上的重要性能损失,具体会在《重构智能合约(中):平行宇宙与无限扩展》中详述。
Fabric
Fabric是超级账本中的一个子项目,它的智能合约采用了重量级的Docker作为执行环境。这可能跟大家的印象有点矛盾——“Docker不是一直被认为是一种轻量级的容器技术吗?”。实际上,Docker的“轻”是相对于模拟物理机架构的重量级虚拟化技术而言。在区块链应用场景下,Docker是一个比较“重”的执行环境,这也是Fabric的性能瓶颈所在,目前只能达到每秒几百TPS。由于Docker的特性,智能合约几乎可以使用物理计算机上的所有功能,因此具有极高的非确定性。所以Fabric要求智能合约的开发者在编写代码的时候尽量避免使用到具有非确定性的功能,并计划提供一套专门开发的确定性系统函数库供开发者使用。然而,由于无法从底层机制上避免非确定性的产生,寄托于开发者遵守良好的开发规范难免有些一厢情愿。非确定性就像幽灵一般,平时似乎并不存在,在一些边缘案例(corner case)上就可能会突然冒出来造成难以判断的故障。
网站模版