Skip to content

Сценарии синхронизации 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

  1. Слейв отправляет hello: head_num=999, head_id=H999.
  2. Проверка 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.
  3. Мастер отвечает: exchange_enabled=true, fork_alignment=true.
  4. Слейв переходит в состояние жизненного цикла 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

  1. Слейв отправляет hello: head_num=800, head_id=H800.
  2. Проверка выравнивания форка мастера: 800 < 1000, проверка граничной ссылки не проходит (800 + 1 ≠ 1000), резервный вариант по LIB тоже не проходит (LIB ID обрезан).
  3. fork_alignment = false, но exchange_enabled = false.
  4. Мастер не отключает слейв, так как 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

  1. Новый узел отправляет hello: head_num=0.
  2. Проверка форка мастера: head_num == 0пустой пирfork_alignment = true (обрабатывается как "новый узел, ещё не на каком-либо форке").
  3. exchange_enabled = true (мастер будет принимать блоки от этого узла).
  4. Новый узел переходит в состояние жизненного цикла 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.

Восстановление при запуске

  1. database::open() проверяет консистентность DLT block log: если head журнала совпадает с head базы данных → консистентен; иначе сбрасывает журнал.
  2. Последние 100 блоков из DLT block log засеваются в fork_db (блоки 1813–1912). Это даёт вновь поступающим блокам окно родителя в 100 блоков без необходимости их предварительной загрузки.
  3. Применяется 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 имеет больший вес голосов.

Обнаружение форка

  1. Блок с форка B поступает через трансляцию. Fork DB привязывает его к родительской цепочке.
  2. track_fork_state() вызывается после каждого блока. Когда форк B удерживает преимущество на протяжении 42 блоков (2 полных раунда валидаторов), запускается resolve_fork().
  3. resolve_fork() вычисляет суммарный вес голосов (делегированные SHARES) валидаторов на каждой ветви. Форк B должен удерживать 6 последовательных подтверждающих блоков до фиксации переключения.

Выполнение переключения форка

  1. pop_block() удаляет блоки форка A обратно до общего предка. Выброшенные транзакции попадают в _popped_tx.
  2. Блоки форка B применяются от общего предка до нового head.
  3. _popped_tx и _pending_tx повторно применяются; транзакции, уже в цепочке форка B, молча пропускаются.

Статус форка в статистике: переходы NORMAL → LOOKING → NORMAL (или MINORITY, если этот узел на проигрывающей ветви).


Сценарий 6: Синхронизация при экстренном консенсусе

Настройка: Сеть застала более 3600 секунд. Экстренный консенсус активен.

Работа мастера

Экстренный мастер (узел с emergency-private-key в конфиге) производит все 21 блок в раунде, используя ключ подписи "committee". В статистике: +emrg +ekey.

Синхронизация слейва при экстренном режиме

  1. Слейв подключается к мастеру. Hello мастера включает emergency_active=true, has_emergency_key=true.
  2. Проверка выравнивания форка слейва выполняется нормально — блоки committee являются обычными подписанными блоками с точки зрения P2P-уровня.
  3. Слейв входит в режим SYNC и запрашивает блоки, произведённые committee, от мастера.
  4. Валидация блоков: verify_signing_witness() смягчает проверку соответствия производитель-слот во время экстренного режима — если производитель блока не совпадает с точным запланированным слотом, он принимается, пока подпись проходит проверку против signing_key производителя.

Восстановление ключа валидатора

Когда реальные валидаторы восстанавливают свои ключи подписи (через validator_update_operation), перестройка расписания включает их в гибридное расписание. Как только 15 из 21 слота валидаторов стали реальными (не committee), экстренный режим деактивируется. Последующие блоки производятся реальными валидаторами и синхронизируются нормально.


Сценарий 7: Восстановление застоя синхронизации

Условие: Режим SYNC, блоки не получены в течение 30 секунд.

  1. Срабатывает sync_stagnation_check(): попытка 1 из 3 — повторный запрос блоков от всех активных пиров с включённым обменом.
  2. Через 30 секунд: попытка 2 из 3.
  3. Через 30 секунд: попытка 3 из 3.
  4. После третьей попытки: 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() после паузы снимка.

Протокол:

  1. Выбрать пир с наибольшим peer_head_num среди активных пиров.
  2. Отправить dlt_gap_fill_request(block_nums=[N+1, N+2, ...]) (макс. 100 блоков).
  3. Ждать ответа до 15 секунд.
  4. При получении применить вернувшиеся блоки. Если блоки всё ещё отсутствуют, запустить ещё один 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.

Поведение:

  1. dlt_delegate::accept_block() возвращает DEAD_FORK.
  2. Блок НЕ сохраняется в fork_db._unlinked_index (предотвращает рост памяти).
  3. Пир накапливает spam-удар за каждый dead-fork блок.
  4. После 10 ударов пир получает мягкий бан на 3600 с.
  5. Цикл синхронизации прерывается — дальнейшие блоки от этого пира не обрабатываются в текущем батче.

Льготный период (исправление 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-blocks100000Ёмкость DLT-журнала; влияет на то, как далеко назад пиры могут перекрывать пробелы
trusted-peer-for-snapshotПиры, от которых принимается загрузка снимка
stalled-sync-timeout-minutes2Минуты до запуска восстановления плагином snapshot
enable-stale-productionfalseРазрешить валидатору производить блоки без синхронизации (только для разработки)

См. также: Обзор P2P, Режим Forward, Экстренный консенсус, Снимки.