Skip to content

Обработка блоков

Внутренняя механика применения блоков, управления ожидающими транзакциями и переключения форков.


Обзор

Когда узел получает новый блок через P2P, плагин chain вызывает database::push_block(). Последовательность:

  1. Временно удалить ожидающие (mempool) транзакции из базы данных.
  2. Применить входящий блок.
  3. Повторно применить ожидающие транзакции, не включённые в блок.

Это управляется вспомогательным классом without_pending_transactions в db_with.hpp.


Ключевые структуры данных

СтруктураТипНазначение
_pending_txvector<signed_transaction>Mempool: полученные транзакции, ожидающие включения в блок
_popped_txdeque<signed_transaction>Транзакции из извлечённого блока (при переключении форка); повторно применяются после переключения
_pending_tx_sessionoptional<session>Сессия отмены, охватывающая все изменения состояния ожидающих транзакций

Поток применения блока

push_block(new_block)
  └─ without_pending_transactions(db, skip, _pending_tx, callback)
       ├─ pending_transactions_restorer ctor: clear_pending()
       ├─ callback: _push_block(new_block)      ← применить входящий блок
       └─ ~pending_transactions_restorer()       ← восстановить ожидающие транзакции

Пошагово внутри _push_block()

  1. Ранние проверки отклонения (см. ниже).
  2. Добавить блок в fork_db.
  3. Если новая голова форка непосредственно продолжает текущую голову (new_block.previous == head_block_id()):
    • Пропустить логику переключения форка, перейти к apply_block().
  4. Если новая голова выше и расходится с текущей головой:
    • Сравнение форков с весом голосов (HF12) — см. Разрешение форков.
    • Извлекать блоки старого форка до общего предка.
    • Применять блоки нового форка по порядку.
  5. apply_block() запускает evaluator-ы транзакций, обновляет динамические глобальные свойства, обрабатывает виртуальные операции.
  6. check_block_post_validation_chain() — продвигает LIB, если ≥14 валидаторов отправили подписи пост-валидации для следующего блока после LIB (быстрый путь, финализация ~4 с). См. Блок-пост-валидация ниже.
  7. update_last_irreversible_block() — классический DPOS-фолбэк: продвигает LIB на основе блоков, произведённых ≥14 валидаторами после целевого (медленный путь, ~63 с).

Восстановление ожидающих транзакций

Деструктор ~pending_transactions_restorer() обрабатывает два списка по порядку после применения нового блока.

Шаг 1: Повторное применение _popped_tx (из переключения форка)

для каждой tx в _popped_tx:
    если time_elapsed > 200ms → отложить (добавить обратно в _pending_tx)
    иначе если is_known_transaction(tx) → пропустить (уже в цепочке)
    иначе → _push_transaction(tx) → applied_txs++

Шаг 2: Повторное применение _pending_transactions (исходный mempool)

для каждой tx в _pending_transactions:
    если time_elapsed > 200ms → отложить
    иначе если is_known_transaction(tx) → пропустить
    иначе → _push_transaction(tx) → applied_txs++
           при transaction_exception → отбросить (недействительная)
           при fc::exception → тихо отбросить

Шаг 3: Сводка в лог

Если какие-либо транзакции были отложены:

Postponed N pending transactions. M were applied.

Временной лимит

CHAIN_PENDING_TRANSACTION_EXECUTION_LIMIT = 200 мс

После истечения этого времени с начала восстановления все оставшиеся транзакции помещаются обратно в _pending_tx без применения. Это предотвращает блокировку узла на большом mempool.

Лимит срабатывает при:

  • Высокопроизводительных блоках с многочисленными ожидающими транзакциями
  • CPU-интенсивных операциях
  • Системе под нагрузкой

Ограничение размера блока при генерации

CHAIN_BLOCK_GENERATION_POSTPONED_TX_LIMIT = 5

Во время _generate_block() транзакции, которые превысят maximum_block_size, пропускаются. После 5 последовательных превышающих транзакций цикл генерации прерывается. Они остаются в _pending_tx для следующего блока.

Лог:

Postponed N transactions due to block size limit

Посев головы fork DB

Перед добавлением блока _push_block() убеждается, что текущий головной блок базы данных присутствует в fork_db:

если new_block.previous == head_block_id()
   И head_block_id() НЕ в fork_db:
     получить головной блок из block log (или dlt_block_log в DLT-режиме)
     fork_db.start_block(head_block)

Без этого посева допустимые следующие блоки выбрасывали бы unlinkable_block_exception, поскольку их previous отсутствует в fork_db. Это также исправляет узлы-валидаторы, генерирующие собственные блоки — generate_block() устанавливает pending_block.previous = head_block_id().


