Индексатор Ethereum: курсор индексатора и снимок блока
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 13 из 30.
После перезапуска индексатор должен продолжить с известного места и проверить, что это место всё ещё относится к выбранной цепочке. Для этого нужен checkpoint: номер блока, его хеш и контекст экземпляра. В этом уроке определим его схему и атомарное продвижение вместе с журналом событий. Результат — отказ между чтением и сохранением не создаёт незаметный пропуск истории.
Предполагаются ограниченные диапазоны предыдущего урока. База данных и индексатор не запускались; SQL является исходником будущего адаптера. Учебный профиль сети31337 использует отдельный environmentId, потому что локальную цепочку можно сбросить. Никакие реальные хеши в статье не объявляются полученными.
Курсор относится к завершённой работе
Checkpoint сообщает, что все нужные блоки до выбранного номера обработаны непрерывно. Он не является просто последним номером, который увидел HTTP-запрос. Если журналы порции прочитаны, но не сохранены, продвижение курсора уже опасно: после перезапуска процесс пропустит данные, которых в базе нет.
Поэтому сохранение нормализованных событий и продвижение checkpoint входят в одну транзакцию базы. При успешном commit обе части известны. При rollback ни одна не объявляется завершённой. Это локальная гарантия хранилища, которая не фиксирует сеть навсегда: позже блок может перестать быть каноническим и потребуется процедура реорганизации.
Курсор также сохраняется для пустой порции. История могла не содержать публикаций в этих блоках, однако чтение завершено. Номер последнего события не заменяет верхнюю границу обработанного диапазона. Именно поэтому schema checkpoint отделена от таблицы событий.
Схема записи
Приведём минимальную таблицу PostgreSQL. Адрес и environmentId нормализованы адаптером, числовые поля имеют достаточный диапазон. Это исходник для рассмотрения, а не выполненная миграция.
CREATE TABLE catalog_checkpoints (
environment_id text NOT NULL,
chain_id numeric(78,0) NOT NULL,
contract_address text NOT NULL,
block_number numeric(78,0) NOT NULL,
block_hash text NOT NULL,
policy_version text NOT NULL,
PRIMARY KEY (environment_id, chain_id, contract_address)
);
Значение blockHash проверяется как32 байта по принятому представлению; произвольная строка из клиента не используется. policy_version сообщает, какой уровень актуальности считался завершённым. Если позже меняется политика подтверждения, старый факт не переименовывается автоматически. Сервис знает, как интерпретировать соответствующую запись.
Таблица не делает sourceRPC честным и не проверяет происхождение байткода реестра. Эти задачи принадлежат конфигурации и источнику данных. База сохраняет конкретное наблюдение, поэтому его смысл зависит от известной сети, экземпляра и выбранного процесса чтения.
Атомарный шаг
Внутри транзакции адаптер записывает порцию событий по идемпотентным ключам и обновляет checkpoint условно относительно ожидаемого прежнего значения. Следующий урок разберёт ключи подробно. Условие защищает от двух работников, которые пытаются продвинуть один курсор независимо.
UPDATE catalog_checkpoints
SET block_number = $1, block_hash = $2
WHERE environment_id = $3
AND chain_id = $4
AND contract_address = $5
AND block_number = $6
AND block_hash = $7;
Если изменена одна строка, прежнее состояние совпало с ожидаемым. Если ноль, другой работник мог уже продвинуть курсор или произошло изменение конфигурации. Нельзя всё равно объявить порцию текущей и перезаписать чужой результат. Адаптер повторно читает checkpoint и принимает новое решение в согласованном процессе.
Для начальной записи используется отдельный insert с выбранным стартовым блоком и его происхождением. Не стоит представлять блок минус1 как unsigned-значение или путать «ещё ничего не обработано» с настоящим блоком0. У начального состояния может быть явный статус или отдельно заданный fromBlock. Схема выбирается так, чтобы этот переход был понятен.
Проверка после перезапуска
Сервер читает checkpoint и запрашивает заголовок его блока в текущем источнике. Если хеш совпадает, можно продолжить со следующего номера по текущей политике. Если не совпадает, сначала выполняется поиск общей точки и пересборка, а не обычное чтение дальше. Курсор привязан к конкретной ветви, а не только к числу.
Если RPC недоступен, сервер не считает прежний checkpoint неверным. Он просто не завершил сверку. API истории может показывать последнее известное состояние с отметкой отставания, а процесс загрузки ждёт восстановления. Такой отказ отличается от подтверждённой реорганизации и требует другого сообщения.
При новом environmentId старый курсор не используется. Он сохраняется как архив прежней лаборатории. Автоматическое удаление всех историй только потому, что один локальный узел сбросился, мешает расследованию; автоматическое смешение, напротив, создаёт ложную непрерывность. Контекст определяет самостоятельный набор записей.
Два работника
Для маленькой лаборатории достаточно одного процесса индексирования. При увеличении нагрузки можно использовать блокировку, lease или условный переход состояния. Но наличие двух работников не должно автоматически удваивать продвижение курсора. Каждый диапазон имеет ожидаемую начальную точку и итоговый блок, а commit проверяет непрерывность.
Если один worker прочитал порцию на прежней ветви, пока другой обнаружил reorg, его старый commit должен быть отвергнут поколением обработки или условием blockHash. Простое сравнение «мой номер больше» не даёт нужной гарантии: большая цифра может относиться к другой истории.
Для упражнения задайте отказ до commit, отказ после commit и несовпадение хеша при перезапуске. В первых двух случаях повторение диапазона должно быть безопасным по идемпотентной схеме; в последнем начинается восстановление ветви. Эти критерии не проверялись программой. Итог — checkpoint является проверяемым утверждением о завершённом префиксе. Транзакции описаны в PostgreSQL.
Checkpoint и версия алгоритма
Номер блока отвечает на вопрос о покрытии источника, но не описывает способ вычисления производных данных. Если новая версия проекции иначе трактует сохранённые наблюдения, прежний checkpoint нельзя объявить паспортом новой версии без пересчёта. Сохраняем идентификатор алгоритма и поколение рядом со срезом.
Например, первая версия истории сортировала записи неверно при нескольких логах одного блока. После исправления можно читать сохранённые события заново и построить другое поколение. Полученная проекция относится к тому же сетевому cutoff, но к новому алгоритму. Это не реорганизация блоков и не причина менять исходные наблюдения.
Для пользователя обновление может выглядеть как изменение порядка строк. API должен удерживать согласованный ответ целиком: один источник, одна версия и одна проекция. Нельзя взять номер блока из нового поколения, а карточки курса оставить из старого только ради быстрого переключения. Внутренняя атомарность требуется даже тогда, когда содержание многих записей случайно совпало.