Skip to content

Архитектура DLT P2P

P2P-слой VIZ Ledger был переработан с устаревшего протокола Graphene на основе synopsis (node.cpp) на специализированный DLT-нативный протокол (dlt_p2p_node.cpp). Публичный API плагина остался неизменным — заменена только внутренняя реализация.


Обзор

До:     p2p_plugin → graphene::network::node    (node.cpp, 6978 строк, STCP, gossip-инвентарь)
После:  p2p_plugin → dlt_p2p_node               (dlt_p2p_node.cpp, 2627 строк, raw TCP, диапазонная синхронизация)

Замена выполнена на месте: то же имя плагина "p2p", тот же порт (2001/4243), тот же публичный API. Все зависимые плагины (валидатор, snapshot и т.п.) не потребовали изменений.


Проводной протокол

Чистый TCP — без уровня шифрования STCP. Каждое сообщение на канале:

[4 байта: размер данных (uint32_t)] [4 байта: msg_type (uint32_t)] [N байт: fc::raw::pack(T)]

Типы сообщений (5100–5116)

ТипIDОписание
dlt_hello_message5100Рукопожатие: версия протокола, head/LIB, DLT-диапазон, статус узла/форка
dlt_hello_reply_message5101Ответ на рукопожатие: exchange_enabled, fork_alignment
dlt_range_request_message5102Запрос диапазона ID блоков
dlt_range_reply_message5103Ответ с доступным диапазоном блоков
dlt_get_block_range_message5104Получить блоки start..end с проверкой prev_block_id
dlt_block_range_reply_message5105Ответ: вектор блоков + флаг is_last
dlt_get_block_message5106Получить один блок по ID
dlt_block_reply_message5107Ответ: блок + next_available + is_last
dlt_not_available_message5108Блок недоступен
dlt_fork_status_message5109Трансляция текущего статуса форка/узла пирам
dlt_peer_exchange_request5110Запрос списка известных пиров
dlt_peer_exchange_reply5111Ответ со списком пиров
dlt_peer_exchange_rate_limited5112Уведомление об ограничении скорости: ожидать N секунд
dlt_transaction_message5113Трансляция подписанной транзакции
dlt_soft_ban_message5114Уведомление перед отключением забаненного пира
dlt_gap_fill_request5115Запрос конкретных номеров блоков для заполнения пробела
dlt_gap_fill_reply5116Ответ с запрошенными блоками

Fiber-архитектура

Весь ввод/вывод выполняется в одном fc::thread с использованием кооперативных файберов — мьютексы для разделяемого состояния не нужны:

ФайберРоль
Цикл принятияОжидает входящие соединения; отклоняет дублирующиеся IP
Цикл чтения (на пира)Читает сообщения; диспетчеризует в on_message()
Периодическая задачаПереподключение, проверка стагнации, статистика пиров, очистка mempool

Файберы уступают управление на блокирующем вводе/выводе (readsome(), writesome()), позволяя работать с несколькими пирами в одном потоке без конкуренции.


Статусы узла и жизненный цикл пира

Статусы узла: SYNC (догоняет) / FORWARD (живой, обменивается блоками)

Состояния жизненного цикла пира:

CONNECTING → HANDSHAKING → SYNCING → ACTIVE → DISCONNECTED → BANNED

Таймауты: connecting=5с, handshaking=10с. Откат переподключения: 30с → 60с → … → 3600с с джиттером ±25%, сброс после 5 минут стабильного аптайма. Пиры удаляются после 8 часов без ответа.


Синхронизация блоков: режим SYNC

Узел в режиме SYNC последовательно получает блоки от пира с более высоким head:

  1. request_blocks_from_peer() — отправляет dlt_get_block_range_message для до 200 блоков после нашего head.
  2. on_dlt_block_range_reply() — валидирует хеш-цепочку prev_block_id, применяет каждый блок.
  3. check_sync_catchup() — сравнивает наш head с head'ами всех пиров; переходит в FORWARD после синхронизации.
  4. sync_stagnation_check() — после 30с без нового блока повторяет до 3 раз, затем переходит в FORWARD с предупреждением.

