Skip to content

Режим 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
Резерв LIBis_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:

  1. Записать, что X имеет этот блок (state.record_known_block(block.id())).
  2. Применить блок к цепочке.
  3. 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() (см. ниже).

Действия при переходе

  1. Уведомить все подключённые пиры: транслировать dlt_fork_status_message с node_status=FORWARD каждому активному/синхронизирующемуся пиру (не только с включённым обменом). Это позволяет пирам немедленно переоценить exchange_enabled для нас.
  2. Переоценить exchange_enabled для всех пиров.
  3. Повторно проверить и очистить недействительные временные записи мемпула.
  4. Сбросить _sync_stagnation_retries = 0.
  5. Сбросить _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():

  1. Переводит все BANNED пиры обратно в состояние DISCONNECTED; очищает spam_strikes.
  2. Сбрасывает backoff всех DISCONNECTED пиров до 30 с (INITIAL_RECONNECT_BACKOFF_SEC) с next_reconnect_attempt = now.
  3. Очищает счётчики повторов застоя.
  4. На следующем тике периодической задачи (~5 с) periodic_reconnect_check() немедленно переподключается.

Что не пересылается

СценарийТрафик
Пир имеет exchange_enabled=falseНет блоков, нет транзакций
Узел в режиме SYNCНет трансляций; только запросы диапазона и запросы gap fill
Обработка блоков приостановлена (_block_processing_paused=true)Блоки принимаются и ставятся в очередь, но периодические задачи с доступом к БД пропускаются

Сводка доставки

СобытиеПолучателиИсключеныФильтрация эха
Узел производит блокВсе ACTIVE пиры с exchange_enabled=true(нет)Пиры с блоком в known_blocks
Узел получает блок от XВсе ACTIVE пиры с exchange_enabled=trueXПиры с блоком в known_blocks
Узел создаёт транзакциюВсе ACTIVE пиры с exchange_enabled=true(нет)(нет)
Узел получает транзакцию от XВсе ACTIVE пиры с exchange_enabled=trueX(нет)

peer_head_num — устаревший снимок

peer_head_num, показанный в статистике, обновляется из:

  • Рукопожатия hello
  • Обменов сообщениями dlt_fork_status_message
  • Ретрансляции блоков (получение блока N означает peer_head_num ≥ N)

Между этими событиями реальный head цепочки пира может быть значительно выше. Не следует воспринимать peer_head_num как актуальное значение.


См. также: Обзор P2P, Сценарии синхронизации, Справочник статистики, Сообщения.