Обход прямого продолжения

После добавления блока в fork_db, если блок непосредственно продолжает голову базы данных:

если new_block.previous == head_block_id():
    → пропустить переключение форка, перейти к apply_block()

Это обрабатывает случай, когда fork_db._head указывает на устаревший более высокий блок из предыдущего неудачного цикла синхронизации. Без этого обхода устаревшая голова запускала бы логику переключения форка, которая тихо отбрасывает допустимый следующий блок.


Раннее отклонение блока

_push_block() применяет несколько ранних проверок отклонения во избежание ненужной работы и предотвращения бесконечных циклов синхронизации:

ПроверкаУсловиеДействие
Уже применёнblock.num ≤ head и ID совпадает с существующим блокомТихо игнорировать (дубликат)
Другой форкblock.num ≤ head, другой ID, родитель не в fork_dbТихо отклонить
Далеко впереди, разрыв > 100block.num > head, родитель неизвестен, разрыв > 100 блоковТихо отклонить (защита памяти)
Далеко впереди, разрыв ≤ 100block.num > head, родитель неизвестен, разрыв ≤ 100Разрешить в fork_db (кешируется в unlinked index)
Следующий блок напрямуюblock.previous == head_block_id()Всегда разрешено

Порог разрыва в 100 блоков предотвращает разбухание памяти от мёртвых форков, допуская нормальную обработку блоков вне порядка при P2P-синхронизации.


Переключение форка

Когда узел переключается на другой форк:

  1. pop_block() удаляет текущий головной блок; его транзакции перемещаются в _popped_tx.
  2. Повторять до достижения общего предка.
  3. Применять блоки нового форка по порядку от общего предка до новой головы.
  4. ~pending_transactions_restorer() сначала повторно применяет _popped_tx, затем исходный mempool.

Транзакции, уже находящиеся в новой цепочке, тихо пропускаются через is_known_transaction().

Линейное продолжение vs. реальный форк

_push_next() в fork_db может автоматически связывать несколько блоков-сирот при прибытии их родителя, вызывая прыжок fork_db._head на несколько блоков впереди головы базы данных за один вызов push_block(). Код различает:

  • Линейное продолжение (branches.second.size() == 1 и общий предок == текущая голова): операции извлечения не нужны; блоки применяются напрямую.
  • Реальное переключение форка (расходящиеся ветви): полная последовательность извлечения и повторного применения.

Это различие критично в DLT-режиме, где LIB == head и сессии отмены зафиксированы — цикл извлечения при линейном продолжении был бы бесконечным.


Обработка блоков-сирот (Unlinked Index)

Когда прибывает блок, родитель которого неизвестен, fork_db сохраняет его в _unlinked_index. Когда недостающий родитель наконец прибывает:

  1. _push_block(parent) связывает родителя с цепочкой.
  2. _push_next(parent) итерирует _unlinked_index в поиске дочерних блоков parent.
  3. Дочерние блоки перемещаются в _index и рекурсивно связываются.
  4. fork_db._head может продвинуться на несколько блоков за один вызов (запускает путь линейного продолжения).

Мягкий бан пиров на основе страйков

Пиры не получают немедленный бан за отправку не связываемых блоков. Счётчик накапливается:

ПутьПорогУсловие сброса
Нормальная работа: не связываемый блок на уровне головы или ниже20 страйковДопустимый блок от того же пира
Путь синхронизации: общее отклонение блока20 страйковДопустимый блок от того же пира
Мёртвый форк / блок слишком старыйНемедленный бан

Честные пиры могут восстановиться после временных ошибок (перезагрузка снимка, гонки таймингов, кратковременные micro-fork).


Тайминг производства блока валидатором

Плагин validator использует таймер с интервалом 250мс и упреждением 250мс:

  1. Таймер срабатывает каждые 250мс (выровнен по 250мс границам системных часов, минимальный сон 50мс).
  2. maybe_produce_block() вычисляет now = NTP_time + 250ms.
  3. get_slot_at_time(now) находит текущий слот.
  4. Если слот принадлежит настроенному валидатору и |scheduled_time - now| ≤ 500ms, производит блок с детерминированным scheduled_time в качестве временной метки.
Слот на T=6.000, тик на T=5.750:
  now = 5.750 + 0.250 = 6.000 → слот совпал → производить

Это даёт 500мс запас безопасности против порога задержки.

Условия производства (проверяются по порядку)

