Разработка системы RNG (Random Number Generator) на блокчейне
block.timestamp, block.prevrandao, хэш предыдущего блока — всё это уже пробовали использовать как источники случайности. И всё это небезопасно: майнер или валидатор может влиять на эти значения в своих интересах. В 2019 году смарт-контракт лотереи потерял $4M — атакующий контролировал несколько пулов и мог выбирать, когда отправлять транзакцию, манипулируя блочным хэшем. Случайные числа на блокчейне — нетривиальная задача, которая решается по-разному в зависимости от threat model.
Почему on-chain рандом сложен
Блокчейн детерминирован. Каждый узел должен прийти к одному результату, выполняя одни и те же операции. Это фундаментально противоречит случайности: если результат предсказуем — он не случаен. Любой источник, который виден в блокчейне до фиксации результата, может быть использован атакующим.
Validator bias — валидатор на Ethereum видит block.prevrandao (RANDAO reveal) до публикации блока. Если результат невыгоден — он может пропустить свой слот (слот пропускается, результат меняется). Стоимость атаки = потерянное вознаграждение за слот (~0.01 ETH). Если ставка в лотерее > 0.01 ETH — атака рациональна.
Chainlink VRF: стандарт для большинства случаев
Chainlink VRF (Verifiable Random Function) — наиболее проверенное решение для NFT-минта, лотерей, игровой механики. Работает через оракульную сеть:
- Контракт запрашивает случайное число, отправляя LINK
- Chainlink-нода генерирует случайное число и криптографическое доказательство
- Доказательство верифицируется on-chain перед использованием числа
// VRF V2.5 (актуальная версия)
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 Lottery is VRFConsumerBaseV2Plus {
uint256 public s_subscriptionId;
bytes32 public keyHash; // gas lane
uint32 public callbackGasLimit = 200_000;
uint16 public requestConfirmations = 3;
mapping(uint256 => address) public requestToPlayer;
function requestRandomWinner() 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;
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
address player = requestToPlayer[requestId];
uint256 result = randomWords[0] % totalTickets;
_declareWinner(player, result);
}
}
requestConfirmations: 3 — ждём 3 блока подтверждения перед генерацией. Это усложняет reorg-атаки на request.
Ограничения VRF: latency 1-3 блока (15-45 секунд на mainnet), стоимость LINK на каждый запрос (0.25-2 LINK в зависимости от сети), необходимость subscription менеджмента. Для high-frequency gameplays (каждый ход в игре требует рандома) — слишком дорого и медленно.
Commit-reveal: рандом без оракула
Для случаев без доступа к Chainlink или при необходимости минимизировать затраты — commit-reveal схема:
Фаза commit: каждый участник публикует keccak256(secret, address). Secret хранится off-chain.
Фаза reveal: участники раскрывают secret. XOR всех secrets = итоговое случайное число.
mapping(address => bytes32) public commits;
mapping(address => uint256) public reveals;
uint256 public combinedRandom;
function commit(bytes32 commitment) external {
commits[msg.sender] = commitment;
}
function reveal(uint256 secret) external {
require(keccak256(abi.encode(secret, msg.sender)) == commits[msg.sender]);
reveals[msg.sender] = secret;
combinedRandom ^= secret; // XOR всех reveals
}
Слабое место commit-reveal: последний, кто раскрывает secret, видит финальный результат до публикации. Он может выбрать не раскрывать (griefing) или раскрыть только если результат выгоден. Mitigation: штраф за не-reveal (bond при commit, который сгорает при неявке).
RANDAO: нативный Ethereum рандом после Merge
После перехода на PoS Ethereum предоставляет block.prevrandao — агрегированный RANDAO reveal от валидаторов. Это лучше, чем старый block.difficulty, но имеет описанную выше проблему validator bias.
Для некритичных применений (косметика в игре, порядок в очереди, некрупные лотереи) — block.prevrandao достаточен и бесплатен:
uint256 random = uint256(keccak256(abi.encode(
block.prevrandao,
block.timestamp,
msg.sender,
nonce++
)));
Добавление msg.sender и nonce увеличивает entropy и усложняет предсказание для конкретного пользователя, хотя не устраняет validator bias.
Выбор решения под задачу
| Применение | Ставка / ценность | Рекомендация |
|---|---|---|
| NFT минт (whitelist рандом) | Высокая | Chainlink VRF |
| Лотерея с крупным призом | Высокая | Chainlink VRF + requestConfirmations: 5+ |
| Внутриигровой рандом (предметы) | Средняя | Commit-reveal или Chainlink VRF |
| Порядок в очереди | Низкая | block.prevrandao |
| PvP матчмейкинг | Низкая | block.prevrandao + nonce |
Гибридные решения
Для gamefi-проектов, где требуется быстрый рандом с высокой throughput, используем off-chain VRF с on-chain commitment:
- Backend генерирует seed через Chainlink VRF заранее
- Hash seed публикуется on-chain (commitment)
- При каждом игровом событии — используем HMAC(seed, event_id) как рандом
- После сессии — раскрываем seed, пользователи могут верифицировать все результаты
Это даёт instant response на каждое действие и полную верифицируемость постфактум.
Процесс работы
Аналитика. Определяем threat model: кто может атаковать? Какова максимальная выгода от манипуляции? Какая latency допустима? Есть ли доступ к Chainlink на целевом чейне?
Разработка и тестирование. VRFConsumer тестируем через VRFCoordinatorV2_5Mock из пакета Chainlink — позволяет симулировать fulfillment в unit-тестах без реальной оракульной сети. Commit-reveal тестируем на сценарии griefing и last-revealer атак.
Деплой. Для Chainlink VRF — создаём subscription, фондируем LINK, добавляем consumer. Настраиваем мониторинг баланса subscription.
Ориентиры по срокам
Интеграция Chainlink VRF в существующий контракт: 1-2 дня. Система RNG с commit-reveal и anti-griefing: 1-2 дня. Гибридный off-chain VRF с on-chain commitment и верификацией: 3-5 дней.







