在以太坊上建立可验证的随机彩票智能合约
在以太坊上,真正的随机性几乎是不可能的。这是因为事务需要由网络上的多个节点进行验证才能确认。如果智能合约功能确实是随机的,那么使用该功能验证交易的每个节点将得出不同的结果,这意味着该交易将永远不会被确认。
以太坊生态系统中最大的参与者之一的最新声明引起了对此问题的兴奋。使用称为可验证随机函数(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
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
87payable(winners.at(index)).transfer(balance.div(winners.length()));
88}
89}
90
91function _changeState(LotteryState _newState) private {
92state = _newState;
93emit LotteryStateChanged(state);
94}
95}
这是一个原始的实现,但是它显示了可验证的随机性在区块链上的出现如何降低了彩票之类的合约的复杂性。以前的彩票合约需要使用哈希机制,基于时间的机制,基于区块的机制等,所有这些都容易受到攻击。