大家應該都知道平常寫程式的時候很常出現的 Random Seed 是時間,或者一些偽種子,這也是為什麼大家在玩遊戲的時候可能會發現/感覺某些時間或地點比較容易掉寶物或遇到怪物。這個概念其實就是我們生產隨機數的過程,是透過一個已知(甚至是公開)的公式計算出來的偽隨機性(Pseudorandomness)。
在 Solidity 中我們的程式碼已經都是確定的了,所以每個人都可以知道我們是怎麼產生隨機數的!大家可以想一下以下這些用 block.timestamp
或搭配 msg.sender
的想法是不是安全呢?
function random1(uint256 _randomNum) public returns(uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, _randomNum)));
}
function random2() public returns(uint256) {
return uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender));
}
uint nonce;
function random3() public returns (uint256) {
nonce++;
return uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp, nonce, msg.sender)));
}
function random4() public returns(uint256)
{
return seed = uint256(keccak256(abi.encodePacked(
block.timestamp + block.difficulty +
((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
block.gaslimit +
((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)) +
block.number
)));
}
// https://stackoverflow.com/questions/58188832/solidity-generate-unpredictable-random-number-that-does-not-depend-on-input
隨機數的概念主要是我們無法從過往的 output 判斷之後的可能(不一定要精準知道結果,光是可以判斷趨勢就很危險!),且盡量是連續型的均勻分配,在產生隨機數的過程中如果有越多可以判斷(已知)的因素,那就越危險。
那其實在可視性作為 public
時,有心人士就可以透過「其他 Contract 呼叫此 Contract」或者「交易中包含其他交易」(internal call)的方式來在「同一個狀態」下呼叫到這個 Random 函式,藉此提升每一個隨機數參數的掌握度,甚至做出一樣的隨機數。此外,block.coinbase
, block.difficulty
, block.timestamp
是可以被礦工操作的喔!
隨機數可以用於很多地方:
這邊 Bitwise Operators 有一個很好的例子,大家可以看看實際的程式碼然後再看看下面的解釋。
型態 bytes
其實就是一個長度 dynamical 的 byte[] array
,以下例子中我們拿兩個型態為 bytes
的變數進行位元運算。
Operations | A | B | A __ B |
---|---|---|---|
AND(&) | 1100 1101 | 1001 1110 | 1000 1100 |
OR(|) | 1100 1101 | 1001 1110 | 1101 1111 |
XOR(^) | 1100 1101 | 1001 1110 | 0101 0011 |
NB, NEGATION(~) | 1100 1101 | 0011 0010 |
<< x
向左移動 x 個 bits>> x
向右移動 x 個 bitsOperations | A | x | A __ B |
---|---|---|---|
<< | 1011 0101 | 3 | 1010 1000 |
>> | 1011 0101 | 3 | 0010 1101 |
接下來我們可以嘗試使用位元運算來大幅增加 block.timestamp
與 msg.sender
的哈希值的亂度,來製作一個更亂的隨機數。
function randomBitwise() public view returns(uint256[] memory){
uint256 randomHash = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender)));
uint256[] stats = new uint256[](10);
for(uint256 i; i < 10; i++){
stats[i] = randomHash;
randomHash >>= 8;
}
return stats;
}
現在最典型的作法是使用 Chainlink Oracle 做 Off-Chain 的 Requesting,背後的原理是當我們發起隨機數的索取時,每個節點都會收到這個要求,然後產生各自的隨機數,最後節點們會做 bitwise XOR 來把最終結果產出並回傳。產生隨機數這件事情是可以透過密碼學的公私鑰驗證它真的是隨機生成的,此外多個節點的同時產出與 XOR 可以避免單點錯誤。
以下例子取自於官方文件:Random Number Generation (RNG) in Solidity
要在合約中使用 Chinalink 的隨機數,必須要 Import 他們的合約:
import "https://raw.githubusercontent.com/smartcontractkit/chainlink/master/evm-contracts/src/v0.6/VRFConsumerBase.sol";
還得使用 public key hash 來當作隨機數種子,以及索取 random numbers 的費用:
contract RandomNumberConsumer is VRFConsumerBase {
bytes32 internal keyHash;
uint256 internal fee;
uint256 public randomResult;
}
接下來我們需要初始化 Chainlink VRF coordinator。包含 VRF Coordinator 與 Link Token 的地址,以及我們將要拿來當隨機種子的公鑰哈希,以及跟預言機互動的手續費值。
constructor()
VRFConsumerBase(
0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9, // VRF Coordinator
0xa36085F69e2889c224210F603D836748e7dC0088 // LINK Token
) public
{
keyHash = 0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4;
fee = 0.1 * 10 ** 18; // 0.1 LINK
}
接下來就可以呼叫 getRandomNumber()
來得到 random Number。可以看見裡面會傳入我們上面定義的變數:keyHash, fee amount 以及自定義的亂數種子。
/**
* Requests randomness from a user-provided seed
*/
function getRandomNumber(uint256 userProvidedSeed) public returns (bytes32 requestId) {
require(LINK.balanceOf(address(this)) > fee, "Not enough LINK - fill contract with faucet");
return requestRandomness(keyHash, fee, userProvidedSeed);
}
觸發這個函式以後,VRF Coordinator contract 會製造最後的亂數種子送到 Chainlink Oracle,這個最後的亂數種子是由以下五個元素哈希而成:
userProvidedSeed
keyHash
nonce 以及 block number 的存在通常是為了避免同一個合約不斷地取得同樣的 result。
最後 fulfillRandomness()
作為 VRF Coordinator 的 callback function,會去接收 random number 最後的值。
function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
randomResult = randomness;
}
最後歡迎大家拍打餵食大學生
0x2b83c71A59b926137D3E1f37EF20394d0495d72d