Режим Forward — обмен блоками и транзакциями
Режим forward (DLT_NODE_STATUS_FORWARD) — это нормальное рабочее состояние после того, как узел догнал сеть. Вместо вытягивания диапазонов блоков от пиров, узел толкает новые блоки и транзакции всем выравненным по форку пирам по мере их поступления.
Шлюз доставки: exchange_enabled
Весь трафик в режиме forward фильтруется двумя флагами для каждого пира:
| Флаг | Значение |
|---|---|
exchange_enabled == true | Пир выровнен по форку — его head блок известен нам (или наш ему) |
lifecycle_state == ACTIVE | Пир завершил рукопожатие |
Оба должны быть истинными, чтобы пир получал трансляции блоков и транзакций.
Центральная функция трансляции send_to_all_our_fork_peers() итерирует все подключённые пиры и пропускает тех, кто не проходит ни одну из проверок.
Установка exchange_enabled
Начальная установка (рукопожатие hello)
Во время hello acceptor вызывает check_fork_alignment() — многоуровневую проверку с учётом DLT-диапазона:
| Случай | Проверка |
|---|---|
Пир без блоков (head_num == 0) | → выровнен |
| Head пира в нашем DLT-диапазоне | is_block_known(peer.head_id) |
| Head пира + 1 == наш DLT earliest | Читаем наш самый ранний блок; проверяем previous == peer.head_id |
| Резерв LIB | is_block_known(peer.lib_id) |
Если хотя бы одна проверка прошла → exchange_enabled = true.
OR-комбинирование
Обе стороны отправляют сообщения hello друг другу и каждая независимо вычисляет exchange_enabled. Финальное значение для пира — это логическое ИЛИ определений обеих сторон. Если любая из сторон распознаёт цепочку другой, обмен включается.
Слейв, чей head ниже DLT-диапазона мастера, не проходит свою check_fork_alignment (он ещё не применил блоки мастера), но проверка мастера проходит (он знает head слейва). OR гарантирует включение обмена даже в этом асимметричном случае.
Триггеры повторной оценки
exchange_enabled переоценивается при изменении знаний узла о блоках пира:
| Триггер | Когда |
|---|---|
transition_to_forward() | Для каждого пира с exchange_enabled=false; повторная проверка is_block_known(peer_head_id) |
on_dlt_fork_status() | Пир переходит SYNC → FORWARD; повторная проверка выравнивания форка |
| Блок принят от пира | Если блок применяется к нашей цепочке, немедленно включает обмен для этого пира |
Трансляция блоков
Блоки собственного производства
Когда валидатор производит блок:
validator.cpp → p2p_plugin.broadcast_block(block)
→ dlt_p2p_node.broadcast_block(block)
→ send_to_all_our_fork_peers(dlt_block_reply_message, exclude=none, block_id=block.id())Блок отправляется всем ACTIVE пирам с включённым обменом. Подавление эха предотвращает повторную отправку пирам, которые уже имеют блок.
Ретрансляция полученных блоков
Когда блок поступает от пира X:
- Записать, что X имеет этот блок (
state.record_known_block(block.id())). - Применить блок к цепочке.
send_to_all_our_fork_peers(block_reply, exclude=X, block_id=block.id())— отправить всем другим ACTIVE пирам с включённым обменом.
Подавление эха блоков
Без подавления блоки возвращаются к производителю через цепочки ретрансляции:
A производит блок N → отправляет B, C
B ретранслирует N в A, C
C ретранслирует N в A, B
A получает собственный блок N обратно от B и C — впустую потраченная пропускная способностьКаждое состояние пира ведёт кольцевой буфер из 20 последних ID блоков (known_blocks). Перед отправкой блока пиру узел проверяет peer.has_block(block_id). Если уже известен, отправка пропускается.
Пир записывается как "имеющий" блок в двух случаях:
- Мы только что отправили его — записывается в
send_to_all_our_fork_peersпосле отправки. - Они отправили его нам — записывается в
on_dlt_block_replyпри получении.
В журнале ретрансляции показаны счётчики фильтрации эха:
Relay block_reply to 3 peers (0 skipped: no_exchange, 0 skipped: not_active, 1 skipped: echo)Трансляция транзакций
Самостоятельно (через API)
Транзакция, отправленная через network_broadcast_api → добавляется в P2P-мемпул → dlt_transaction_message отправляется всем ACTIVE пирам с включённым обменом.
Ретрансляция полученных транзакций
Транзакция поступает от пира X → добавляется в мемпул → ретранслируется всем ACTIVE пирам с включённым обменом кроме X.
Предварительная фильтрация мемпула
Перед принятием транзакции в мемпул или её пересылкой она должна пройти:
| Проверка | Неудача |
|---|---|
Дубликат (trx_id уже в мемпуле) | Молча пропустить |
Истекшая (expiration < now) | Отклонить; увеличить spam-удар, если от пира |
| Срок действия слишком далеко (>24 ч в будущем) | Отклонить; увеличить spam-удар |
Слишком большая (>dlt-mempool-max-tx-size, по умолч. 64 КБ) | Отклонить; увеличить spam-удар |
| TaPoS недействителен (ссылочный блок неизвестен) | Отклонить; увеличить spam-удар |
| Мемпул полон | Вытеснить запись с наиболее ранним сроком, затем добавить |
Временные записи: Транзакции, полученные в режиме SYNC, помечаются is_provisional = true — хранятся локально, но не пересылаются пирам. При переходе в FORWARD временные записи повторно проверяются против текущего head, недействительные удаляются.
Переход SYNC → FORWARD
Триггеры
| Триггер | Условие |
|---|---|
Ответ на диапазон блоков с is_last=true | И применён хотя бы один блок (не все dead-fork) |
check_sync_catchup() | our_head >= все активные heads пиров И хотя бы один активный пир |
| Таймаут застоя | 30 с без блока, 3 повтора исчерпаны |
check_sync_catchup() запускается после каждого принятия блока и каждые 5 секунд из периодической задачи.
Защита от изоляции: check_sync_catchup() НЕ заявляет о догоне при нулевом количестве активных пиров. Вместо этого запускается 60-секундный таймер изоляции; после истечения срабатывает emergency_peer_reset() (см. ниже).
Действия при переходе
- Уведомить все подключённые пиры: транслировать
dlt_fork_status_messageсnode_status=FORWARDкаждому активному/синхронизирующемуся пиру (не только с включённым обменом). Это позволяет пирам немедленно переоценитьexchange_enabledдля нас. - Переоценить
exchange_enabledдля всех пиров. - Повторно проверить и очистить недействительные временные записи мемпула.
- Сбросить
_sync_stagnation_retries = 0. - Сбросить
_last_block_received_time = now, чтобы таймер застоя forward начинал заново.
Откат FORWARD → SYNC
Если блоки перестают поступать в режиме FORWARD, узел откатывается в SYNC:
| Триггер | Условие |
|---|---|
| Ответ hello показывает, что пир далеко впереди | peer_head_num > our_head + 2 при получении hello_reply |
| Периодическая проверка | check_forward_behind(): любой активный пир имеет peer_head_num > our_head + 2 |
| Застой | check_forward_stagnation(): head не двигается 30 с И хотя бы один пир впереди |
Бездействие при отсутствии опережающих пиров: check_forward_stagnation() НЕ переходит в SYNC, когда все подключённые пиры имеют тот же head. Синхронизировать нечего; переход просто вызвал бы осцилляцию. Таймер застоя сбрасывается, и узел остаётся в FORWARD.
При переходе в SYNC _last_block_received_time сбрасывается до now, чтобы таймер застоя синхронизации начинал заново (не унаследовав время из фазы FORWARD).
Восстановление изоляции пира
Когда все пиры отключены или заблокированы (например, после паузы снимка), нормальные переходы режима SYNC/FORWARD крутятся бессмысленно. После 60 секунд с нулём активных соединений:
emergency_peer_reset():
- Переводит все BANNED пиры обратно в состояние DISCONNECTED; очищает
spam_strikes. - Сбрасывает backoff всех DISCONNECTED пиров до 30 с (
INITIAL_RECONNECT_BACKOFF_SEC) сnext_reconnect_attempt = now. - Очищает счётчики повторов застоя.
- На следующем тике периодической задачи (~5 с)
periodic_reconnect_check()немедленно переподключается.
Что не пересылается
| Сценарий | Трафик |
|---|---|
Пир имеет exchange_enabled=false | Нет блоков, нет транзакций |
| Узел в режиме SYNC | Нет трансляций; только запросы диапазона и запросы gap fill |
Обработка блоков приостановлена (_block_processing_paused=true) | Блоки принимаются и ставятся в очередь, но периодические задачи с доступом к БД пропускаются |
Сводка доставки
| Событие | Получатели | Исключены | Фильтрация эха |
|---|---|---|---|
| Узел производит блок | Все ACTIVE пиры с exchange_enabled=true | (нет) | Пиры с блоком в known_blocks |
| Узел получает блок от X | Все ACTIVE пиры с exchange_enabled=true | X | Пиры с блоком в known_blocks |
| Узел создаёт транзакцию | Все ACTIVE пиры с exchange_enabled=true | (нет) | (нет) |
| Узел получает транзакцию от X | Все ACTIVE пиры с exchange_enabled=true | X | (нет) |
peer_head_num — устаревший снимок
peer_head_num, показанный в статистике, обновляется из:
- Рукопожатия hello
- Обменов сообщениями
dlt_fork_status_message - Ретрансляции блоков (получение блока N означает
peer_head_num ≥ N)
Между этими событиями реальный head цепочки пира может быть значительно выше. Не следует воспринимать peer_head_num как актуальное значение.
См. также: Обзор P2P, Сценарии синхронизации, Справочник статистики, Сообщения.