在以太坊上,真正的隨機性幾乎是不可能的。這是因為事務需要由網絡上的多個節點進行驗證才能確認。如果智能合約功能確實是隨機的,那麼使用該功能驗證交易的每個節點將得出不同的結果,這意味著該交易將永遠不會被確認。
以太坊生態系統中最大的參與者之一的最新聲明引起了對此問題的興奮。使用稱為可驗證隨機函數(VRF)的系統,以太坊智能合約現在可以生成隨機數。
這意味著,那些看似與智能合約完美契合,但卻無法實現的概念,因為它們現在需要隨機數。
其中一個概念是彩票。
建立彩票智能合約
我們的彩票有三個階段。第一種是開放式,任何人都可以提交新的號碼,只需支付少量費用。第二個是關閉的,沒有新的數字可以提交,隨機數正在生成。第三個已經完成,號碼已經生成,贏家已經獲得獎勵。
如果沒有人中獎,可以將彩票合約延期,從而增加頭獎籌碼。
定義階段
階段應限制操作,以便只能執行允許的操作。例如應該允許新提交的唯一階段是開放階段。如果彩票關閉或結束,合同應禁止新的提交。
使用enum,我們可以定義任意多個階段。我們稱它為LotteryState。在狀態變量中,我們定義以下內容:
1enum LotteryState { Open, Closed, Finished }
2LotteryState public state;
現在已經定義了枚舉,我們可以在函數中設置規則(require語句),以確保合約的當前狀態符合我們的期望。
鑑於這些require聲明可能在整個合約中看起來都相似,所以我們將其最小化。我們可以定義一個執行require語句的修飾符,並將其分配給我們想要的任何函數。
1modifier isState(LotteryState _state) {
2 require(state == _state, “Wrong state for this action”);
3 _;
4}
現在當我們定義函數時,我們可以添加此修飾符以確保彩票的當前狀態是我們期望的狀態。
提交數字
只要支付了最低入場費,任何人都可以提交號碼。但是每個參賽者不能一次提交同一號碼。應該允許新提交的唯一狀態是打開狀態。
這是我們的SubmitNumber函數:
1function submitNumber(uint _number) public payable isState(LotteryState.Open) {
2 require(msg.value >= entryFee, “Minimum entry fee required”);
3 require(entries[_number].add(msg.sender), “Cannot submit the same number more than once”);
4 numbers.push(_number);
5 numberOfEntries++;
6 payable(owner()).transfer(ownerCut);
7 emit NewEntry(msg.sender, _number);
8}
第1行定義了名稱,單個_number參數以及它是public的和payable的事實。它還添加了isState修飾符,以確保彩票是開放的。
第2行確保已支付正確的報名費,第3行確保消息的發件人尚未提交該號碼,並將其添加到流程中的條目中。
變量entries引用了一個映射,該映射定義了猜測的數字和已輸入該數字的一組地址。定義如下:
1mapping(uint => EnumerableSet.AddressSet) entries;
AddressSet引用OpenZeppelin EnumerableSet協定,該協定為原始類型提供附加函數。
一旦檢查完成,接下來的四行將數字添加到猜測中,支付所有者削減的一小部分,並發出NewEntry事件。
輸入數字
如果您已經閱讀了有關如何使用VRF的文章,那麼您將知道生成隨機數並不像調用單個函數那樣簡單(例如JavaScript中的Math.random())。
要生成隨機數,必須從VRF協調器請求隨機性,並實現VRF可以在響應中回調的功能。為此我們需要定義一個VRF使用者(可在此處找到創建VRF使用者的詳細信息),在圖2中將其稱為RandomNumberGenerator。
1pragma solidity ^0.6.2;
2
3import “./VRFConsumerBase.sol”;
4import “./Lottery.sol”;
5
6contract RandomNumberGenerator is VRFConsumerBase {
7
8 address requester;
9 bytes32 keyHash;
10 uint256 fee;
11
12 constructor(address _vrfCoordinator, address _link, bytes32 _keyHash, uint256 _fee)
13 VRFConsumerBase(_vrfCoordinator, _link) public {
14 keyHash = _keyHash;
15 fee = _fee;
16 }
17
18 function fulfillRandomness(bytes32 _requestId, uint256 _randomness) external override {
19 Lottery(requester ).numberDrawn(_requestId, _randomness);
20 }
21
22 function request(uint256 _seed) public returns(bytes32 requestId) {
23 require(keyHash != bytes32(0), “Must have valid key hash”);
24 requester = msg.sender;
25 return this.requestRandomness(keyHash, fee, _seed);
26 }
27}
我們的彩票將在構建時將此合同的地址作為註入參數。繪製數字時,它將調用請求函數。這要求VRF提供隨機性,然後VRF向第18行的filfullRandomness提供響應。您可以在圖2中看到調用,它調用了我們的numberDrawn彩票合約。讓我們定義這些功能:
1function drawNumber(uint256 _seed) public onlyOwner isState(LotteryState.Open) {
2_changeState(LotteryState.Closed);
3randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed);
4emit NumberRequested(randomNumberRequestId);
5}
6
7function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) {
8if (_randomNumberRequestId == randomNumberRequestId) {
9winningNumber = _randomNumber;
10emit NumberDrawn(_randomNumberRequestId, _randomNumber);
11_payout(entries[_randomNumber]);
12_changeState(LotteryState.Finished);
13}
14}
在我們的定義的第1行中,只能由彩票所有者調用drawNumber,並且只能在彩票處於打開狀態時調用。
第7行上的numberDrawn是一旦VRF接收到隨機數後,complementRandomness會回調的函數。它確保request-id是從請求返回的ID,發出事件,支付中獎者並將彩票的狀態更改為Finished。
完整代碼展示:
1pragma solidity >=0.6.2;
2
3import “@openzeppelin/contracts/access/Ownable.sol”;
4import “@openzeppelin/contracts/utils/EnumerableSet.sol”;
5import “@openzeppelin/contracts/utils/Address.sol” ;
6import “@openzeppelin/contracts/math/SafeMath.sol”;
7import “./RandomNumberGenerator.sol”;
8
9contract Lottery is Ownable{
10
11using EnumerableSet for EnumerableSet.AddressSet;
12using Address for address;
13using SafeMath for uint;
14
15enum LotteryState { Open, Closed, Finished }
16
17mapping(uint => EnumerableSet.AddressSet) entries;
18uint[] numbers;
19LotteryState public state;
20uint public numberOfEntries;
21uint public entryFee;
22uint public ownerCut;
23uint public winningNumber;
24address randomNumberGenerator;
25bytes32 randomNumberRequestId;
26
27event LotteryStateChanged(LotteryState newState);
28event NewEntry(address player, uint number);
29event NumberRequested(bytes32 requestId);
30event NumberDrawn( bytes32 requestId, uint winningNumber);
31
32// modifiers
33modifier isState(LotteryState _state) {
34require(state == _state, “Wrong state for this action”);
35_;
36}
37
38modifier onlyRandomGenerator {
39require(msg.sender == randomNumberGenerator, “Must be correct generator”);
40_;
41}
42
43//constructor
44constructor (uint _entryFee, uint _ownerCut, address _randomNumberGenerator) public Ownable() {
45require(_entryFee > 0, “Entry fee must be greater than 0”);
46require(_ownerCut < _entryFee, “Entry fee must be greater than owner cut”);
47require(_randomNumberGenerator != address(0), “Random number generator must be valid address”);
48require( _randomNumberGenerator.isContract(), “Random number generator must be smart contract”);
49entryFee = _entryFee;
50ownerCut = _ownerCut;
51randomNumberGenerator = _randomNumberGenerator;
52_changeState(LotteryState.Open);
53}
54
55//functions
56function submitNumber(uint _number) public payable isState(LotteryState.Open) {
57require(msg.value >= entryFee, “Minimum entry fee required”);
58require(entries[_number].add(msg.sender) , “Cannot submit the same number more than once”);
59numbers.push(_number);
60numberOfEntries++;
61payable(owner()).transfer(ownerCut);
62emit NewEntry(msg.sender, _number);
63}
64
65function drawNumber( uint256 _seed) public onlyOwner isState(LotteryState.Open) {
66_changeState(LotteryState.Closed);
67randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed);
68emit NumberRequested(randomNumberRequestId);
69}
70
71function rollover() public onlyOwner isState(LotteryState.Finished) {
72//rollover new lottery
73}
74
75function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) {
76if (_randomNumberRequestId == randomNumberRequestId) {
77winningNumber = _randomNumber;
78emit NumberDrawn(_randomNumberRequestId, _randomNumber);
79_payout(entries[_randomNumber]);
80_changeState(LotteryState.Finished);
81}
82}
83
84function _payout(EnumerableSet.AddressSet storage winners) private {
85uint balance = address(this).balance ;
86for (uint index = 0; index < winners.length(); index++) {
87payable(winners.at(index)).transfer(balance.div(winners.length()));
88}
89}
90
91function _changeState(LotteryState _newState) private {
92state = _newState;
93emit LotteryStateChanged(state);
94}
95 }
這是一個原始的實現,但是它顯示了可驗證的隨機性在區塊鏈上的出現如何降低了彩票之類的合約的複雜性。以前的彩票合約需要使用哈希機制,基於時間的機制,基於區塊的機制等,所有這些都容易受到攻擊。
作者:鏈三豐
來源:區塊鏈研究實驗室
更多文章:幣區
聲明:本文為作者獨立觀點,不代表幣區立場,且不構成投資建議。部分內容參照網絡信息,無法保證正確性,請謹慎對待。