Skip to content

Разрешение форков

На этой странице описывается, как VIZ Ledger обнаруживает, выбирает и разрешает конкурирующие форки — от базовой базы данных форков до решателя коллизий HF12 с весом голосов.


База данных форков

База данных форков (fork_database) — это хранящееся в памяти дерево кандидатных вершин цепочки. Каждый блок, полученный по P2P, вставляется сюда до применения к состоянию цепочки.

Ключевые операции:

  • push_block(b) — связать b с его родителем; если родитель неизвестен, кешировать в _unlinked_index
  • _push_next(item) — когда родитель прибывает, итеративно связывать все кешированные дочерние блоки
  • fetch_branch_from(a, b) — пройти обе ветви до общего предка
  • set_max_size(n) — обрезать старые блоки из обоих linked и unlinked индексов

Обнаружение дубликатов

Перед вставкой fork DB проверяет, существует ли уже блок с тем же ID. Если да — тихо игнорируется. Это предотвращает избыточную обработку блоков, повторно транслируемых при P2P-синхронизации.

Unlinked index

Блоки, родитель которых ещё не в fork DB, хранятся в _unlinked_index. Когда родитель наконец прибывает:

  1. _push_block(parent) связывает родителя.
  2. _push_next(parent) итерирует _unlinked_index в поиске дочерних блоков родителя.
  3. Дочерние блоки перемещаются в _index и рекурсивно связываются.
  4. Голова fork DB может прыгнуть на несколько блоков за один вызов.

Порог разрыва (100 блоков) предотвращает разбухание памяти: блоки более чем на 100 опережающие голову базы данных с неизвестным родителем тихо отклоняются до попадания в fork DB.


Выбор форка: правило самой длинной цепочки

После вставки блока fork DB возвращает новую голову. Если новая голова выше головы базы данных и расходится с ней, предпринимается переключение форка.

До HF12: Простое правило самой длинной цепочки — побеждает форк с наибольшим номером блока.


HF12: Сравнение форков с весом голосов

Начиная с харфорка 12, при двух конкурирующих форках на одной высоте блока вместо простого правила длинной цепочки используется compare_fork_branches():

Алгоритм

  1. Получить ветви через fetch_branch_from(fork_a_tip, fork_b_tip) до общего предка.
  2. Суммировать вес голосов по валидатору для каждой ветви — только уникальные аккаунты валидаторов учитываются один раз на ветвь. Аккаунт экстренного валидатора ("committee") исключается.
  3. Применить бонус +10% к более длинной цепочке.
  4. Вернуть: +1 если ветвь A сильнее, -1 если B сильнее, 0 если равенство.

Обработка коллизий форков

Когда compare_fork_branches() вызывается из плагина validator:

  • Если один форк явно сильнее → производить на этом форке.
  • Если равенство или неопределённость → отложить (инкрементировать fork_collision_defer_count_).
  • После 21 последовательного откладывания (один полный раунд валидаторов) → таймаут: вызвать remove_blocks_by_number(height) для очистки устаревших конкурирующих блоков, затем производить на канонической цепочке.
УсловиеФлаг peer_needs_sync_items_from_us
Ответ пустойfalse — наша цепочка пуста
Ответ = 1 элемент в synopsisfalse — пир обновлён
Ответ >1 элемента, remaining == 0false — пир почти обновлён (переключиться на инвентарь)
Ответ >1 элемента, remaining > 0true — пир далеко позади (оставаться в режиме синхронизации)

Процесс переключения форка

Когда узел переключается на лучший форк:

1. fetch_branch_from(new_head, current_head)
   → branches.first  = [new_tip, ..., common_ancestor]
   → branches.second = [current_tip, ..., common_ancestor]

2. Проверка линейного продолжения:
   branches.second.size() == 1 И common_ancestor == head
   → Пропустить цикл извлечения; применить branches.first напрямую.

3. Реальное переключение форка:
   для каждого блока в branches.second (в обратном порядке):
       FORK-SWITCH-POP: pop_block()      ← сохранить txs в _popped_tx
   для каждого блока в branches.first (в обратном порядке):
       FORK-SWITCH-APPLY: apply_block()

