Разрешение форков
На этой странице описывается, как 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. Когда родитель наконец прибывает:
_push_block(parent)связывает родителя._push_next(parent)итерирует_unlinked_indexв поиске дочерних блоков родителя.- Дочерние блоки перемещаются в
_indexи рекурсивно связываются. - Голова fork DB может прыгнуть на несколько блоков за один вызов.
Порог разрыва (100 блоков) предотвращает разбухание памяти: блоки более чем на 100 опережающие голову базы данных с неизвестным родителем тихо отклоняются до попадания в fork DB.
Выбор форка: правило самой длинной цепочки
После вставки блока fork DB возвращает новую голову. Если новая голова выше головы базы данных и расходится с ней, предпринимается переключение форка.
До HF12: Простое правило самой длинной цепочки — побеждает форк с наибольшим номером блока.
HF12: Сравнение форков с весом голосов
Начиная с харфорка 12, при двух конкурирующих форках на одной высоте блока вместо простого правила длинной цепочки используется compare_fork_branches():
Алгоритм
- Получить ветви через
fetch_branch_from(fork_a_tip, fork_b_tip)до общего предка. - Суммировать вес голосов по валидатору для каждой ветви — только уникальные аккаунты валидаторов учитываются один раз на ветвь. Аккаунт экстренного валидатора (
"committee") исключается. - Применить бонус +10% к более длинной цепочке.
- Вернуть:
+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 элемент в synopsis | false — пир обновлён |
Ответ >1 элемента, remaining == 0 | false — пир почти обновлён (переключиться на инвентарь) |
Ответ >1 элемента, remaining > 0 | true — пир далеко позади (оставаться в режиме синхронизации) |
Процесс переключения форка
Когда узел переключается на лучший форк:
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):
- Собирает
last_supported_block_numдля каждого из 21 запланированных валидаторов. - Сортирует и берёт
⌈21 × 25%⌉ = 5-й снизу (т.е. значение, на уровне которого или выше находятся 75% валидаторов). - Полученный номер блока становится новым LIB.
После того как блок становится LIB, он записывается в block_log (или dlt_block_log в DLT-режиме), а его сессия отмены фиксируется.
LIB ограничен значением HEAD − 1 в режиме экстренного консенсуса для предотвращения фиксации сессии отмены применяемого в данный момент блока.
Обрезка устаревших форков
Два механизма предотвращают накопление устаревших данных:
remove_blocks_by_number(num)— удаляет все блоки на конкретной высоте. Вызывается решателем коллизий форков после таймаута 21 откладывания.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_count | uint32_t | Накопленное количество коллизий форков с genesis |
last_fork_collision_block_num | uint32_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, Обработка блоков, Экстренный консенсус.