Заполнение пробелов

Когда между нашим head и ранним доступным блоком синхронизирующегося пира существует непрерывный пробел, request_gap_fill() отправляет dlt_gap_fill_request (до 100 блоков на запрос) любому пиру, чей DLT-диапазон покрывает пробел. Заполнение пробелов работает как в SYNC, так и в FORWARD режиме:

  • Запускается из on_dlt_block_reply() (обнаружен блок не в порядке) и periodic_task() (каждые 5с).
  • Откат от пиров с включённым обменом к любому активному пиру с более высоким head.
  • Откат в режим SYNC, если ни один пир не имеет нужных блоков.
  • Большие пробелы обрабатываются кусками по 100 блоков с задержкой 5с между запросами.

Обмен блоками: режим FORWARD

В режиме FORWARD пиры обмениваются живыми блоками и транзакциями:

  • Флаг exchange_enabled контролирует, получает ли пир от нас новые блоки.
  • При переходе в FORWARD, dlt_fork_status_message отправляется всем пирам (не только с включённым обменом), уведомляя их о готовности.
  • on_dlt_fork_status() пересчитывает exchange_enabled при переходе пира из SYNC в FORWARD.
  • check_forward_stagnation() — если head не продвигался 30с И хотя бы один пир опережает, переходит в SYNC.

Выравнивание форков и право на обмен

В процессе рукопожатия check_fork_alignment() выполняет многоуровневое сопоставление ID блоков для определения, находятся ли пиры на одном форке:

ПроверкаУсловие
Пустой пирhead_block_num == 0 → выровнен (новый узел)
Перекрытие диапазоновНаш DLT-лог покрывает head пира → is_block_known(head_id)
Граничная связьpeer_head + 1 == our_earliest → проверить previous нашего раннего блока == peer_head_id
Откат к LIBВсегда проверять is_block_known(lib_id)

Эта многоуровневая проверка предотвращает ложные отключения "разные форки" в DLT-режиме, где старые блоки обрезаны, и старая проверка по одному ID не прошла бы для пиров на одной цепочке.


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

Подсистема разрешения форков отслеживает конкурирующие верхушки цепочки:

  • Порог: 42 блока расхождения запускают resolve_fork() (= CHAIN_MAX_VALIDATORS × 2, один полный оборот расписания).
  • Выбор: Наиболее тяжёлая ветка по весу голосов.
  • Гистерезис: 6 последовательных блоков как победитель перед переключением (CONFIRMATION_BLOCKS).
  • Статус: _fork_status открыт через is_on_majority_fork() для плагина валидатора для проверки перед производством блоков.

Антиспам

МеханизмОписание
Счётчик spam_strikesЕдиный счётчик на пира; сбрасывается на хорошем пакете; мягкий бан при пороге=10
Мягкий банУстанавливает состояние BANNED на 3600с; отправляет dlt_soft_ban_message перед закрытием
Дедупликация по IPОтклоняет дублирующиеся соединения с одного IP (как входящие, так и исходящие)
Дедупликация трансляцийsend_to_all_our_fork_peers() отслеживает std::set<ip::address> для пропуска дублирующихся IP

Дублирующиеся блоки и блоки не в порядке из ответов на диапазонные запросы молча пропускаются — не считаются спамом. Ошибки десериализации не увеличивают счётчик spam strikes.


P2P Mempool

Отдельный внутрипроцессный mempool (отличный от _pending_tx цепочки) обеспечивает раннюю фильтрацию транзакций до приёма цепочкой:

  • Дедупликация по tx_id.
  • Вытеснение по старейшему истечению при достижении лимитов.
  • Лимиты (настраиваемые): максимум 10 000 записей, 100 МБ всего, 64 КБ на транзакцию.
  • Предварительные записи помечаются в режиме SYNC; повторно валидируются при переходе в FORWARD.
  • Очистка при получении блока (remove_transactions_in_block) и переключении форка (prune_mempool_on_fork_switch).

