Chainlink VRF: проверяемая случайность для смарт-контрактов (как это работает и как внедрять без уязвимостей)

Chainlink VRF (Verifiable Random Function) — это сервис криптографически проверяемой случайности для смарт-контрактов. Контракт-потребитель запрашивает случайное значение, оракул генерирует рандом + доказательство корректности, а Coordinator проверяет это доказательство ончейн и только затем передаёт число в колбэк потребителя. Так достигаются три свойства, важных для Web3-приложений:

  • Непредсказуемость до момента публикации результата.
  • Невозможность смещения результата исполнителем.
  • Проверяемость на уровне смарт-контракта: потребителю не нужно «доверять» поставщику, он проверяет криптодоказательство.

Chainlink VRF: проверяемая случайность для смарт-контрактов

Типичные кейсы: честная генерация NFT-атрибутов, распределение эирдроп-слотов/вайтлистов, лотереи и розыгрыши, случайные дропы в играх, распределение матчей/пар и любые сценарии, где нельзя полагаться на blockhash/timestamp.

  • blockhash и timestamp манипулируемы валидаторами/майнёрами в пределах окна; достаточно сдвинуть/отклонить блок.
  • Псевдо-RNG из ончейн-состояния детерминирован и предсказуем наблюдателю.
  • В commit-reveal схемах без внешнего источника часто остаётся окно смещения (последний раскрывающий может «выбрать» значение молчанием).

VRF убирает эти классы атак, потому что доказательство связывает случайность с запросом и публичным ключом провайдера — изменить число без провала верификации нельзя.

Архитектура и роли

  • Consumer — ваш смарт-контракт, который запрашивает случайность и получает её в колбэке.
  • VRF Coordinator — ончейн-контракт Chainlink, который принимает запросы, валидирует доказательства и вызывает ваш колбэк.
  • Oracle/Prover — оффчейн-узел, генерирующий случайность и криптодоказательство корректности.
  • Плательщик — источник оплаты за запрос (в версиях VRF это либо прямая оплата, либо subscription с балансом).

Высокоуровневый поток:

  1. Consumer вызывает requestRandomWords(…).
  2. Coordinator регистрирует запрос и отдаёт его оракулу.
  3. Оракул генерирует (randomWords, proof) и отправляет в Coordinator.
  4. Coordinator проверяет proof ончейн и вызывает у Consumer fulfillRandomWords(requestId, randomWords).

Именно онチェйн-верификация делает результат проверяемым и «недоверенным» к провайдеру.

Параметры запроса: что действительно важно спроектировать

  • requestConfirmations — сколько подтверждений ожидать перед обработкой запроса (уменьшает риск reorg). Выше подтверждения → дольше, но безопаснее.
  • callbackGasLimit — газовый лимит для fulfillRandomWords. Должен покрыть всю логику потребителя; если не хватит — колбэк упадёт.
  • numWords — сколько случайных 256-битных слов нужно сразу (экономит накладные расходы).
  • keyHash/«gas lane» — выбор ключа/линии обслуживания провайдеров (на разных сетях доступны разные конфигурации).
  • Модель оплаты — в актуальных конфигурациях применяется подписка (subscription) c пополнением; это дешевле и удобнее «прямых платежей».

Советы:

  1. закладывайте запас на callbackGasLimit и избегайте тяжёлой логики в колбэке;
  2. подбирайте requestConfirmations под риск-профиль (лотерея на крупные суммы ≠ обычный дроп).

Модель безопасности: что гарантирует VRF

  • Связность результата с запросом. Доказательство криптографически привязывает randomWords к конкретному requestId и публичному ключу оракула.
  • Ончейн-верификация. Coordinator отклонит любой результат, не соответствующий доказательству.
  • Нуль-доверия к исполнителю. Даже «злой» оракул не сможет подменить число; максимум — не ответить (см. эксплуатационные риски ниже).

Что не гарантирует:

  • моментальное время ответа в условиях сетевых перегрузок;
  • исполнение вашего колбэка, если вы занижаете callbackGasLimit или нарушаете инварианты (реентранси, проверки и т. п.).

Паттерны для Consumer-контрактов

  • Идемпотентность. Свяжите requestId → «заказчик/слот» и не позволяйте повторно «получить приз». См. Идемпотентность.
  • Двухфазная логика.
    1. Фаза 1: при requestRandomWords записать намерение (кто/что ожидает случайность).
    2. Фаза 2: в fulfillRandomWords применить результат. При фейле оставить состояние в безопасном виде.
  • Хранилище статусов. Держите Pending/Executed/Failed; не переводите в Executed, пока колбэк не завершён.
  • Проверки входа. Колбэк должен принимать вызовы только от Coordinator; защитите публичные методы.
  • События для фронта. Помогают пользователю отслеживать статус (Requested, Fulfilled, Failed).
  • Газ-безопасность. Никаких тяжёлых циклов/внешних вызовов внутри колбэка; лучше планировать «пост-обработку» отдельным вызовом.

Анти-паттерны:

  1. использование randomWords для всего без «смешивания» с пер-пользовательским сидом → облегчает предсказание. Всегда хешируйте: keccak256(abi.encode(randomWord, userSeed, nonce)).
  2. сдвоенные действия «минт + перевод» внутри колбэка без лимитов/пайплайна → риски газа и реентранси.

Проектирование сидов и «смешивание» случайности

Даже с проверяемым 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-интеграции

  1. проверяйте в колбэке msg.sender == VRFCoordinator;
  2. связывайте requestId c контекстом (кто/что ожидает результат);
  3. проектируйте идемпотентность колбэка (повтор = *no-op*);
  4. держите статусы Pending/Executed/Failed, события и геттеры;
  5. закладывайте запас в callbackGasLimit и избегайте тяжёлых внешних вызовов;
  6. ограничивайте частоту/объём запросов и следите за балансом подписки;
  7. добавляйте «смешивание» сидом/nonce на уровне приложения;
  8. покрывайте тестами: повтор колбэка, out-of-order, недостаток газа, массовые запросы, тайм-ауты.

Анти-паттерны

  • колбэк меняет критичное состояние до проверок отправителя/идентификатора;
  • «монолитный» колбэк с большим количеством внешних вызовов;
  • перезапись результатов без контроля requestId;
  • ожидание «моментального» ответа без учёта P95/P99;
  • использование VRF-слова напрямую для всех действий без дополнительно сидов/индексации.

FAQ

Можно ли предсказать результат до публикации? Нет, без доступа к приватному ключу оракула и возможности подделать доказательство. Результат становится известен только после ончейн-верификации.

Что, если оракул «завис» и не отвечает? Результат не будет принят без доказательства, но ваш протокол должен уметь повторно запросить случайность или предложить пользователю альтернативу (например, отмену участия) при длительном ожидании.

Можно ли сместить результат через газ/реорганизации? Риск смещения минимизируется: вы настраиваете requestConfirmations, Coordinator проверяет proof. Дисциплина в колбэке и выбор разумных подтверждений критичны.

Нужна ли дополнительная «соль» от приложения? Да. Это повышает энтропию на прикладном уровне и делает предикцию для конкретного пользователя практически невозможной.

Сколько слова случайности хранить в состоянии? Часто достаточно одного VRF-слова + детерминированного «смешивания» для множества исходов.

См. также

Task Runner