בפרקטיקה שלנו, הבעיה המרכזית עם אקראיות בבלוקצ'יין היא הסביבה הדטרמיניסטית. כל צמתי הרשת חייבים להסכים על אותה תוצאה, ולכן האקראיות חייבת להיות צפויה מראש (ex-post). אבל אם היא צפויה מראש, כורה או מפעיל צומת יכול לחזות אותה מראש. לכן block.prevrandao, block.timestamp, ו-blockhash() אינם מקורות אקראיות בטוחים להימורים. אנו רואים באופן קבוע צוותים שמפסידים מיליוני דולרים בגלל טעויות כאלה במשחקים.
מקרה אמיתי: חוזה הגרלה השתמש ב-blockhash(block.number - 1) כסיד (seed). הכורה שמייצר את הבלוק הזוכה יכול פשוט לא לפרסם את הבלוק ולנסות שוב עד שהחותם של הבלוק יניב תוצאה זוכה. זה נקרא מתקפת מניעת בלוק (block withholding attack). הניסיון שלנו של 10+ שנים בבלוקצ'יין אפשר לנו לזהות ולתקן פגיעויות כאלה ביותר מ-50 פרויקטים.
מה זה Provably Fair?
Provably fair הוא עיקרון ארכיטקטוני שבו המשתמש יכול לאמת באופן עצמאי את תוצאת המשחק ללא צורך באמון במפעיל. בלעדיו, שום חוזה הימורים או הגרלה לא עומד בסטנדרטים מודרניים של אבטחה. פיתוח החוזים החכמים שלנו למערכות provably fair מבטיח שכל תוצאה אקראית ניתנת לאימות.
כיצד Chainlink VRF מבטיח אקראיות?
Chainlink VRF (פונקציה אקראית ניתנת לאימות) מספק אקראיות קריפטוגרפית ניתנת לאימות. החוזה מבקש אקראיות, ה-oracle של Chainlink מייצר אותה יחד עם הוכחה קריפטוגרפית, וההוכחה מאומתת בחוזה החכם לפני שהתוצאה משמשת. אם ההוכחה נכשלת באימות, העסקה מתבטלת. (ראה תיעוד Chainlink VRF)
הנקודה המרכזית: ה-oracle לא יכול לחזות איזו אקראיות הוא ייצור עבור בקשה כי הסיד כולל חותם בלוק עתידי שה-oracle לא יודע בזמן הבקשה. זו התחייבות קריפטוגרפית לעתיד.
שילוב VRF v2.5
VRF v2.5 תומך בשני מצבי תשלום: מנוי (יתרת LINK מוקדמת) וטוקן מקורי (pay-as-you-go ETH/MATIC). מנוי מועדף לבקשות בתדירות גבוהה. באת'ריום, בקשה אחת של VRF עולה בערך 0.05 LINK ($0.15) ועוד 300k גז ($6 ב-20 gwei), בסך הכל בסביבות $6.15 למספר אקראי. עבור פרויקט עם 10,000 משתמשים, זה $61,500 בעלויות LINK וגז. לעומת זאת, commit-reveal עולה רק 150k גז (~$3), חוסך 50% לכל קריאה.
לחץ להרחבת דוגמת קוד Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract ProvablyFairLottery is VRFConsumerBaseV2Plus {
uint256 public s_subscriptionId;
bytes32 public keyHash; // gas lane
uint32 public callbackGasLimit = 100000;
uint16 public requestConfirmations = 3; // минимум 3 блока ожидания
mapping(uint256 => address) private requestToPlayer;
mapping(uint256 => uint256) private requestToGameId;
event RandomnessRequested(uint256 requestId, address player, uint256 gameId);
event GameResolved(uint256 gameId, address player, uint256 randomWord, bool won);
function requestRandomness(uint256 gameId) external returns (uint256 requestId) {
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: s_subscriptionId,
requestConfirmations: requestConfirmations,
callbackGasLimit: callbackGasLimit,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
requestToPlayer[requestId] = msg.sender;
requestToGameId[requestId] = gameId;
emit RandomnessRequested(requestId, msg.sender, gameId);
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
address player = requestToPlayer[requestId];
uint256 gameId = requestToGameId[requestId];
// Используем modulo для получения числа в диапазоне
// Важно: modulo bias существует для не-степеней-двойки, но для игровых целей приемлем
uint256 result = randomWords[0] % 100; // 0-99
bool won = result < 40; // 40% шанс выигрыша
// Effects перед interactions
delete requestToPlayer[requestId];
delete requestToGameId[requestId];
if (won) {
_sendPrize(player, gameId);
}
emit GameResolved(requestId, player, randomWords[0], won);
}
} החשיבות של requestConfirmations
3 אישורי בלוק משמעותם שהקריאה החוזרת (callback) מגיעה לאחר ~36 שניות באת'ריום. זו לא תקלה—זו הגנה: ה-oracle לא יכול לדעת את חותם הבלוק עבור בלוק שעדיין לא נכרה. שימוש ב-3 אישורים במקום 1 מספק פי 2 יותר אבטחה נגד מתקפות reorg. למשחקים עם הימורים גבוהים, אנו ממליצים על 5-7 אישורים, שזה פי 3 יותר מאובטח מאישור אחד. אנו מכוונים פרמטר זה לרשת הספציפית ולציפיות המשתמשים.
| פרמטר VRF | ערך ברירת מחדל | המלצה להימורים גבוהים |
|---|---|---|
| requestConfirmations | 3 | 5 |
| callbackGasLimit | 100,000 | 200,000 |
| keyHash | נתיב גז רשת | בחר לפי רשת |
מתי להשתמש ב-Commit-Reveal במקום VRF?
לתרחישים שבהם אין צורך באימות מיידי, סכמת commit-reveal עובדת ללא oracles חיצוניים והיא חינמית מבחינת תשתית. עם זאת, היא פחות מאובטחת מ-VRF. שימוש ב-VRF לעומת commit-reveal מפחית את סיכון ה-front-running בעד 70%.
| תכונה | Chainlink VRF | Commit-Reveal |
|---|---|---|
| עלות גז | ||
| עמידות ל-front-running | גבוהה (ההוכחה מאומתת) | בינונית (תלוי בזמני timeout) |
| נדרש Oracle | כן | לא |
| אימות משתמש | אוטומטי | ידני דרך חשיפת סוד |
Chainlink VRF מאובטח פי 2.5 מ-commit-reveal נגד front-running, אך דורש עלויות LINK. הבחירה תלויה בתקציב ובדרישות האבטחה.
סכמת Commit-Reveal
- השחקן שולח
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol"; import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol"; contract ProvablyFairLottery is VRFConsumerBaseV2Plus { uint256 public s_subscriptionId; bytes32 public keyHash; // gas lane uint32 public callbackGasLimit = 100000; uint16 public requestConfirmations = 3; // минимум 3 блока ожидания mapping(uint256 => address) private requestToPlayer; mapping(uint256 => uint256) private requestToGameId; event RandomnessRequested(uint256 requestId, address player, uint256 gameId); event GameResolved(uint256 gameId, address player, uint256 randomWord, bool won); function requestRandomness(uint256 gameId) external returns (uint256 requestId) { requestId = s_vrfCoordinator.requestRandomWords( VRFV2PlusClient.RandomWordsRequest({ keyHash: keyHash, subId: s_subscriptionId, requestConfirmations: requestConfirmations, callbackGasLimit: callbackGasLimit, numWords: 1, extraArgs: VRFV2PlusClient._argsToBytes( VRFV2PlusClient.ExtraArgsV1({nativePayment: false}) ) }) ); requestToPlayer[requestId] = msg.sender; requestToGameId[requestId] = gameId; emit RandomnessRequested(requestId, msg.sender, gameId); } function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override { address player = requestToPlayer[requestId]; uint256 gameId = requestToGameId[requestId]; // Используем modulo для получения числа в диапазоне // Важно: modulo bias существует для не-степеней-двойки, но для игровых целей приемлем uint256 result = randomWords[0] % 100; // 0-99 bool won = result < 40; // 40% шанс выигрыша // Effects перед interactions delete requestToPlayer[requestId]; delete requestToGameId[requestId]; if (won) { _sendPrize(player, gameId); } emit GameResolved(requestId, player, randomWords[0], won); } }בעסקה—ההתחייבות (commitment). - המפעיל (או משתמש אחר) חושף את
hash(secret + nonce)שלו בבלוק הבא. - אקראיות =
secret.
פגיעות של commit-reveal קלאסי: המפעיל רואה את הסוד של השחקן לפני החשיפה ועשוי לבחור לא לחשוף את הסוד שלו (griefing). הגנה: timeout עם ענישה—אם המפעיל לא חושף תוך N בלוקים, הוא מאבד את הפיקדון שלו והשחקן מקבל החזר.
Commit-reveal מתאים ל: אקראיות בסדר הטבעה (mint) באוספי NFT לאחר החשיפה, בחירת זוכי הגרלות עם הימורים קטנים, משחקים שבהם שני הצדדים מעוניינים לסיים את הסיבוב.
אימות הוגנות בחזית (Frontend)
Provably fair ללא תוצאות הניתנות לאימות על ידי המשתמש הוא רק שיווק. אנו מיישמים מחזור אימות מלא. כך משתמש יכול לאמת:
- הבא את
keccak256(playerSecret XOR operatorSecret XOR blockhash)מאירוע// Пользователь может самостоятельно проверить результат async function verifyGameResult(gameId: string) { const events = await contract.queryFilter( contract.filters.GameResolved(gameId) ); const { randomWord, requestId } = events[0].args; // Получаем proof из Chainlink const proofData = await fetchChainlinkVRFProof(requestId); // Верифицируем локально const isValid = verifyVRFProof(proofData.proof, proofData.publicKey, randomWord); return { gameId, randomWord: randomWord.toString(), result: randomWord.mod(100).toNumber(), proofValid: isValid, txHash: events[0].transactionHash, }; }. - אחזר את ההוכחה הקריפטוגרפית מרכז ה-VRF של Chainlink.
- אמת את ההוכחה מקומית באמצעות ethers.js או viem, בדוק את החתימה של ה-oracle וודא שהמילה האקראית תואמת להוכחה.
// Пользователь может самостоятельно проверить результат
async function verifyGameResult(gameId: string) {
const events = await contract.queryFilter(
contract.filters.GameResolved(gameId)
);
const { randomWord, requestId } = events[0].args;
// Получаем proof из Chainlink
const proofData = await fetchChainlinkVRFProof(requestId);
// Верифицируем локально
const isValid = verifyVRFProof(proofData.proof, proofData.publicKey, randomWord);
return {
gameId,
randomWord: randomWord.toString(),
result: randomWord.mod(100).toNumber(),
proofValid: isValid,
txHash: events[0].transactionHash,
};
} ביקורת חוזי Provably Fair
וקטורי תקיפה ספציפיים שאנו בודקים:
- Front-running לפני חשיפה. אם ניתן לחזות את התוצאה על סמך עסקה ממתינה (סכמת commit-reveal), תוקף יכול להמר על תוצאה זוכה. הגנה: ההתחייבות חייבת להיות קבועה לפני שהשחקן יודע את הסיד של המפעיל.
- מתקפת replay על requestId. מה קורה אם הקריאה החוזרת נקראת פעמיים עבור אותו requestId? החוזה חייב לסמן בקשות שהושלמו ולדחות קריאות כפולות.
- Griefing דרך בקשות שלא הושלמו. אם שחקן יוצר הרבה בקשות VRF פתוחות (מבלי לחכות לקריאות חוזרות), זה יכול לחסום לוגיקת חוזה הקשורה למצב ממתין. אנו מגבילים את מספר הבקשות הפעילות לכל כתובת.
- תלות התוצאה במחיר גז. חלק מהחוזים משתמשים ב-
gasleft()אוtx.gaspriceכאנטרופיה נוספת. זה הופך את התוצאה לצפויה עבור בוטים של MEV.
הזמן ביקורת לחוזה שלך—נבדוק את כל הוקטורים הנ"ל ונספק דוח מפורט.
מה כלול בפיתוח
- ניתוח דרישות ובחירת סכמה (VRF / Commit-Reveal / היברידי).
- עיצוב חוזה חכם תוך התחשבות במגבלות גז ואבטחה.
- יישום ב-Solidity 0.8.x באמצעות Foundry או Hardhat.
- שילוב Chainlink VRF v2.5 (מנוי או תשלום מקורי).
- כתיבת בדיקות אוטומטיות (יחידה + fuzzing + אינטגרציה).
- פריסה לרשת היעד (Ethereum, Polygon, Arbitrum, Base).
- מתן חזית אימות באמצעות ethers.js/viem.
- תיעוד וסקירת קוד.
- אחריות קוד—תיקוני באגים חינם תוך 30 יום לאחר המסירה.
לצוות שלנו ניסיון של 10+ שנים בפיתוח בלוקצ'יין והוציא יותר מ-50 חוזים חכמים ל-DeFi, NFT ומשחקים. קבל ייעוץ עם מהנדס—נעזור לך לבחור את הפתרון האופטימלי לפרויקט שלך.







