Индексатор Ethereum: поиск общей точки при реорганизации
Оглавление курса «Web3: вход, смарт-аккаунты и индексатор» · Урок 16 из 30.
Реорганизация меняет выбранную ветвь цепочки. Номер блока может остаться тем же, а его хеш и события — стать другими. В этом уроке определим, как индексатор обнаруживает несовпадение и ищет сохранённую общую точку. Результат — процесс не продолжает старый checkpoint как будто ничего не произошло.
Предполагаются журнал, проекция и checkpoint с blockHash. Узел и индексатор не запускались; сценарии ветвей являются собственными заданными данными. Учебная лаборатория не даёт универсальных production-параметров глубины, финальности или числа подтверждений. Эти правила выбираются по конкретной сети и требованиям приложения.
Сравнение хеша
После перезапуска или перед новой порцией индексатор читает блок номера checkpoint из текущего источника. Если его хеш совпадает, сохранённая точка пока относится к выбранной ветви. Если отличается, прежняя история после некоторого места может быть неканоничной. Продвигать курсор дальше без восстановления нельзя.
Отказ RPC не является доказательством реорганизации. Сервис мог просто не ответить. В таком случае процесс сохраняет последнее известное состояние и сообщает о невозможности сверки. Только полученный несовпадающий хеш создаёт факт смены наблюдаемого блока. Эта разница предотвращает ненужные массовые откаты при кратком сбое endpoint.
Высота head может оказаться меньше сохранённого checkpoint, например после сброса локального окружения или смены источника. Тогда конфигурация и environmentId проверяются прежде обычной процедуры reorg. Нельзя считать любой новый пустой узел продолжением прежней сети только по chainId31337.
Сохранённые опорные точки
Одного последнего хеша недостаточно для поиска общей истории. Сохраняем предыдущие anchors: номер и хеш завершённой порции. Это расширение схемы предыдущего урока, где checkpoint описывал только текущий уровень. История anchors появляется при каждом успешном атомарном commit порции.
CREATE TABLE catalog_anchors (
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,
PRIMARY KEY (environment_id, chain_id, contract_address, block_number, block_hash)
);
Опорные точки могут быть редкими, например на верхних границах порций. Тогда найденная общая точка будет раньше точного разветвления, и потребуется перечитать больший диапазон. Это допустимый компромисс: мы не обязаны знать каждый заголовок, если готовы повторить безопасный объём работы. Для точного поиска сохраняют более подробную цепочку заголовков по отдельной политике.
Таблица здесь является исходником будущей миграции, не созданной базой. Политика хранения anchors должна оставлять достаточный объём для допустимого восстановления. Если удалены все старые точки, процесс не может выдумать общий блок по памяти приложения. Он требует начальную пересборку или другой подтверждённый источник.
Поиск общей точки
Начинаем с последнего anchor и идём назад по сохранённому порядку. Для каждого читаем текущий блок этого номера и сравниваем хеш. Первый совпавший anchor задаёт безопасную общую точку для дальнейшего восстановления. Это может быть более ранний блок, чем фактический fork, но его происхождение известно.
export async function findCommonAnchor(client, anchors, limit = 20) {
for (const anchor of anchors.slice(0, limit)) {
const block = await client.getBlock({ blockNumber: BigInt(anchor.blockNumber) });
if (block.hash === anchor.blockHash) return anchor;
}
return null;
}
Входной список уже отсортирован от новых точек к старым и относится к одному окружению. Число20 является ограничением нашей демонстрационной попытки, не гарантией достаточной глубины. Ошибка чтения прерывает поиск с отдельной категорией, а не превращается в несовпадение. Нельзя пропускать недоступные точки и заявлять проверенную общность ниже без объяснения.
Если совпадения нет в доступном окне, процесс останавливает продвижение. Восстановление может начать с известного блока развёртывания, если его происхождение подтверждено, или потребовать новую конфигурацию. Сервис не обещает автоматически обработать любую глубину при ограниченном архиве.
Что изменится после поиска
События выше общей точки старой ветви перестают быть каноничными. Проекция откатывается или пересобирается из каноничного префикса, checkpoint возвращается к anchor. Затем новые диапазоны читаются заново с текущего источника. Это согласованный переход поколения обработки, а не несколько независимых присваиваний в таблицах.
Параллельный worker, который ещё читает старую порцию, должен потерять право commit после смены поколения. Иначе он снова добавит старые строки как текущие и продвинет курсор поверх восстановленной истории. Условный переход checkpoint и идентификатор обработки позволяют отвергнуть такой поздний результат.
Видимое API истории показывает уровень восстановления и последнее завершённое наблюдение. Оно не выдаёт временную неполную проекцию как свежий каталог. Читатель может продолжать пользоваться статическими материалами, а публичная история сообщает о задержке. Связь с личным кабинетом остаётся отдельной.
Подтверждения уменьшают риск, но не меняют алгоритм
Чтение более устойчивого уровня по политике сети снижает частоту некоторых изменений, однако не отменяет необходимость проверить происхождение. Не следует считать число подтверждений универсальной математической гарантией любой сети. Для L2 появятся дополнительные этапы, которые рассматриваются в отдельном курсе.
В локальной лаборатории процесс полезен как модель: можно задать две ветви и рассчитать ожидаемый откат без запуска узла. В этой работе такого сетевого эксперимента не проводилось. Схемы ветвей и результаты служат критериями будущего самостоятельного наблюдения.
Для упражнения возьмите anchors100A,150A,200A и текущую ветвь с совпадением100A, но другими150B и200B. Общая известная точка —100A; повторное чтение начнётся после неё, даже если фактическое разветвление было позже. Итог — индексатор выбирает проверяемый префикс и сохраняет честные границы восстановления. Блоки описаны в Ethereum JSON-RPC.
Глубина поиска и честный предел
Ограниченный список anchors ускоряет обычное восстановление, но устанавливает явную границу. Если общий блок расположен раньше самой старой сохранённой точки, алгоритм не получает права считать её общей по умолчанию. Он сообщает, что текущего окна недостаточно, и требует более широкий источник или повторное построение от поддерживаемой границы.
Такой результат не является доказательством атаки и не даёт оценки безопасности всей сети. Это свойство нашего хранения. Для оператора полезно записать число доступных точек, их диапазон и причину прекращения поиска. Пользовательский API показывает recovering с прежним срезом либо временно ограничивает историю по выбранной политике.
Во время поиска новый head может двигаться. Поэтому сначала фиксируем целевой контекст восстановления и затем проверяем его перед переключением поколения. Если ветвь изменилась снова, повторяем решение в новом контексте. Не смешиваем anchors из нескольких несвязанных проходов в одну воображаемую последовательность.