تصادفی بودن کامل در شبکه اتریوم تقریبا غیر ممکن است. دلیل این امر این است که لازم است که تراکنشها توسط نودهای مختلفی روی شبکه تایید شوند. اگر یک تابع قرارداد هوشمند کاملا تصادفی باشد، هر نودی که تراکنش را با استفاده از آن تابع تایید کند، منجر به ایجاد نتایج مختلفی خواهد شد و این یعنی تراکنش هرگز روی بلاکچین اتریوم تایید نمیشود. اخیرا اعلامیهای در این زمینه توسط یکی از بزرگترین بازیگران اکوسیستم اتریوم بیرون داده شد که موجب شور و هیجان زیادی پیرامون این مشکل شد. با استفاده از یک سیستم بنام عملکرد تصادفی قابل تایید (VRF)، قراردادهای هوشمند اتریوم حالا میتوانند اعداد تصادفی ایجاد کنند. این یعنی مفاهیمی که قبلا برای قرارداد هوشمند بسیار مناسب بودند و تنها به دلیل نیاز به اعداد تصادفی قابل اجرا نبودند، اینک در عمل قابل اجرا هستند. یکی از چنین مفاهیمی مفهوم قرعهکشی (Lottery) در اتریوم است که اینجا مورد بحث قرار میگیرد. برای آشنایی با لاتاری در اتریوم با میهن بلاکچین همراه باشید.
ساخت یک قرارداد هوشمند اتریوم برای قرعه کشی
قرعه کشی در اتریوم سه مرحله خواهد داشت. مرحله اول مرحله باز کردن (Open) است که هر کس میتواند اعداد جدید را در قبال کارمزدی کم ارائه دهد. مرحله دوم مرحله بستن (Closed) است که هیچ عدد جدیدی را نمیتوان ارائه داد و عدد تصادفی در حال ایجاد شدن است. مرحله سوم مرحله خاتمه است که عدد ایجاد شده است و به برنده پرداخت شده است. اگر کسی برنده نشود، قرارداد قرعهکشی متوقف میشود و جایزه افزایش پیدا میکند.
تعریف مراحل
مراحل ساخت قرارداد هوشمند قرعه کشی در اتریوم باید اقدامات را محدود کنند، بنابراین تنها فعالیتهای مجاز را میتوان اجرا کرد. به عنوان مثال، تنها مرحلهای که باید ارائه جدید را اجازه دهد، مرحله افتتاح است. اگر قرعهکشی بسته یا تمام شود، قرارداد هوشمند باید ارائههای جدید را ممنوع کند.
با استفاده از enum، میتوانیم هر چقدر که بخواهیم مرحله تعریف کنیم. اجازه دهید آن را LotteryState بنامیم. در متغیرهای وضعیت، ما مورد زیر را تعریف میکنیم:
enum LotteryState { Open, Closed, Finished }
LotteryState public state;
حال که شمارش تعریف شد، میتوان قوانین را در توابع تنظیم کرد و اطمینان حاصل کرد که وضعیت حال حاضر این قرارداد هوشمند اتریوم همان چیزی است که ما انتظار داریم.
این وضعیتهای require احتمالا در سراسر قرارداد مشابه هستند، اجازه دهید آن را به حداقل برسانیم. ما میتوانیم تعدیلکنندهای را تعریف کنیم که اظهار require را اجرا کند. و در نهایت میتوانیم آن را برای هر تابعی که بخواهیم تعیین کنیم.
modifier isState(LotteryState _state) {
require(state == _state, "Wrong state for this action");
_;
}
حال وقتی که ما توابع را تعریف میکنیم، میتوانیم این تعدیلکننده را اضافه کنیم تا از وضعیت حال حاضر قرعهکشی اطمینان حاصل کنیم و مطابق انتظار ما باشد.
ارائه اعداد
تا زمانی که کارمزد ورود پرداخت شده باشد، هر کسی باید اجازه ارائه یک عدد داشته باشد. البته هر واردشونده نمیتواند همان عدد را بیش از یک بار ارائه دهد. تنها وضعیتی که باید ارائههای جدید اجازه داده شود، وضعیت یا مرحله افتتاح است. تابع SubmitNumber ما به صورت زیر است: function submitNumber(uint _number) public payable isState(LotteryState.Open) { require(msg.value >= entryFee, “Minimum entry fee required”); require(entries[_number].add(msg.sender), “Cannot submit the same number more than once”); numbers.push(_number); numberOfEntries++; payable(owner()).transfer(ownerCut); emit NewEntry(msg.sender, _number); } view rawsubmitNumber.sol hosted with ❤ by GitHub
خط اول نام، پارامتر خاص _number و این حقیقت که آن public و payable است را تعریف میکند. آن همچنین تعدیلکننده isState را اضافه میکند تا از باز بودن قرعهکشی اطمینان حاصل شود.
خط ۲ اطمینان حاصل میکند که کارمزد صحیح ورود پرداخت شده است و خط ۳ اطمینان حاصل میکند که فرستنده آن پیام از قبل آن عدد را ارائه نداده است و آن را به ورودیهای فرآیند اضافه میکند.
متغیر entries به نوعی طرحبندی اشاره دارد که عدد حدس زده شده و مجموعهای از آدرسهایی که داخل عدد شدهاند را تعریف میکند. آن به صورت زیر تعریف میشود:
mapping(uint => EnumerableSet.AddressSet) entries;
AdressSet به قرارداد هوشمند EnumerableSet اشاره دارد که عملکرد اضافی را برای تایپهای اولیه فراهم میآورد. زمانی که بررسیها کامل شد، چهار خط بعدی، شماره را به حدسها اضافه میکنند و درصد کوچکی از کات (cut) مالک را پرداخت کرده و یک واقعه NewEntry صادر میکنند.
طراحی عدد
اگر شما نحوه استفاده از VRF را بدانید، میدانید که ایجاد یک عدد تصادفی تنهایی به این سادگی نیست که یک تابع خاص را فراخوانی کنید. برای ایجاد یک عدد تصادفی، باید تصادفی بودن را از VRF تقاضا کنید و تابعی را اعمال کنید که VRF بتواند با پاسخ فراخوانی کند. به همین خاطر، لازم است که یک مصرفکننده VRF تعریف کنیم که آن را در شکل دوم RandomNumberGenerator مینامیم. pragma solidity ^0.6.2; import “./VRFConsumerBase.sol”; import “./Lottery.sol”; contract RandomNumberGenerator is VRFConsumerBase { address requester; bytes32 keyHash; uint256 fee; constructor(address _vrfCoordinator, address _link, bytes32 _keyHash, uint256 _fee) VRFConsumerBase(_vrfCoordinator, _link) public { keyHash = _keyHash; fee = _fee; } function fulfillRandomness(bytes32 _requestId, uint256 _randomness) external override { Lottery(requester).numberDrawn(_requestId, _randomness); } function request(uint256 _seed) public returns(bytes32 requestId) { require(keyHash != bytes32(0), “Must have valid key hash”); requester = msg.sender; return this.requestRandomness(keyHash, fee, _seed); } } view rawRandomNumberGenerator.sol hosted with ❤ by GitHub
قرعهکشی ما آدرس این قرارداد هوشمند اتریوم را به عنوان یک پارامتر به این ساخت وارد میکند. وقتی که عدد را طراحی میکنیم، آن تابع request نامیده خواهد شد. این تصادفی بودن را از VRF تقاضا میکند که در عوض جواب را برای filfullRandomness در خط ۱۸ فراهم میآورد. شما در فراخوانیهای شکل ۲ میبینید که این به قرارداد Lottery ما با numberDrawn برگشت داده میشود. اجازه دهید آن توابع را تعریف کنیم: function drawNumber(uint256 _seed) public onlyOwner isState(LotteryState.Open) { _changeState(LotteryState.Closed); randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed); emit NumberRequested(randomNumberRequestId); } function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) { if (_randomNumberRequestId == randomNumberRequestId) { winningNumber = _randomNumber; emit NumberDrawn(_randomNumberRequestId, _randomNumber); _payout(entries[_randomNumber]); _changeState(LotteryState.Finished); } } view rawdrawingNumbers.sol hosted with ❤ by GitHub
drawNumber در تعریف ما، تنها میتواند توسط مالک قرعهکشی فراخوانی شود و تنها وقتی میتواند فراخوانی شود که قرعهکشی در وضعیت افتتاح باشد. numberDrawn در خط ۷، تابعی است که زمانی که عدد تصادفی توسط VRF دریافت شد، توسط fulfillRandomness فراخوانی میشود. آن اطمینان حاصل میکند که request-id، آی دی (ID) بازگشت داده شده از درخواست است که رویداد را صادر میکند، به برنده پرداخت انجام میدهد و وضعیت قرعهکشی را به finished تغییر میدهد. و در نهایت کد نود کامل به صورت زیر خواهد بود:
pragma solidity >=0.6.2;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/EnumerableSet.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "./RandomNumberGenerator.sol";
contract Lottery is Ownable{
using EnumerableSet for EnumerableSet.AddressSet;
using Address for address;
using SafeMath for uint;
enum LotteryState { Open, Closed, Finished }
mapping(uint => EnumerableSet.AddressSet) entries;
uint[] numbers;
LotteryState public state;
uint public numberOfEntries;
uint public entryFee;
uint public ownerCut;
uint public winningNumber;
address randomNumberGenerator;
bytes32 randomNumberRequestId;
event LotteryStateChanged(LotteryState newState);
event NewEntry(address player, uint number);
event NumberRequested(bytes32 requestId);
event NumberDrawn(bytes32 requestId, uint winningNumber);
// modifiers
modifier isState(LotteryState _state) {
require(state == _state, "Wrong state for this action");
_;
}
modifier onlyRandomGenerator {
require(msg.sender == randomNumberGenerator, "Must be correct generator");
_;
}
//constructor
constructor (uint _entryFee, uint _ownerCut, address _randomNumberGenerator) public Ownable() {
require(_entryFee > 0, "Entry fee must be greater than 0");
require(_ownerCut < _entryFee, "Entry fee must be greater than owner cut");
require(_randomNumberGenerator != address(0), "Random number generator must be valid address");
require(_randomNumberGenerator.isContract(), "Random number generator must be smart contract");
entryFee = _entryFee;
ownerCut = _ownerCut;
randomNumberGenerator = _randomNumberGenerator;
_changeState(LotteryState.Open);
}
//functions
function submitNumber(uint _number) public payable isState(LotteryState.Open) {
require(msg.value >= entryFee, "Minimum entry fee required");
require(entries[_number].add(msg.sender), "Cannot submit the same number more than once");
numbers.push(_number);
numberOfEntries++;
payable(owner()).transfer(ownerCut);
emit NewEntry(msg.sender, _number);
}
function drawNumber(uint256 _seed) public onlyOwner isState(LotteryState.Open) {
_changeState(LotteryState.Closed);
randomNumberRequestId = RandomNumberGenerator(randomNumberGenerator).request(_seed);
emit NumberRequested(randomNumberRequestId);
}
function rollover() public onlyOwner isState(LotteryState.Finished) {
//rollover new lottery
}
function numberDrawn(bytes32 _randomNumberRequestId, uint _randomNumber) public onlyRandomGenerator isState(LotteryState.Closed) {
if (_randomNumberRequestId == randomNumberRequestId) {
winningNumber = _randomNumber;
emit NumberDrawn(_randomNumberRequestId, _randomNumber);
_payout(entries[_randomNumber]);
_changeState(LotteryState.Finished);
}
}
function _payout(EnumerableSet.AddressSet storage winners) private {
uint balance = address(this).balance;
for (uint index = 0; index < winners.length(); index++) {
payable(winners.at(index)).transfer(balance.div(winners.length()));
}
}
function _changeState(LotteryState _newState) private {
state = _newState;
emit LotteryStateChanged(state);
}
}
view rawlottery.sol hosted with ❤ by GitHub
پرسش و پاسخ (FAQ)
- چگونه با استفاده از قرارداد هوشمند اتریوم قرعه کشی انجام بدهیم؟
کد قرعه کشی در اتریوم را میتوان با استفاده از توابع تصادفی قابل تایید (VRF) و زبان سالیدیتی ایجاد کرد. کد ایجاد لاتاری در اتریوم را در ادامه مطلب میتوانید مشاهده کنید.
- نحوه ایجاد قرار داد هوشمند در اتریوم چگونه است؟
قراردادهای هوشمند صرفا کدهایی هستند که روی شبکه بلاکچینی ذخیره شده و شرایط و توابع از پیش تعیین شده را اجرا میکنند. اسمارت کانترکتهای اتریوم به زبان سالیدیتی نوشته میشوند.
جمعبندی
آنچه که دیدید یک پیادهسازی اولیه از قرارداد هوشمند قرعه کشی در اتریوم است. اما نشان میدهد که چگونه ظهور تصادفی بودن قابل تایید در بلاک چین، میزان پیچیدگی قراردادهایی مانند قرارداد هوشمند قرعهکشی را در بستر اتریوم کاهش میدهد. در قراردادهای شرطبندی قبلی لازم بود که از مکانیسمهای هشینگ، مکانیسمهای زمانی، مکانیسمهای مبتنی بر بلاک (Block) و غیره استفاده کنند که آسیبپذیری در همه آنها به چشم میخورد.
آیا برای نوشتن قرارداد هوشمند قرعه کشی در اتریوم تلاش کردهاید؟ لطفا تجربیات و نظرات خود را با ما در میان بگذارید.