Архитектура 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_message | 5100 | Рукопожатие: версия протокола, head/LIB, DLT-диапазон, статус узла/форка |
dlt_hello_reply_message | 5101 | Ответ на рукопожатие: exchange_enabled, fork_alignment |
dlt_range_request_message | 5102 | Запрос диапазона ID блоков |
dlt_range_reply_message | 5103 | Ответ с доступным диапазоном блоков |
dlt_get_block_range_message | 5104 | Получить блоки start..end с проверкой prev_block_id |
dlt_block_range_reply_message | 5105 | Ответ: вектор блоков + флаг is_last |
dlt_get_block_message | 5106 | Получить один блок по ID |
dlt_block_reply_message | 5107 | Ответ: блок + next_available + is_last |
dlt_not_available_message | 5108 | Блок недоступен |
dlt_fork_status_message | 5109 | Трансляция текущего статуса форка/узла пирам |
dlt_peer_exchange_request | 5110 | Запрос списка известных пиров |
dlt_peer_exchange_reply | 5111 | Ответ со списком пиров |
dlt_peer_exchange_rate_limited | 5112 | Уведомление об ограничении скорости: ожидать N секунд |
dlt_transaction_message | 5113 | Трансляция подписанной транзакции |
dlt_soft_ban_message | 5114 | Уведомление перед отключением забаненного пира |
dlt_gap_fill_request | 5115 | Запрос конкретных номеров блоков для заполнения пробела |
dlt_gap_fill_reply | 5116 | Ответ с запрошенными блоками |
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:
request_blocks_from_peer()— отправляетdlt_get_block_range_messageдля до 200 блоков после нашего head.on_dlt_block_range_reply()— валидирует хеш-цепочкуprev_block_id, применяет каждый блок.check_sync_catchup()— сравнивает наш head с head'ами всех пиров; переходит в FORWARD после синхронизации.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-blocks | 100 000 | Максимальное число блоков в скользящем DLT block log |
dlt-peer-max-disconnect-hours | 8 | Удалить пира после N часов без ответа |
dlt-mempool-max-tx | 10 000 | Жёсткий лимит записей mempool |
dlt-mempool-max-bytes | 100 МБ | Жёсткий лимит памяти mempool |
dlt-mempool-max-tx-size | 64 КБ | Отклонять слишком большие транзакции |
dlt-mempool-max-expiration-hours | 24 | Отклонять транзакции с далёким будущим истечением |
dlt-peer-exchange-max-per-reply | 10 | Максимум пиров в ответе на обмен |
dlt-peer-exchange-max-per-subnet | 2 | Антисибил: максимум 2 пира на /24 |
dlt-peer-exchange-min-uptime-sec | 600 | Минимальный аптайм перед обменом пиром |
dlt-stats-interval-sec | 300 | Интервал лога статистики пиров (мин. 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.