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, Сценарии синхронизации, Справочник статистики, Сообщения.