在以太坊上使用的随机数来源主要有链上和链下两种途径,其中链上生成核心要解决随机数生成种子的不可预测性。
随机数都是由随机数生成器(Random Number Generator)生成的。随机数分为”真随机数“和”伪随机数“两种。
真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等等,这样的随机数发生器叫做物理性随机数发生器,它们的缺点是技术要求比较高。 ----百度百科
根据百科上的定义可以看到,真随机数是依赖于物理随机数生成器的。使用较多的就是电子元件中的噪音等较为高级、复杂的物理过程来生成。
真正意义上的随机数(或者随机事件)在某次产生过程中是按照实验过程中表现的分布概率随机产生的,其结果是不可预测的,是不可见的。而计算机中的随机函数是按照一定算法模拟产生的,其结果是确定的,是可见的。我们可以这样认为这个可预见的结果其出现的概率是100%。所以用计算机随机函数所产生的“随机数”并不随机,是伪随机数。---百度百科
从定义我们可以了解到,伪随机数其实是有规律的。只不过这个规律周期比较长,但还是可以预测的。主要原因就是伪随机数是计算机使用算法模拟出来的,这个过程并不涉及到物理过程,所以自然不可能具有真随机数的特性。
以太坊作为区块链,是一种确定性的图灵机,所有分布式节点需要对链上状态改变达成共识,就需要交易在所有节点上的计算结果都是一样的。这意味着以太坊不能涉及随机性。如果存在随机的操作码,则所有矿工将获得不同的结果,网络将无法达成共识。
以太坊上没有random方法,但并不代表在以太坊上对随机数没有需求。在一些业务场景下,特别是菠菜类Dapp,对随机数是有强需求的。
例如在彩票的场景下,现实生活中,彩票开奖是由彩票中心使用彩票机开奖的(看起来是随机生成的号码,但确一直被人怀疑)。在区块链上,我们需要中奖的彩票号是随机产生的,从而保证游戏的公平性和可信力。
在以太坊上,所使用的随机数主要有两种来源,一种是通过链上生成,一种是通过链下生成。
链上生成随机数的核心是在交易被打包到区块之前尽可能的选取不可预测的种子(数)来生成随机数。
接下来介绍的几种方法,其区别也是随机数生成种子的可预测性不同,越不可预测,其安全性也就越高。
在一笔交易中,这笔交易什么时候,被谁打包到区块中,对用户来说是不可知的,但是一旦被打包到区块中,这些值就是确定的了,因此我们可以利用区块的打包时间block.timestamp、区块的打包难度block.difficulty作为种子生成随机数。0-100随机数生成器代码如下:
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">function importSeedFromThird() public view returns (uint8) { return uint8( uint256(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % 100 ); }</pre>
其中:
bytes
类型,可以转化为 uint256
类型。虽然block.timestamp和block.difficulty对普通用户来说无法预测,但是对矿工来说,却是可以操控的。有足够的利益驱动,矿工可以持续对区块进行挖矿打包,直到计算出对自己有利的随机数,进而打包区块。
针对这种情况,我们需要加强我们的随机数生成器,可以通过引起业务数据来加强。
通过对第一种生成的随机数作为数据源重复进行哈希运算,同样可以大大增大矿工的攻击成本,增强安全性。
重复哈希是将哈希函数的一次运行的输出用作下一次运行的输入,从而多次运行哈希函数的行为。如果初始输入值有稍微的变动,最终计算的结果也会有天壤之别。
将业务数据加入到随机数生成器中,可以解决矿工利用随机数生成器攻击Dapp。 这里以彩票合约为示例,用户Tjaden Hess(https://ethereum.stackexchange.com/users/131/tjaden-hess)在stackoverflow上对彩票合约提出过比较好的解决方案。其核心是使用玩家的地址和所选号码作为随机数生成器的种子。
彩票合约的逻辑是:
彩票合约代码如下:
//THIS CONTRACT IS CONSUMING A LOT OF GAS
//THIS CONTRACT IS ONLY FOR DEMONSTRATING HOW RANDOM NUMBER CAN BE GENERATED
//DO NOT USE THIS FOR PRODUCTION
pragma solidity ^0.4.8;
contract Lottery {
mapping (uint8 => address[]) playersByNumber ;
mapping (address => bytes32) playersHash;
uint8[] public numbers;
address owner;
function Lottery() public {
owner = msg.sender;
state = LotteryState.FirstRound;
}
enum LotteryState { FirstRound, SecondRound, Finished }
LotteryState state;
function enterHash(bytes32 x) public payable {
require(state == LotteryState.FirstRound);
require(msg.value > .001 ether);
playersHash[msg.sender] = x;
}
function runSecondRound() public {
require(msg.sender == owner);
require(state == LotteryState.FirstRound);
state = LotteryState.SecondRound;
}
function enterNumber(uint8 number) public {
require(number<=250);
require(state == LotteryState.SecondRound);
require(keccak256(number, msg.sender) == playersHash[msg.sender]);
playersByNumber[number].push(msg.sender);
numbers.push(number);
}
function determineWinner() public {
require(msg.sender == owner);
state = LotteryState.Finished;
uint8 winningNumber = random();
distributeFunds(winningNumber);
selfdestruct(owner);
}
function distributeFunds(uint8 winningNumber) private returns(uint256) {
uint256 winnerCount = playersByNumber[winningNumber].length;
require(winnerCount == 1);
if (winnerCount > 0) {
uint256 balanceToDistribute = this.balance/(2*winnerCount);
for (uint i = 0; i<winnerCount; i++) {
require(i==0);
playersByNumber[winningNumber][i].transfer(balanceToDistribute);
}
}
return this.balance;
}
function random() private view returns (uint8) {
uint8 randomNumber = numbers[0];
for (uint8 i = 1; i < numbers.length; ++i) {
randomNumber ^= numbers[i];
}
return randomNumber;
}
}
彩票合约代码来源于:https://gist.github.com/promentol/d94959bfaf10f6b64d3cbf9c293de468
链下方式生成随机数供链上使用,主要通过预言机 oracle来实现,而预言机又分为中心化预言机和去中心预言机。
使用中心化的方式生成随机数其首要前提是要保证随机数的可信性,这里推荐使用random,地址:https://www.random.org/
目前有很多oracle服务提供随机数,如
对于以太坊合约中使用随机数,永远没有最安全的方式,只有最适合业务场景的方式。
如果业务数据本身具有随机性,可选择利用业务数据作为随机数生成器的种子;
如果业务场景(合约)不涉及利益或者利益驱动比较小的情况下,使用区块变量+重复hash的方式完全可以满足需求;
在一些安全性要求非常高的场景下,可以选择预言机提供随机数服务,但会牺牲请求效率。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!