Обмен пирами

Ограниченное по скорости обнаружение пиров:

  • Максимум 3 запроса за 5-минутное окно на пира.
  • Фильтр разнообразия подсетей: максимум 2 пира на префикс /24 в каждом ответе.
  • Обмениваются только пиры с аптаймом ≥600с.
  • Входящие пиры (эфемерные порты) исключаются из ответов на обмен.

Механизмы восстановления

Изоляция пиров (P53)

Когда в течение 60 секунд нет активных пиров, emergency_peer_reset():

  • Очищает все мягкие баны (BANNED → DISCONNECTED, сбрасывает spam strikes).
  • Сбрасывает откаты всех отключённых пиров до минимума с немедленным переподключением.

Пауза/возобновление обработки блоков

pause_block_processing() / resume_block_processing() позволяют плагину snapshot приостановить приём P2P-блоков во время сериализации состояния. Периодическая задача пропускает операции, обращающиеся к БД, в режиме паузы.

Начальный льготный период (P22)

В течение первых 60 секунд после запуска блоки в пределах 10 от head обрабатываются как FORK_DB_ONLY вместо DEAD_FORK — предотвращая каскадные отклонения пока fork_db восстанавливается из block log.


Результаты приёма блоков

Перечисление dlt_block_accept_result заменяет старый булев возврат:

ЗначениеСмысл
ACCEPTEDБлок применён к цепочке (стал новым head)
FORK_DB_ONLYХранится в fork_db, но не применён (несвязываемый, конкурирующий форк)
DEAD_FORKБлок на/ниже head из мёртвого форка — пир получает мягкий бан
ALREADY_KNOWNУже есть этот блок (дубликаты, block_too_old_exception)
REJECTEDПровал полной валидации

Справочник конфигурации

ПараметрПо умолчаниюОписание
dlt-block-log-max-blocks100 000Максимальное число блоков в скользящем DLT block log
dlt-peer-max-disconnect-hours8Удалить пира после N часов без ответа
dlt-mempool-max-tx10 000Жёсткий лимит записей mempool
dlt-mempool-max-bytes100 МБЖёсткий лимит памяти mempool
dlt-mempool-max-tx-size64 КБОтклонять слишком большие транзакции
dlt-mempool-max-expiration-hours24Отклонять транзакции с далёким будущим истечением
dlt-peer-exchange-max-per-reply10Максимум пиров в ответе на обмен
dlt-peer-exchange-max-per-subnet2Антисибил: максимум 2 пира на /24
dlt-peer-exchange-min-uptime-sec600Минимальный аптайм перед обменом пиром
dlt-stats-interval-sec300Интервал лога статистики пиров (мин. 30)

Цветовое логирование

ЦветЗначение
ЗелёныйПрогресс синхронизации и производство блоков
БелыйНормальный обмен блоками
КрасныйСобытия форка
Тёмно-серыйОбработка транзакций
ОранжевыйПредупреждения (мягкие баны, стагнация, пробелы)
ГолубойВывод статистики пиров

Паттерн делегата

Сетевая библиотека ссылается только на fc и graphene_protocol — не на graphene_chain. Абстрактный интерфейс dlt_p2p_delegate устраняет этот разрыв:

dlt_p2p_node (сетевая библиотека)  ←→  dlt_p2p_delegate (интерфейс)  ←→  dlt_delegate (p2p_plugin)

dlt_delegate в p2p_plugin.cpp реализует:

  • read_block_by_num() — проверяет dlt_block_log, затем fork_db.
  • accept_block() — вызывает push_block(); перехватывает unlinkable_block_exception → хранит в fork_db.
  • get_fork_branch_tips() — получает из fork_db вокруг текущего head.
  • is_tapos_block_known() — делегирует db.is_known_block().

См. также: Обзор P2P, Сценарии синхронизации, Плагин snapshot, Block log.