События и логи: eth_getLogs, bloom-фильтры и ограничения публичных RPC
Как работают события контрактов и eth_getLogs, почему публичные узлы режут диапазон блоков и как строить историю сделок без собственной базы.
В этой статье
У смарт-контракта есть два способа сообщить миру, что произошло: изменить состояние (storage) и записать событие (event). Состояние показывает, «как сейчас». События рассказывают, «как к этому пришли». Для интерфейсов, показывающих историю сделок, именно события - основной источник данных.
Что такое событие
Событие - это запись в журнале транзакции. Контракт делает emit DisputeOpened(dealId, by, at), и в квитанции транзакции появляется лог: адрес контракта, набор топиков и данные. Структуру лога разобрали в статье про ABI: topic0 - идентификатор события, индексированные аргументы - следующие топики, остальное - data.
Важные свойства событий:
- Дёшево. Запись события в разы дешевле записи в storage.
- Недоступно контракту. Контракт не может прочитать свои или чужие события - они нужны только внешнему миру.
- Постоянно. Однажды записанный лог остаётся в истории цепочки.
- Привязано к транзакции. Откатилась транзакция - откатились и её события.
Читаем события: eth_getLogs
Интерфейс получает события от узла запросом eth_getLogs с фильтром:
{
"address": "0x6462DDDc0a3D893A7C17f94410e2d27162a390aD",
"fromBlock": "0x…",
"toBlock": "0x…",
"topics": [
["0x2241d0fd…", "0xd716fe62…"],
"0x0000…0007"
]
}Здесь topics[0] - список интересующих событий (допускается несколько вариантов), topics[1] - значение первого индексированного параметра, например номер сделки. Фильтр по адресу и топикам - это и есть причина, по которой индексируют параметры: так узел находит нужное быстро.
В ответ приходит список логов с номером блока, хешем транзакции, топиками и данными. Дальше интерфейс декодирует их по ABI и строит ленту: «создана», «принята», «открыт спор», «завершена».
Как узел ищет быстро: bloom-фильтры
Просматривать все блоки подряд было бы мучительно медленно. В заголовок каждого блока записывается bloom-фильтр - компактная структура, которая позволяет быстро ответить «в этом блоке точно нет событий этого контракта с этим топиком» или «возможно, есть». Узел отбрасывает пустые блоки и проверяет только вероятные. Фильтр допускает ложные срабатывания, но никогда не пропускает настоящее событие.
Практическая проблема: лимиты публичных узлов
Здесь начинается то, о чём редко пишут в учебниках. Публичные RPC-узлы бесплатны, поэтому защищаются от дорогих запросов. Запрос логов за миллионы блоков - именно такой. Из нашего опыта подключения четырёх сетей:
- Некоторые узлы ограничивают диапазон блоков за один запрос - от 2 000 до 50 000, зависит от сети и поставщика.
- Некоторые вообще не отдают логи (например, один из публичных узлов BNB Chain).
- Самое коварное: отдельные узлы при слишком широком диапазоне отвечают не ошибкой, а пустым списком. Интерфейс видит «событий нет» и показывает пустую историю, хотя сделки есть.
- Публичные узлы часто не хранят старое состояние: запросить «как было 100 000 блоков назад» нельзя.
Из этого следует несколько правил для разработчика.
Правило 1. Не доверяйте пустому ответу
Пустой список на широкий диапазон может означать «нет событий», а может - «узел отказался». Проверяйте на заведомо известном событии при настройке нового узла, а в коде не принимайте решение «событий нет» по одному запросу.
Правило 2. Двигайтесь окнами и адаптируйтесь
Если нужны последние события, не запрашивайте всю историю. Идите от свежих блоков к старым окнами, и если узел вернул ошибку лимита, уменьшайте окно вдвое. Остановитесь, когда нашли достаточно данных или упёрлись в блок развёртывания контракта. Вот схема такого обхода:
// от свежих блоков к старым, окно сжимается при ошибке
let to = latest;
let span = 50_000n;
while (to > deployBlock && found.length < needed && budget-- > 0) {
const from = to - span < deployBlock ? deployBlock : to - span;
try {
found.push(...(await getLogs({ fromBlock: from, toBlock: to })));
to = from - 1n;
} catch {
span /= 2n; // узел отказал - сузим окно
if (span < 1000n) break;
}
}Правило 3. Знайте блок развёртывания
Нет смысла просматривать блоки до момента, когда контракт появился. Блок развёртывания - параметр конфигурации, который ограничивает поиск.
Правило 4. Не заводите лишнего: счётчик часто лучше логов
Для списка «сделки пользователя» контракту удобно хранить счётчик dealCount. Пока сделок немного, их можно перебрать по номерам через пакетное чтение состояния (Multicall) - это не требует логов вообще. Логи подключаются, когда сделок слишком много для перебора. Мы используем именно такую гибридную схему: она работает даже на узлах, которые не отдают логи.
Когда логов недостаточно: индексаторы
Для больших объёмов и сложных запросов («все сделки пользователя за год, отсортированные по сумме») нужен индексатор - отдельный сервис, который один раз читает все события в собственную базу и отвечает на запросы быстро. Примеры: The Graph, Ponder, самописные решения. Он не доверяется слепо: данные в нём - копия цепочки, а источником истины остаётся блокчейн, и важные действия следует перепроверять чтением контракта напрямую.
Реорганизации цепочки
Недавние блоки могут быть переписаны (reorg): сеть изменила мнение о последних блоках, и ваша транзакция оказалась в другом блоке или в никаком. Поэтому интерфейсы не считают событие окончательным, пока не прошло достаточно подтверждений, а индексаторы умеют «откатывать» последние блоки.
Что почитать дальше
Теперь, когда понятно, как данные попадают из блокчейна в интерфейс, пора написать собственный контракт: ERC-20 с нуля. Дальше - как подписи работают в контрактах: ECDSA и EIP-712, и затем конечный автомат эскроу на Solidity.
Читайте также
- 2 · Практика
Как читать Etherscan: проверяем контракт, транзакцию и события
Практическое руководство по обозревателям блоков (Etherscan, Basescan, Polygonscan, BscScan): как проверить адрес, транзакцию, контракт, токены и разрешения.
3 мин чтения - 3 · Под капотом
ABI: как вызов функции превращается в байты - селекторы, calldata и события
Как кодируются вызовы смарт-контрактов: селектор функции, выравнивание аргументов, динамические типы, пользовательские ошибки и топики событий на реальных примерах.
4 мин чтения
Упоминается в статьях
- Словарь терминов: блокчейн, кошельки, смарт-контракты и эскроу от A до Я
- Жизненный цикл сделки: от оферты до выплаты шаг за шагом
- Как читать Etherscan: проверяем контракт, транзакцию и события
- Как проверить смарт-контракт перед использованием: пошаговая инструкция
- Обзор языка Solidity: типы, функции, ошибки и события на примерах
- ABI: как вызов функции превращается в байты - селекторы, calldata и события
- ERC-20 с нуля: пишем токен на Solidity и разбираем каждую строку
- Пишем эскроу на Solidity: конечный автомат и разбор реального контракта
- Раскладка хранилища и упаковка переменных в Solidity
- Оптимизация газа в Solidity: 12 приёмов и когда они уместны
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

