Сценарии синхронизации P2P
Эта страница описывает, как уровень DLT P2P обрабатывает типичные ситуации синхронизации: первый запуск, догон после простоя, разрывы DLT-диапазона, восстановление после форка и экстренный консенсус.
Классификация узлов
В сценариях ниже используется эталонная конфигурация из 4 узлов:
| Роль | Описание |
|---|---|
| Master | Режим FORWARD; DLT block log [A..B]; держит экстренный приватный ключ |
| Slave (NEAR) | Head на A-1 (точно примыкает к DLT-диапазону мастера) |
| Slave (FAR) | Head значительно ниже A (не в DLT-диапазоне мастера) |
| Fresh node | Нет блоков; только состояние genesis |
Сценарий 1: NEAR Slave (head примыкает к DLT-диапазону мастера)
Настройка: DLT-диапазон мастера [1000-2000]. Head слейва = 999.
Рукопожатие hello
- Слейв отправляет hello:
head_num=999, head_id=H999. - Проверка
check_fork_alignmentмастера — многоуровневая проверка:head_num=999нижеdlt_earliest=1000— не в диапазоне.head_num + 1 == dlt_earliest (1000)→ проверка граничной ссылки: читает блок 1000, проверяетblock_1000.previous == H999.- Совпадение →
fork_alignment = true,exchange_enabled = true.
- Мастер отвечает:
exchange_enabled=true, fork_alignment=true. - Слейв переходит в состояние жизненного цикла SYNCING на мастере.
Синхронизация блоков
Слейв запрашивает dlt_get_block_range(start=1000, end=1199, prev=H999). Мастер отвечает блоками 1000–1199 из своего DLT-журнала. Слейв применяет каждый блок. Это повторяется батчами по 200 блоков, пока слейв не достигнет блока 2000 и is_last=true не вызовет transition_to_forward().
Результат: Чистая P2P-синхронизация без загрузки снимка. Без штрафных задержек.
Сценарий 2: FAR Slave (head намного ниже DLT-диапазона мастера)
Настройка: DLT-диапазон мастера [1000-2000]. Head слейва = 800.
Рукопожатие hello
- Слейв отправляет hello:
head_num=800, head_id=H800. - Проверка выравнивания форка мастера:
800 < 1000, проверка граничной ссылки не проходит (800 + 1 ≠ 1000), резервный вариант по LIB тоже не проходит (LIB ID обрезан). fork_alignment = false, ноexchange_enabled = false.- Мастер не отключает слейв, так как
hello.node_status == SYNC— SYNC-пиры всегда переходят в состояние жизненного цикла ACTIVE.
Попытка синхронизации
Слейв переходит в состояние жизненного цикла ACTIVE на мастере. Поскольку exchange_enabled = false, мастер не отправляет forward-блоки. Слейв пытается запросить диапазон блоков: request_blocks_from_peer обнаруживает our_head+1 (801) < peer_dlt_earliest (1000) — разрыв обнаружен.
Узел ищет среди всех подключённых пиров того, чей DLT-диапазон покрывает блок 801. Если найден, этот пир используется как источник синхронизации-мостика. Если ни один пир не может перекрыть разрыв:
[P2P] Gap detected: our_head=800, nearest_peer_dlt_earliest=1000
No peer can serve blocks 801-999. Snapshot import may be required.Примерно через 90 секунд без прогресса head детектор застывшей синхронизации плагина snapshot срабатывает и инициирует загрузку снимка с доверенных пиров (настраивается через trusted-peer-for-snapshot). После импорта снимка на блоке 1500 слейв снова входит в режим SYNC и нормально догоняет сеть.
Сценарий 3: Fresh Node (нет блоков)
Настройка: У узла нет блоков; head_num=0, head_id=zero_id.
Рукопожатие hello
- Новый узел отправляет hello:
head_num=0. - Проверка форка мастера:
head_num == 0→ пустой пир →fork_alignment = true(обрабатывается как "новый узел, ещё не на каком-либо форке"). exchange_enabled = true(мастер будет принимать блоки от этого узла).- Новый узел переходит в состояние жизненного цикла ACTIVE на мастере.
Попытка синхронизации
В request_blocks_from_peer, our_head=0 и peer_dlt_latest=2000. Однако peer_dlt_earliest=1000, поэтому самый ранний доступный блок — 1000. Запрос начинается с max(our_head+1, peer_dlt_earliest) = 1000. Узел получает блоки от 1000, но не может применить их, так как база данных цепочки не имеет состояния до блока 1000.
Плагин snapshot обнаруживает застой и загружает снимок (например, на блоке 1500). После импорта новый узел нормально догоняет от блока 1500 → 2000.
Сценарий 4: Перезапуск после сбоя
Настройка: Узел был на head 1912, DLT-диапазон [1750-1912]. После перезапуска пиры на блоке 2000.
Восстановление при запуске
database::open()проверяет консистентность DLT block log: если head журнала совпадает с head базы данных → консистентен; иначе сбрасывает журнал.- Последние 100 блоков из DLT block log засеваются в
fork_db(блоки 1813–1912). Это даёт вновь поступающим блокам окно родителя в 100 блоков без необходимости их предварительной загрузки. - Применяется 60-секундный льготный период: в первые 60 секунд после запуска блоки в пределах 10 от head обрабатываются как
FORK_DB_ONLYвместоDEAD_FORK. Это предотвращает "каскад отклонений", когда пиры воспроизводят блоки вблизи head, о которых только что заполненный fork_db ещё не знает.
Догон
Узел снова входит в режим SYNC и запрашивает блоки начиная с 1913. Пиры с DLT-диапазоном [1800-2000] могут обслуживать все нужные блоки. Узел догоняет до 2000 и переходит в FORWARD.
Сценарий 5: Переключение форка
Настройка: Узел на head H на форке A. Пир имеет head форка B H', где H' > H и форк B имеет больший вес голосов.
Обнаружение форка
- Блок с форка B поступает через трансляцию. Fork DB привязывает его к родительской цепочке.
track_fork_state()вызывается после каждого блока. Когда форк B удерживает преимущество на протяжении 42 блоков (2 полных раунда валидаторов), запускаетсяresolve_fork().resolve_fork()вычисляет суммарный вес голосов (делегированные SHARES) валидаторов на каждой ветви. Форк B должен удерживать 6 последовательных подтверждающих блоков до фиксации переключения.
Выполнение переключения форка
pop_block()удаляет блоки форка A обратно до общего предка. Выброшенные транзакции попадают в_popped_tx.- Блоки форка B применяются от общего предка до нового head.
_popped_txи_pending_txповторно применяются; транзакции, уже в цепочке форка B, молча пропускаются.
Статус форка в статистике: переходы NORMAL → LOOKING → NORMAL (или MINORITY, если этот узел на проигрывающей ветви).
Сценарий 6: Синхронизация при экстренном консенсусе
Настройка: Сеть застала более 3600 секунд. Экстренный консенсус активен.
Работа мастера
Экстренный мастер (узел с emergency-private-key в конфиге) производит все 21 блок в раунде, используя ключ подписи "committee". В статистике: +emrg +ekey.
Синхронизация слейва при экстренном режиме
- Слейв подключается к мастеру. Hello мастера включает
emergency_active=true, has_emergency_key=true. - Проверка выравнивания форка слейва выполняется нормально — блоки committee являются обычными подписанными блоками с точки зрения P2P-уровня.
- Слейв входит в режим SYNC и запрашивает блоки, произведённые committee, от мастера.
- Валидация блоков:
verify_signing_witness()смягчает проверку соответствия производитель-слот во время экстренного режима — если производитель блока не совпадает с точным запланированным слотом, он принимается, пока подпись проходит проверку противsigning_keyпроизводителя.
Восстановление ключа валидатора
Когда реальные валидаторы восстанавливают свои ключи подписи (через validator_update_operation), перестройка расписания включает их в гибридное расписание. Как только 15 из 21 слота валидаторов стали реальными (не committee), экстренный режим деактивируется. Последующие блоки производятся реальными валидаторами и синхронизируются нормально.
Сценарий 7: Восстановление застоя синхронизации
Условие: Режим SYNC, блоки не получены в течение 30 секунд.
- Срабатывает
sync_stagnation_check(): попытка 1 из 3 — повторный запрос блоков от всех активных пиров с включённым обменом. - Через 30 секунд: попытка 2 из 3.
- Через 30 секунд: попытка 3 из 3.
- После третьей попытки:
transition_to_forward()с предупреждением о застое.
Если узел всё ещё отставал при переходе в FORWARD, check_forward_stagnation() обнаружит отсутствие прогресса head через 30 секунд и вернётся в режим SYNC, начиная новый цикл.
Сценарий 8: Заполнение пробелов (Gap Fill)
Условие: Режим FORWARD; 1–100 блоков отсутствуют в потоке блоков.
Gap fill запускается автоматически при:
- Получении блока не по порядку (блок N+2 поступает до N+1).
periodic_task()обнаруживаетhighest_seen_block_num > our_head + 1.- Вызов
resume_block_processing()после паузы снимка.
Протокол:
- Выбрать пир с наибольшим
peer_head_numсреди активных пиров. - Отправить
dlt_gap_fill_request(block_nums=[N+1, N+2, ...])(макс. 100 блоков). - Ждать ответа до 15 секунд.
- При получении применить вернувшиеся блоки. Если блоки всё ещё отсутствуют, запустить ещё один gap fill на следующем периодическом цикле.
Если ни один пир не может обслужить пробел (нет exchange-enabled или SYNCING-пира с более высоким head), узел немедленно переходит в режим SYNC.
Сценарий 9: Предотвращение осцилляции SYNC ↔ FORWARD
Корневая причина осцилляции: После перехода FORWARD→SYNC таймер застоя синхронизации наследует устаревшую временную метку, немедленно срабатывает, и check_sync_catchup видит ноль пиров впереди → переходит обратно в FORWARD. Цикл продолжается.
Применённые исправления:
transition_to_sync()сбрасывает_last_block_received_timeдоnow, чтобы таймеры застоя начинали заново.check_forward_stagnation()НЕ переходит в SYNC, когда все подключённые пиры имеют тот же head, что и наш узел — нет смысла синхронизироваться, когда никто не впереди.check_sync_catchup()НЕ заявляет "догнал", когда нет активных пиров; вместо этого запускается 60-секундный таймер изоляции.- После 60 секунд изоляции
emergency_peer_reset()очищает все мягкие баны и задержки, принуждая к немедленному переподключению ко всем известным пирам.
Сценарий 10: Блоки dead fork
Условие: Пир отправляет блоки из цепочки, которая разошлась до окна fork DB узла. push_block() выбрасывает unlinkable_block_exception, и номер блока ≤ head_block_num.
Поведение:
dlt_delegate::accept_block()возвращаетDEAD_FORK.- Блок НЕ сохраняется в
fork_db._unlinked_index(предотвращает рост памяти). - Пир накапливает spam-удар за каждый dead-fork блок.
- После 10 ударов пир получает мягкий бан на 3600 с.
- Цикл синхронизации прерывается — дальнейшие блоки от этого пира не обрабатываются в текущем батче.
Льготный период (исправление P22): В первые 60 секунд после запуска узла блоки в пределах 10 от текущего head, дающие сбой с unlinkable_block_exception, возвращаются как FORK_DB_ONLY (не DEAD_FORK). Это предотвращает ложную блокировку легитимных пиров, отправляющих блоки вблизи head до полного заполнения fork_db из последних 100 блоков.
Конфигурация, влияющая на синхронизацию
| Настройка | По умолчанию | Эффект |
|---|---|---|
seed-node | — | Статические пиры; переподключаются после emergency_peer_reset() |
dlt-block-log-max-blocks | 100000 | Ёмкость DLT-журнала; влияет на то, как далеко назад пиры могут перекрывать пробелы |
trusted-peer-for-snapshot | — | Пиры, от которых принимается загрузка снимка |
stalled-sync-timeout-minutes | 2 | Минуты до запуска восстановления плагином snapshot |
enable-stale-production | false | Разрешить валидатору производить блоки без синхронизации (только для разработки) |
См. также: Обзор P2P, Режим Forward, Экстренный консенсус, Снимки.