Chainlink VRF (Verifiable Random Function) — это сервис криптографически проверяемой случайности для смарт-контрактов. Контракт-потребитель запрашивает случайное значение, оракул генерирует рандом + доказательство корректности, а Coordinator проверяет это доказательство ончейн и только затем передаёт число в колбэк потребителя. Так достигаются три свойства, важных для Web3-приложений:
- Непредсказуемость до момента публикации результата.
- Невозможность смещения результата исполнителем.
- Проверяемость на уровне смарт-контракта: потребителю не нужно «доверять» поставщику, он проверяет криптодоказательство.
Типичные кейсы: честная генерация NFT-атрибутов, распределение эирдроп-слотов/вайтлистов, лотереи и розыгрыши, случайные дропы в играх, распределение матчей/пар и любые сценарии, где нельзя полагаться на blockhash/timestamp.
Chainlink VRF: почему «наивная» случайность опасна
- blockhash и timestamp манипулируемы валидаторами/майнёрами в пределах окна; достаточно сдвинуть/отклонить блок.
- Псевдо-RNG из ончейн-состояния детерминирован и предсказуем наблюдателю.
- В commit-reveal схемах без внешнего источника часто остаётся окно смещения (последний раскрывающий может «выбрать» значение молчанием).
VRF убирает эти классы атак, потому что доказательство связывает случайность с запросом и публичным ключом провайдера — изменить число без провала верификации нельзя.
Архитектура и роли
- Consumer — ваш смарт-контракт, который запрашивает случайность и получает её в колбэке.
- VRF Coordinator — ончейн-контракт Chainlink, который принимает запросы, валидирует доказательства и вызывает ваш колбэк.
- Oracle/Prover — оффчейн-узел, генерирующий случайность и криптодоказательство корректности.
- Плательщик — источник оплаты за запрос (в версиях VRF это либо прямая оплата, либо subscription с балансом).
Высокоуровневый поток:
- Consumer вызывает requestRandomWords(…).
- Coordinator регистрирует запрос и отдаёт его оракулу.
- Оракул генерирует (randomWords, proof) и отправляет в Coordinator.
- Coordinator проверяет proof ончейн и вызывает у Consumer fulfillRandomWords(requestId, randomWords).
Именно онチェйн-верификация делает результат проверяемым и «недоверенным» к провайдеру.
Параметры запроса: что действительно важно спроектировать
- requestConfirmations — сколько подтверждений ожидать перед обработкой запроса (уменьшает риск reorg). Выше подтверждения → дольше, но безопаснее.
- callbackGasLimit — газовый лимит для fulfillRandomWords. Должен покрыть всю логику потребителя; если не хватит — колбэк упадёт.
- numWords — сколько случайных 256-битных слов нужно сразу (экономит накладные расходы).
- keyHash/«gas lane» — выбор ключа/линии обслуживания провайдеров (на разных сетях доступны разные конфигурации).
- Модель оплаты — в актуальных конфигурациях применяется подписка (subscription) c пополнением; это дешевле и удобнее «прямых платежей».
Советы:
- закладывайте запас на callbackGasLimit и избегайте тяжёлой логики в колбэке;
- подбирайте requestConfirmations под риск-профиль (лотерея на крупные суммы ≠ обычный дроп).
Модель безопасности: что гарантирует VRF
- Связность результата с запросом. Доказательство криптографически привязывает randomWords к конкретному requestId и публичному ключу оракула.
- Ончейн-верификация. Coordinator отклонит любой результат, не соответствующий доказательству.
- Нуль-доверия к исполнителю. Даже «злой» оракул не сможет подменить число; максимум — не ответить (см. эксплуатационные риски ниже).
Что не гарантирует:
- моментальное время ответа в условиях сетевых перегрузок;
- исполнение вашего колбэка, если вы занижаете callbackGasLimit или нарушаете инварианты (реентранси, проверки и т. п.).
Паттерны для Consumer-контрактов
- Идемпотентность. Свяжите requestId → «заказчик/слот» и не позволяйте повторно «получить приз». См. Идемпотентность.
- Двухфазная логика.
- Фаза 1: при requestRandomWords записать намерение (кто/что ожидает случайность).
- Фаза 2: в fulfillRandomWords применить результат. При фейле оставить состояние в безопасном виде.
- Хранилище статусов. Держите Pending/Executed/Failed; не переводите в Executed, пока колбэк не завершён.
- Проверки входа. Колбэк должен принимать вызовы только от Coordinator; защитите публичные методы.
- События для фронта. Помогают пользователю отслеживать статус (Requested, Fulfilled, Failed).
- Газ-безопасность. Никаких тяжёлых циклов/внешних вызовов внутри колбэка; лучше планировать «пост-обработку» отдельным вызовом.
Анти-паттерны:
- использование randomWords для всего без «смешивания» с пер-пользовательским сидом → облегчает предсказание. Всегда хешируйте: keccak256(abi.encode(randomWord, userSeed, nonce)).
- сдвоенные действия «минт + перевод» внутри колбэка без лимитов/пайплайна → риски газа и реентранси.
Проектирование сидов и «смешивание» случайности
Даже с проверяемым VRF-словом вы часто будете генерировать множество «локальных» случайностей. Делайте это так:
- Пер-пользовательский сид. Включайте адрес/идентификатор и nonce пользователя.
- Порядок операций. Для списка действий используйте random = keccak256(abi.encode(randomWord, i, userId, salt)).
- Диапазоны. Получить число в диапазоне [0, n) корректно через модуль, но следите за равномерностью: если распределение элементов неравномерное по n, используйте схемы «фишера-йетса» для перестановки.
Отказоустойчивость, SLA и эксплуатация
- SLA-метрики. Трекать latency до Fulfilled по процентилям P95/P99; анализировать причины хвостов (газ, перегрузка сети, очередь провайдера). См. SLA: latency P95/P99.
- Повторы/ретраи. При срыве колбэка допускайте повторный запрос (с новым requestId) и корректно закрывайте старый как Failed.
- Лимиты и квоты. Для публичных минтов ограничивайте «скорость» запросов и ставьте пер-адресные лимиты, чтобы не забить очередь.
- Мониторинг баланса подписки. Недостаток средств на subscription — популярная причина отказов.
Варианты источников случайности: сравнение
| Подход | Безопасность | UX/стоимость | Комментарий |
|---|---|---|---|
| blockhash/timestamp | уязвим для смещения | дешёво | подходит только для игрушечных задач |
| Коммит-ревил (on-chain) | лучше, но окно смещения/отказов | средне | требует двух транзакций и дисциплины участников |
| Внешний «рандомизатор» без доказательств | доверие к провайдеру | зависит | «чёрный ящик», неподходит для DeFi |
| Chainlink VRF | проверяемая ончейн-доказательность | умеренно | стандарт индустрии для on-chain честности |
VRF можно комбинировать с коммит-ревил (например, «VRF + пер-пользовательский коммит»), чтобы дополнительно защититься от предиктивных атак на уровень приложения.
Стоимость и оптимизация газа
- Подписки позволяют агрегировать несколько потребителей и снижать комиссионные накладные.
- Запрашивайте несколько слов за раз (numWords) и используйте «смешивание» локально.
- Не храните все randomWords в состоянии, если их можно восстановить из одного VRF-слова и индекса.
Частые сценарии внедрения
- NFT-минт: VRF определяет редкость/атрибуты после фиксации участия, чтобы исключить «снайпинг».
- Игры: выпадение лута/крит-шансов; результат сочетается с внутренним сидом игрока.
- Лотереи/розыгрыши: выбор победителей/порядка, в т. ч. «батчами» через numWords.
- Аукционы/распределение слотов: случайный порядок очереди/тиражирования, нормализованный через перестановки.
Чек-лист безопасности для VRF-интеграции
- проверяйте в колбэке msg.sender == VRFCoordinator;
- связывайте requestId c контекстом (кто/что ожидает результат);
- проектируйте идемпотентность колбэка (повтор = *no-op*);
- держите статусы Pending/Executed/Failed, события и геттеры;
- закладывайте запас в callbackGasLimit и избегайте тяжёлых внешних вызовов;
- ограничивайте частоту/объём запросов и следите за балансом подписки;
- добавляйте «смешивание» сидом/nonce на уровне приложения;
- покрывайте тестами: повтор колбэка, out-of-order, недостаток газа, массовые запросы, тайм-ауты.
Анти-паттерны
- колбэк меняет критичное состояние до проверок отправителя/идентификатора;
- «монолитный» колбэк с большим количеством внешних вызовов;
- перезапись результатов без контроля requestId;
- ожидание «моментального» ответа без учёта P95/P99;
- использование VRF-слова напрямую для всех действий без дополнительно сидов/индексации.
FAQ
Можно ли предсказать результат до публикации? Нет, без доступа к приватному ключу оракула и возможности подделать доказательство. Результат становится известен только после ончейн-верификации.
Что, если оракул «завис» и не отвечает? Результат не будет принят без доказательства, но ваш протокол должен уметь повторно запросить случайность или предложить пользователю альтернативу (например, отмену участия) при длительном ожидании.
Можно ли сместить результат через газ/реорганизации? Риск смещения минимизируется: вы настраиваете requestConfirmations, Coordinator проверяет proof. Дисциплина в колбэке и выбор разумных подтверждений критичны.
Нужна ли дополнительная «соль» от приложения? Да. Это повышает энтропию на прикладном уровне и делает предикцию для конкретного пользователя практически невозможной.
Сколько слова случайности хранить в состоянии? Часто достаточно одного VRF-слова + детерминированного «смешивания» для множества исходов.