4. При исключении:
   для каждого применённого выше блока:
       FORK-RECOVER-POP: pop_block()     ← отменить частичное применение
   Инвалидировать неудавшийся форк.
   Повторно бросить исключение.

Различие линейного продолжения критично в DLT-режиме, где LIB == head: цикл извлечения был бы бесконечным, поскольку сессии отмены уже зафиксированы.


Определение необратимого блока

После каждого применения блока update_last_irreversible_block() продвигает Last Irreversible Block (LIB):

  1. Собирает last_supported_block_num для каждого из 21 запланированных валидаторов.
  2. Сортирует и берёт ⌈21 × 25%⌉ = 5-й снизу (т.е. значение, на уровне которого или выше находятся 75% валидаторов).
  3. Полученный номер блока становится новым LIB.

После того как блок становится LIB, он записывается в block_log (или dlt_block_log в DLT-режиме), а его сессия отмены фиксируется.

LIB ограничен значением HEAD − 1 в режиме экстренного консенсуса для предотвращения фиксации сессии отмены применяемого в данный момент блока.


Обрезка устаревших форков

Два механизма предотвращают накопление устаревших данных:

  1. remove_blocks_by_number(num) — удаляет все блоки на конкретной высоте. Вызывается решателем коллизий форков после таймаута 21 откладывания.
  2. set_max_size(n) — обрезает старые блоки из _index и _unlinked_index, когда fork DB превышает n записей.

Защита от minority fork

Перед каждым производством блока плагин validator проверяет последние 21 блок в fork DB:

  • Если все 21 были произведены собственными настроенными валидаторами этого узла → узел изолирован на minority fork.
  • Действие (enable-stale-production = false): вызвать resync_from_lib() — извлечь до LIB, сбросить fork DB, переинициировать P2P-синхронизацию, переподключить сид-узлы.
  • Действие (enable-stale-production = true): зафиксировать предупреждение, продолжить производство.
  • Активен экстренный консенсус → пропустить проверку (ожидается, что все слоты будут "нашими" для экстренного мастера).

Метрики коллизий форков (HF12)

HF12 добавил два поля в dynamic_global_property_object для on-chain мониторинга:

ПолеТипОписание
fork_collision_countuint32_tНакопленное количество коллизий форков с genesis
last_fork_collision_block_numuint32_tНомер блока последней коллизии

Читается через get_dynamic_global_properties.


Диагностика fork DB

Fork DB предоставляет O(1) аксессоры для мониторинга:

МетодВозвращает
linked_size()Количество блоков в linked index
unlinked_size()Количество блоков в unlinked index
linked_min_block_num()Наименьший номер блока в linked index
linked_max_block_num()Наибольший номер блока в linked index
unlinked_min_block_num()Наименьший номер блока в unlinked index
unlinked_max_block_num()Наибольший номер блока в unlinked index

Задача P2P-статистики записывает их каждые 5 минут:

Block storage | dlt_log: [79174319..79274318] | dlt_resizes: 412 | fork_db: linked=18 unlinked=0

Растущий unlinked_size, который не уменьшается, указывает на постоянный разрыв в получаемом потоке блоков (проблема P2P-подключения или узел на изолированном форке).


Устранение неполадок

СимптомДиагноз
Результат производства fork_collisionКонкурирующий блок на целевой высоте; дождитесь таймаута 21 откладывания или разрешения по весу голосов
Результат производства minority_forkУзел изолирован; проверьте P2P-пиры и подключение к сидам
unlinked_size растёт неограниченноРодительские блоки не поступают; проверьте P2P-подключение
Повторяющиеся переключения форков в логахСетевой раздел между двумя подмножествами валидаторов; изучите подключение между ними
Голова не продвигается в DLT-режимеПутаница с линейным продолжением vs. переключением форка; проверьте логи FORK-SWITCH-POP

См. также: Fair-DPOS, Обработка блоков, Экстренный консенсус.