本文为智能合约设计模式系列的一部分。
目的
在确定性的区块链环境中生成指定范围的随机数
动机
计算机系统的随机性,尤其是以太坊的随机性,是总所周知的难题。虽然很难甚至不可能生成真正的随机数,但以太坊对随机性的需求却很高。这是由于以太坊上存在很多游戏类的智能合约,而游戏通常依赖某种随机性来确定赢家。以太坊是一个确定图灵机,天生排斥随机性。大多数矿工依靠相同的结果达成共识,共识是区块链技术支柱之一,随机性意味着所有几点不可能达成一致。另一个问题是区块链的公开特性。合约的内部状态以及区块链的整个历史都市公开可见的,因此,很难找到一个安全的随机性源。以太坊随机性的最初来源之一是区块时间戳。区块时间戳的问题是矿工可能会影响它只要不早于父区块时间。大多数时候,这没有问题,但如果矿工有从错误时间戳中获益的动机,他就可以利用出块能力,赋予区块错误的时间戳以操作随机数来获利。
有几种方法克服这个限制,它们可以分为以下几组,每组都有各自的优点和缺点:
- 块hash伪随机数生成 - 块hash做为随机性源
- 预言机随机数生成 - 预言机提供随机性,请参考预言机模式
- 协作伪随机数生成 - 区块链内协作生成随机数
由于预言机模式前面已介绍,协作伪随机数的最著名项目Randao已不被积极开发,我们主要介绍块hash为随机数生成。在结果部分将对比块hash伪随机数生成和预言机随机数生成。
适用性
在以下条件时使用随机性模式
- 希望生成一个用户无法预测的随机数。
- 不想为了随机性而使用任何外部服务。
- 拥有一个可信源,能够可靠地产生随机数的种子。
参与者和协作
此模式的参与者是调用合约,可信源和矿工,矿工产生做为熵源的块hash。合约利用公开的块hash和可信源提供的种子一起计算出一个在出块前任何人都不知道的数字。
实现
最简单实现是直接使用最新的块哈希。
// Randomness provided by this is predicatable. Use with care!
function randomNumber() internal view returns (uint) {
return uint(blockhash(block.number - 1));
}
这个实现由2个问题,使它不可用:
- 如果产生的随机数对矿工不利,它可以拒绝出块,当然这会使他失去出块奖励。因此,这个问题只会发生在随机数的价值大于出块奖励时。
- 更严重的问题是
block.number
公开可见,任何用户都可以将其作为输入参数。在赌博合约情况下,用户可以使用uint(blockhash(block.number - 1)
作为其赌注输入,将一定获胜。
为了消除矿工预测和干扰随机数的可能性,Bonneau等人提出一种应用于比特币的解决方案\cite{cryptoeprint:2015:1015}: 可信源提供一个种子和未来区块的hash一起生成随机数,使得矿工无法预测生成的随机数。我们在这个模式中采用这个方式来避免恶意矿工的干扰。
可信源可有合约创建者指定并保存在合约中。开始时,用户同合约进行第一阶段交互(如下注),可信源提交种子hash后,结束下注并保存下一个区块号,将在后面使用。种子hash可以通过其值和可信用地址生成,这将简化下一步的验证。
等待至少一个块以后,可信源提交种子。根据种子和可信源地址生成种子hash和上一步提交的种子hash对比是否一致。如果一致,将使用种子和上一步保存的块hash一起生成随机数。采用上一步保存的块而非当前块是为了避免矿工的干预。上一步中保存一个将来的区块号,则是防止可信源预测生成的随机数。
如果随机数需要在一个范围内,可以使用模函数,只保留所需部分。
代码示例
下面的示例展示了下注合约使用可信源生成伪随机数。简便起见,省略了投注相关逻辑。
// This code has not been professionally audited, therefore I cannot make any promises about
// safety or correctness. Use at own risk.
contract Randomness {
bytes32 sealedSeed;
bool seedSet = false;
bool betsClosed = false;
uint storedBlockNumber;
address trustedParty = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;
function setSealedSeed(bytes32 _sealedSeed) public {
require(!seedSet);
require (msg.sender == trustedParty);
betsClosed = true;
sealedSeed = _sealedSeed;
storedBlockNumber = block.number + 1;
seedSet = true;
}
function bet() public {
require(!betsClosed);
// Make bets here
}
function reveal(bytes32 _seed) public {
require(seedSet);
require(betMade);
require(storedBlockNumber < block.number);
require(keccak256(msg.sender, _seed) == sealedSeed);
uint random = uint(keccak256(_seed, blockhash(storedBlockNumber)));
// Insert logic for usage of random number here;
seedSet = false;
betsClosed = false;
}
}
可信源在第9行硬编码到合同中。也可以增加一个只能合约所有者调用的setter方法修改它(借助访问限制模式)。用户调用bet
方法来下注。可信源调用并且也只能由其调用setSealedSeed
方法,此方法保存种子hash和下一个块号,并将seedSet
设置为true,以避免方法再次调用而覆盖种子hash,同时关闭下注,以避免可信源或矿工得到种子或块hash后去下注。
提交种子hash至少经过一个块后,可信源可以调用第25行的reveal
方法来提交种子。第26-29行执行守卫检查模式,确保已经设置了种子hash(第26行)、已下注(第27行)并且引用的块已生成(第28行)。可信源的访问限制不是必须的,因为只有它知道和种子hash匹配的种子。第29行验证提交的种子是否和前一步保存的种子hash一致。随机数在第30行根据种子和先前保存的块hash来生成。后面的步骤可能是将随机数转换到指定的范围内,或利用随机数的任何逻辑,例如支付给胜者。
结果
随机性的结果可以采用Kofler(2016)提出的以下标准评估:
- 随机性 - 达到的随机性有多好?是伪随机还是真随机?
- 安全性 - 使用的方法有多安全?
- 成本 - 产生随机性的成本有多高?
- 延迟 - 请求和接收到随机数之间的延迟有多大?
本方法产生的随机性是伪随机性。块hash和种子以确定性方式提供,如果知道知道这两个值,可以推出结果。但是,由于块hash和种子来自两个不同的源,并且两个源在知道对方值之前已提交自己的值,因此实际上不会影响产生的随机数。
一旦生成随机数,我们就认为它是安全的。唯一的不安全来自可信源。可信源这个名字并不意味着我们必须盲目地信任它。相反,即使对可信源,也应采取措施使其不能操纵随机数。我们只需要相信它一定会提交种子值,可信源先提交种子hash,只有它才知道对应的种子值。此外,以太坊只允许访问最近256个区块,这意味着可信源必须在这之前提交种子。应该实现一个针对这个情况的恢复机制,允许用户取回下注。总之,实施此模式后,唯一的欺骗方法是可信源不提交种子,或者可信源影响引用块的hash创建(它自己是矿工或和矿工勾结)。不过,和以前方案相比已有所改进。
因为不需要支付外部服务费,这种方法的成本相对较低。和最简单方式相比,由于需要额外的交易和存储,需要更多的gas费。
由于使用种子hash和未来块hash,随机数的生成会有一点延迟,最快情况下,两个块后会产生。
将这些结果同预言机模式比较,可以发现它们的区别。预言机能够提供真正的随机性,因为可以从提供真正随机数的服务中获得结果。上面示例中,只需要信任一方,而预言机模式需要信任两方:预言机和数据源。另一个不同是预言机必须为每个请求付费。预言机方案的延迟于上面的延迟基本一样。
综上所述,财务无关的简单合约使用简单的无种子的块hash随机性足够了。对于高资金的情况,可以采用预言机或上面所示的带有种子的解决方案,这取决于是否愿意信赖第三方。
已知应用
随机性常见于游戏或赌博合约中。根据未来块hash和种子实现随机性应用于Cryptos合约中,它是一个以太坊上的瓶盖游戏。但是,他们宣称与简单随机性相比,增加的安全性并不值得,因为合约处理的货币价值同额外花费的时间和费用并不匹配。
尽管存在信任问题,但许多合约都在使用预言机服务获取随机数。看到的合约都在使用Oraclize服务。Oraclize获得随机数的实际来源更是多样。vDice合约是一个使用Oraclize和random.org的例子,访问超过7万次。另一个依赖Oraclize的例子是Pray4Prey合约,采用WolframAlpha做为数据源。
一般来说,简单合约依赖块hash,避免额外交互。较复杂的合约或处理大资金的合约更倾向于采用预言机服务。
推而广之
前面已多次提到,区块链一个天生的特点就是确定性。这也导致随机性成为智能合约或去中心化应用的一大公认难点。无论是公链还是联盟链,只要是去中心就需要确定性,就无法使用rand
这种随机函数。
上面介绍的方法为解决随机问题提供了一个很好的方向。EOS v1.3的dice例子给出了一个更安全的方法,多方(至少是双方)各提供一部分数据,基于这个完整数据作为种子生成随机数。
这两种方式背后是一种如何在去中心环境中提供信任的思想。去中心化环境中,算法(代码)是公开的,如果数据也公开,那各方就会知道结果。为保证数据可信,同时结果又不可预知,需要把数据提交分成两步。第一步提交数据证据即Hash,等各方都完成第一步后,第二步提交真实数据。这样,各方数据完全可信,事先也无法预测结果。它不但可以用来生成随机数,也可以用在其它场景中,例如我们就基于这种方式,开发了一个去中心化石头剪子布对战游戏。
完整内容请查看智能合约设计模式系列
网友评论