智能合约错误随机性
错误随机性
智能合约开发中,在程序中使用随机数较好的伪随机数是很难的。很多看似无法被预言的随机数种子或变量,实际被预言的难度很低。
核心问题:一旦在智能合约中使用了随机性很差的随机数作为关键变量,就面临着随机数被预言的攻击风险。
PRNG相关漏洞类型
开发者生成随机数时,一般都会使用伪随机数生成器(pseudo-random number generator),简称 PRNG
。而有漏洞的PRNG,一般有三种类型:
使用区块变量作为熵源的 PRNG
基于过往区块(和私有种子)的区块哈希的 PRNG
易被抢占交易(front-running)的 PRNG
使用区块变量作为熵源
block.coinbase
表示当前区块的矿工地址
block.difficulty
表示当前区块的挖掘难度
block.gaslimit
区块内交易的最大限制燃气消耗量
block.number
表示当前区块高度
block.timestamp
表示当前区块挖掘时间
以上所有的区块变量都可以被矿工操纵,所以都不能用来做信息熵源。因为这些区块变量在同一区块上是共用的。攻击者通过其恶意合约调用受害者合约,那么此交易打包在同一区块中,其区块变量是一样的。
基于过往区块的区块哈希
每一个Ethereum区块链上的区块都有认证的hash值,通过 block.blockhash()
函数可以获取此值。此函数经常被错误地使用。
block.blockhash(block.number)
:基于当前区块的区块哈希
block.blockhash(block.number - 1)
: 基于负一区块的区块哈希
Blockhash of a future block
: 使用未来区块的区块哈希
Blockhash with a private seed
: 使用一个私有种子(seed)变量
基于当前区块的区块哈希
通过 block.number
变量可以获取当前区块区块高度。但是还没执行时,这个“当前区块”是一个未来区块,即只有当一个矿工拾取一个执行合约代码的交易时,这个未来区块才变为当前区块,所以合约才可以可靠地获取此区块的区块哈希。而一些合约曲解了block.blockhash(block.number)
的含义,误认为当前区块的区块哈希在运行过程中是已知的,并将之做为熵源。还有一点就是在以太坊虚拟机中(EVM),区块哈希恒为 0。
基于负一区块的区块哈希
1 |
|
这样的方式,虽然理论上可以获得随机数,但这个随机数是不安全的。因为攻击者可以使用改造后的FullNode
,让这笔交易可以在FullNode
上执行,并获得结果后,再选择性广播那些可以符合攻击者期望的交易,即可以操纵交易的执行结果。
攻击合约只要以相同代码执行,即可以产生到同样的伪随机数。
使用未来区块的区块哈希
第一笔交易触发合约,合约存储某个未来区块高度。
第二笔交易,合约检索当前区块高度,如果超过了存储的未来区块高度,则通过区块哈希获得伪随机数结果。
然而,这种方式也有它的局限性:在TVM中,blockhash
被限定为只能获取近256个高度区块的数据,因此在以上的两笔交易间隔超过256 * 3s,大约12.8分钟后,这种方式就会失效。
此方法只有在十分必要的时候才能使用。因为也存在一定危险性,EVM 能存储的区块哈希为最近的 256 条。超过的话值为 0。
易被抢占交易(front-running)
原理:更高的 gas 价格,交易将更快被矿工拾取打包。
为了获取最大的奖励,矿工通过每个交易的 gas 累积值来选择并创建新的区块。而这些交易的排序是基于它们的 gas 价格。最高的 gas 价格会先被执行。由此通过操纵 gas 价格,可以将交易的顺序排在当前区块的前面。这就会引发抢占交易问题。
复现
前提
Ganache CLI使用ethereumjs来模拟完整的客户端行为,使开发以太坊应用程序更快,更轻松,更安全。它还包括所有主流的RPC函数和功能(如event),并可以准确地运行以使开发变得容易。
在后文的复现中,由于在remix中使用VM会报错,所以会使用ganache-cli来进行模拟。
ganache-cli是用Javascript编写的,并通过npm作为Node包进行分发。安装之前首先要确保安装了Node.js(> = v6.11.5),可以使用node -v
来检查自己的Node.js的版本
安装
1 |
|
启动
1 |
|
漏洞demo
1 |
|
攻击合约
1 |
|
复现过程
- 在remix中运行的时候选择Web3 Provider,注意这里的Web3 Provider Endpoint应匹配使用ganache-cli中的端口
- 分别为攻击者和受害者创建智能合约
- 输入受害者合约地址进行攻击后,即可看到猜测成功,余额增加
较安全伪随机数的产生方法
hash-commit-reveal
hash-commit-reveal被很多合约开发者视为随机数的最佳实践方案,已经被广泛应用于大量的DAPP中,这里我们来看看它的工作原理。
hash-commit-reveal的本质,是合约调用者和随机数提供者(通常情况下是某外部预言机)在波场区块链平台上通过一系列协议来生成随机数。
Dice2Win采用混合模式, 巧妙地解决随机数弱, 且容易被预测的问题. 其整个流程如下:
1. 玩家指定行动计划, 并生产对应的hash值.
2. 服务端收到玩家的hash值, 产生随机值reveal, 然后根据reveal生产commit值, 把这个返回给玩家
3. 玩家带着commit和行动信息, 在智能合约下真正下注
4. 服务端发起结算, 带着真正的reveal值去结算
中间的行动计划和reveal没法中途修改, 因为有hash值的验证
其本质的思想是hash-commit-reveal, 其核心的思想是: 服务端不知道玩家的行为, 玩家不知道服务端真正的随机数. 而最终结果在合约里验证hash, 并给出预期的结果. 这样的流程, 保证玩家和服务端都满意。
此类随机数生成策略的缺点也是很明显的:高度依赖于预言机(secretSigner
)对合约的回调。因此,预言机有选择性回调的作恶风险。
Oraclize
Oraclize定位为去中心化应用的数据搬运工,它作为Web APIs和DApp的可靠链接,有了Oraclize,就不需要建立额外的信任链,因为我们的行为已经被强制加密验证。
Oraclize 提供了一个连接以太坊与外部环境(互联网)的桥梁。通过 Oraclize,智能合约能够通过 web API 请求数据。如当前的兑换率,天气预报或股票价格。其中一个最大的作用是能提供伪随机数。一些合约通过 Oraclize 中的 URL 连接器来连接 random.org 来获取伪随机数。
Oraclize是一个可证明的诚实的预言机服务,可以让智能合约访问互联网,Oraclize是平台无关的,为所有主流的智能合约平台提供一种虚拟的接口,通过Oraclize投入大量有意义的数据到区块链中,可以使得智能合约产业更加繁荣,让更多有价值的应用呈现更大的生命力,Oraclize的使用方式可以参考下面的代码:
1 |
|
考虑一个提供打赌的智能合约,用户调用打赌的接口,这个接口会把用户的请求存储起来,然后调用Oracle随机数生成服务,然后通过Oracle回调服务,判断随机数是否大于某个值,如果成立,那么用户成功,否则用户失败,这就是典型的Oracle的使用案例。
Randao
RANDAO 机制就是,当用户通过储存(质押)32 ETH 成为验证者之后,该用户可以任意选定一个随机数。当需要为某个区块公布随机数时,将所有验证者的随机数加起来就可以得到一个全新的随机数。
randao是一个DAO(去中心化的匿名组织)允许任何人加入,随机数由所有参与者一起合作生成,首先我们需要在区块链上创建一个RANDAO的智能合约,合约定义了参与规则,然后生成随机数的基本过程可以分为下面三个步骤:
第一步:收集有效的sha3(s):参与随机数生成的参与者,首先需要在一个指定的时间区间(比如6个区块的区间,大约72秒)发送m ETH作为抵押到智能合约C,同时发送一个sha3(s)的值到智能合约C ,s是一个只有参与者自己知道的数字
第二步:收集有效的s,在第一步结束后,那些提交了sha3(s)的参与者需要在指定的时间区间内发送s到智能合约C,智能合约C会检查sha3(s)和之前提交的值是否相同,相同的s会被保存到种子集合用来最终生成随机数。
第三步:计算随机数并退回抵押和奖金,在所有的秘密数字s被成功收集后,智能合约C会使用函数f(s1,s2,…,sn)来计算随机数,随机数的结果会写入智能合约的存储,而且结果会被发送到所有之前请求随机数的其他智能合约上面,智能合约C会把第一阶段的抵押返回给参与者,然后奖金会被分成同等分发送给所有的参与者,奖金来源于请求随机值的其他智能合约。
RNG补充规则:
为了确保RNG不能被操控,以及为了安全和效率,智能合约C有以下的补充规则:
在第一步中,如果有两个或更多个的同样的sha3(s)被提交上来,那么只有第一个会被接受
在第一步中,对于参与者有最低要求,如果在指定时间区间内没有收集到足够多的sha3(s)的值,那么RNG在这个区块高度会失败
如果参与者提交了sha3(s),那么他必须在第二步提交s
如果参与者在第二步没有提交s,那么第一阶段提供的m ETH会被没收而且没有奖励
如果一个或者多个s没有在第二步被提交,RNG在这个区块高度会失败,没收的ETH会被分成同等分发送给提交了s的其他参与者,其他申请随机数的其他合约的费用会被退回
RNG激励机制:
RNG的周期非常短,例如一个小时20个生成周期,如果没有周期的利润是0.001%,一个月的盈利会达到0.00001 * 20 * 24 * 30 = 0.144,为了达到14.4%每个月的盈利,并且RNG平均有n个参与者,运行智能合约C的费用为n * 3 * 500 * gasPrice + Ccost,CCost是合约内部的gas消费,包括计算和存储)假设每个随机值平均有r个请求,每个请求的费用是p ETH, 那么收入是r*p. 所以每个参与者每一次参与会收到rp - 1500n * gasPrice - Ccost)/n,当前的gasPrice是10 szabo, 合约的消费大概是1500n gas, 所以大概的净收入是(rp/n-0.03)ETH. 假设每个RNG有10个参与者,并且抵押是1000ETH,所以如果RNG如果只请求一次,那么一次的费用是0.4 ETH, 如果请求是10次,那么一次请求的价格会被降到0.04ETH
RANDAO作为以太坊系统的基础设施,被其他的合约调用,不同的合约因为有不同的目的所以需要不同的随机值,有些需要高度加密的,比如说抽奖;有些需要稳定的回应,并且要求立即作出回应,这些合约本身的价值不高;有些需要回调函数,当随机值已经生成的时候需要接收到通知。
但即使在这种情况下,最后一个公开随机数的人也可以在一定程度上操纵随机数。最后一个人可以选择保持沉默,以这样或那样的方式改变这个最终的随机数:房间里的最后一个人可以记住之前每个人公布的数字,如此一来,就可以知道加上(或者不加上)他提供的数字之后的最终随机数结果。如果相对于其他数字,某个数字对最后一个人更有利,那最后一个人就有动机去进行某种程度的操纵,不管程度高低。
对于这一问题,以太坊 2.0 将通过 VDF(可验证延迟函数)来解决!
……