УсловиеРезультат при сбое
Цепочка синхронизирована (или enable-stale-production)not_synced
get_slot_at_time(now) > 0not_time_yet
Запланированный валидатор в нашем настроенном набореnot_my_turn
Ненулевой ключ подписи on-chainnot_my_turn
Приватный ключ для ключа подписи в памятиno_private_key
Участие в сети ≥ порога (до HF12)low_participation
`scheduled_time - now
Нет конкурирующего блока на той же высоте в fork_dbfork_collision
Последние 21 блок НЕ все от наших валидаторовminority_fork

Блок-пост-валидация: быстрая финализация LIB

Классический Fair-DPOS продвигает LIB только после того, как 2/3 валидаторов произведут блоки поверх целевого — при 21 валидаторе с интервалом 3 с это занимает ~63 секунды. Блок-пост-валидация заменяет этот механизм явными внеполосными сообщениями-подтверждениями, сокращая время финализации до ~4 секунд.

Как это работает

После apply_block(N):
  create_block_post_validation(N, block_id, producer)
    → сохраняет validator_confirmation_object в chainbase
    → удаляет записи ниже LIB
    → ограничивает список CHAIN_MAX_BLOCK_POST_VALIDATION_COUNT (20) записями

Тик таймера плагина validator:
  для каждого запланированного валидатора с загруженным приватным ключом:
    confirmations = get_validator_confirmations(validator)
    для каждого подтверждения:
      sig = sign(chain_id + block_id)   ← secp256k1 с ключом подписи валидатора
      p2p.broadcast_block_post_validation(block_id, validator, sig)
                                         ← fire-and-forget, неблокирующий

Принимающий пир (p2p_plugin handle_message, тип сообщения 6009):
  восстановить pubkey из sig
  сравнить с validator.signing_key on-chain
  если совпадает → db.apply_block_post_validation(block_id, validator)
    → отметить валидатора как подтвердившего этот блок
    → вызвать check_block_post_validation_chain()

check_block_post_validation_chain():
  обойти validator_confirmation_index начиная с (LIB + 1)
  подсчитать уникальных запланированных валидаторов, подтвердивших каждый блок
  если подтверждений ≥ ⌈2/3 × num_scheduled_validators⌉ (≥ 14 из 21):
    продвинуть last_irreversible_block_num
    зафиксировать undo-сессии до нового LIB
    повторить для следующего блока

Проводное сообщение

block_post_validation_message (тип 6009, legacy graphene протокол):

cpp
struct block_post_validation_message {
    block_id_type  block_id;
    std::string    witness_account;   // имя валидатора
    signature_type validator_signature; // sign(chain_id + block_id)
};

Тайминги

ФазаДлительность
Блок произведён и распространён0 – 1 с
Валидаторы подписывают и рассылают подтверждение~0 с (следующий тик плагина, 250 мс)
Подтверждения распространяются по всем пирам1 – 2 с
check_block_post_validation_chain() собирает ≥14 подписей1 – 2 с
Итоговая финализация~3 – 5 с

Классический DPOS-путь (~63 с) остаётся активным как фолбэк, если сообщения-подтверждения теряются или ещё не получены.

Ограничения

  • В зачёт порога 2/3 идут только валидаторы из текущего перетасованного расписания; валидаторы вне расписания пропускаются.
  • Во время аварийного консенсуса (emergency_consensus_active = true) check_block_post_validation_chain() сразу возвращается — LIB продвигается исключительно по классическому пути, чтобы не заблокировать восстановление.
  • Список подтверждений ограничен 20 записями (CHAIN_MAX_BLOCK_POST_VALIDATION_COUNT). Записи ниже текущего LIB удаляются после каждого блока.

Константы конфигурации

КонстантаЗначениеОписание
CHAIN_PENDING_TRANSACTION_EXECUTION_LIMIT200 мсМаксимальное время повторного применения ожидающих транзакций после добавления блока
CHAIN_BLOCK_GENERATION_POSTPONED_TX_LIMIT5Максимум последовательных превышающих транзакций, пропускаемых при генерации
CHAIN_BLOCK_SIZE65536 байтЖёсткий лимит размера блока
maximum_block_sizeДинамический (медиана валидаторов)Мягкий лимит размера блока
CHAIN_BLOCK_INTERVAL3 сИнтервал производства блоков

Префиксы отладочных логов

ПрефиксЗначение
FORK-SWITCH-POP: popping head #HНормальное переключение форка — извлечение блока старого форка
FORK-RECOVER-POP: popping head #HВосстановление после ошибки — откат неудачного переключения форка
POP_BLOCK: db_head=#X fork_db_head=#YСостояние перед каждым вызовом pop_block()
Fork switch: new_head=#X branches.first=N branches.second=MВетви перед переключением форка; M=0 означает линейное продолжение

См. также: Fair-DPOS, Разрешение форков, Узел-валидатор.