一、什么是隨機數(shù)
隨機數(shù)都是由隨機數(shù)生成器(Random Number Generator)生成的。隨機數(shù)分為”真隨機數(shù)“和”偽隨機數(shù)“兩種。
1、 真隨機數(shù)
真正的隨機數(shù)是使用物理現(xiàn)象產(chǎn)生的:比如擲錢幣、骰子、轉(zhuǎn)輪、使用電子元件的噪音、核裂變等等,這樣的隨機數(shù)發(fā)生器叫做物理性隨機數(shù)發(fā)生器,它們的缺點是技術(shù)要求比較高。 ----百度百科
根據(jù)百科上的定義可以看到,真隨機數(shù)是依賴于物理隨機數(shù)生成器的。使用較多的就是電子元件中的噪音等較為高級、復雜的物理過程來生成。
2、偽隨機數(shù)
真正意義上的隨機數(shù)(或者隨機事件)在某次產(chǎn)生過程中是按照實驗過程中表現(xiàn)的分布概率隨機產(chǎn)生的,其結(jié)果是不可預測的,是不可見的。而計算機中的隨機函數(shù)是按照一定算法模擬產(chǎn)生的,其結(jié)果是確定的,是可見的。我們可以這樣認為這個可預見的結(jié)果其出現(xiàn)的概率是100%。所以用計算機隨機函數(shù)所產(chǎn)生的“隨機數(shù)”并不隨機,是偽隨機數(shù)。---百度百科
從定義我們可以了解到,偽隨機數(shù)其實是有規(guī)律的。只不過這個規(guī)律周期比較長,但還是可以預測的。主要原因就是偽隨機數(shù)是計算機使用算法模擬出來的,這個過程并不涉及到物理過程,所以自然不可能具有真隨機數(shù)的特性。
二、以太坊上的隨機數(shù)
1、為什么沒有random方法?
以太坊作為區(qū)塊鏈,是一種確定性的圖靈機,所有分布式節(jié)點需要對鏈上狀態(tài)改變達成共識,就需要交易在所有節(jié)點上的計算結(jié)果都是一樣的。這意味著以太坊不能涉及隨機性。如果存在隨機的操作碼,則所有礦工將獲得不同的結(jié)果,網(wǎng)絡(luò)將無法達成共識。
2、兩種來源
以太坊上沒有random方法,但并不代表在以太坊上對隨機數(shù)沒有需求。在一些業(yè)務(wù)場景下,特別是菠菜類Dapp,對隨機數(shù)是有強需求的。
例如在彩票的場景下,現(xiàn)實生活中,彩票開獎是由彩票中心使用彩票機開獎的(看起來是隨機生成的號碼,但確一直被人懷疑)。在區(qū)塊鏈上,我們需要中獎的彩票號是隨機產(chǎn)生的,從而保證游戲的公平性和可信力。
在以太坊上,所使用的隨機數(shù)主要有兩種來源,一種是通過鏈上生成,一種是通過鏈下生成。
三、鏈上生成隨機數(shù)
鏈上生成隨機數(shù)的核心是在交易被打包到區(qū)塊之前盡可能的選取不可預測的種子(數(shù))來生成隨機數(shù)。
接下來介紹的幾種方法,其區(qū)別也是隨機數(shù)生成種子的可預測性不同,越不可預測,其安全性也就越高。
1、不怎么安全的隨機數(shù)
在一筆交易中,這筆交易什么時候,被誰打包到區(qū)塊中,對用戶來說是不可知的,但是一旦被打包到區(qū)塊中,這些值就是確定的了,因此我們可以利用區(qū)塊的打包時間block.timestamp、區(qū)塊的打包難度block.difficulty作為種子生成隨機數(shù)。0-100隨機數(shù)生成器代碼如下:
<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>
其中:
- abi.encodePacked 緊密打包數(shù)據(jù)的 bytes 而沒有任何填充,因為如果沒有填充,則無法從此函數(shù)中提取數(shù)據(jù)。函數(shù)返回
bytes類型,可以轉(zhuǎn)化為uint256類型。 - keccak256 是和 sha3 類似的 hash 函數(shù),只是采用不同的補齊模式。
雖然block.timestamp和block.difficulty對普通用戶來說無法預測,但是對礦工來說,卻是可以操控的。有足夠的利益驅(qū)動,礦工可以持續(xù)對區(qū)塊進行挖礦打包,直到計算出對自己有利的隨機數(shù),進而打包區(qū)塊。
針對這種情況,我們需要加強我們的隨機數(shù)生成器,可以通過引起業(yè)務(wù)數(shù)據(jù)來加強。
2、利用重復哈希加強安全性
通過對第一種生成的隨機數(shù)作為數(shù)據(jù)源重復進行哈希運算,同樣可以大大增大礦工的攻擊成本,增強安全性。
重復哈希是將哈希函數(shù)的一次運行的輸出用作下一次運行的輸入,從而多次運行哈希函數(shù)的行為。如果初始輸入值有稍微的變動,最終計算的結(jié)果也會有天壤之別。

3、利用業(yè)務(wù)邏輯生成相對安全的隨機數(shù)
將業(yè)務(wù)數(shù)據(jù)加入到隨機數(shù)生成器中,可以解決礦工利用隨機數(shù)生成器攻擊Dapp。
這里以彩票合約為示例,用戶Tjaden Hess(https://ethereum.stackexchange.com/users/131/tjaden-hess)在stackoverflow上對彩票合約提出過比較好的解決方案。其核心是使用玩家的地址和所選號碼作為隨機數(shù)生成器的種子。
彩票合約的邏輯是:
- 新一期彩票投注開啟,玩家提交以太坊地址和投注號碼計算的哈希,之所以提交hash是為了保障在計算隨機數(shù)(中獎號碼)之前,無法預知投注號碼
- 按照區(qū)塊數(shù)或者參與者達到上限,投注截止
- 投注玩家提交自己的投注號碼,合約會根據(jù)之前玩家提交的hash值進行校驗。此時玩家投注的號碼已不可改變
- 組織者開獎,從投注號碼中隨機選擇中獎號碼(取隨機數(shù)),并將獎金發(fā)放給中獎用戶
彩票合約代碼如下:
//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
四、鏈下生成隨機數(shù)
鏈下方式生成隨機數(shù)供鏈上使用,主要通過預言機 oracle來實現(xiàn),而預言機又分為中心化預言機和去中心預言機。
1、中心化
使用中心化的方式生成隨機數(shù)其首要前提是要保證隨機數(shù)的可信性,這里推薦使用random,地址:https://www.random.org/
2、去中心化
目前有很多oracle服務(wù)提供隨機數(shù),如
- randao提供了commit reveral合約和BLS合約兩種方式提供隨機數(shù),其白皮書詳見:https://randao.org/whitepaper/Randao_v0.85.pdf
- Niguez隨機引擎,使用說明詳見:https://github.com/niguezrandomityengine/guide/blob/master/guide.md
五、總結(jié)
對于以太坊合約中使用隨機數(shù),永遠沒有最安全的方式,只有最適合業(yè)務(wù)場景的方式。
如果業(yè)務(wù)數(shù)據(jù)本身具有隨機性,可選擇利用業(yè)務(wù)數(shù)據(jù)作為隨機數(shù)生成器的種子;
如果業(yè)務(wù)場景(合約)不涉及利益或者利益驅(qū)動比較小的情況下,使用區(qū)塊變量+重復hash的方式完全可以滿足需求;
在一些安全性要求非常高的場景下,可以選擇預言機提供隨機數(shù)服務(wù),但會犧牲請求效率。