Обработка блоков
Внутренняя механика применения блоков, управления ожидающими транзакциями и переключения форков.
Обзор
Когда узел получает новый блок через P2P, плагин chain вызывает database::push_block(). Последовательность:
- Временно удалить ожидающие (mempool) транзакции из базы данных.
- Применить входящий блок.
- Повторно применить ожидающие транзакции, не включённые в блок.
Это управляется вспомогательным классом without_pending_transactions в db_with.hpp.
Ключевые структуры данных
| Структура | Тип | Назначение |
|---|---|---|
_pending_tx | vector<signed_transaction> | Mempool: полученные транзакции, ожидающие включения в блок |
_popped_tx | deque<signed_transaction> | Транзакции из извлечённого блока (при переключении форка); повторно применяются после переключения |
_pending_tx_session | optional<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()
- Ранние проверки отклонения (см. ниже).
- Добавить блок в
fork_db. - Если новая голова форка непосредственно продолжает текущую голову (
new_block.previous == head_block_id()):- Пропустить логику переключения форка, перейти к
apply_block().
- Пропустить логику переключения форка, перейти к
- Если новая голова выше и расходится с текущей головой:
- Сравнение форков с весом голосов (HF12) — см. Разрешение форков.
- Извлекать блоки старого форка до общего предка.
- Применять блоки нового форка по порядку.
apply_block()запускает evaluator-ы транзакций, обновляет динамические глобальные свойства, обрабатывает виртуальные операции.check_block_post_validation_chain()— продвигает LIB, если ≥14 валидаторов отправили подписи пост-валидации для следующего блока после LIB (быстрый путь, финализация ~4 с). См. Блок-пост-валидация ниже.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 | Тихо отклонить |
| Далеко впереди, разрыв > 100 | block.num > head, родитель неизвестен, разрыв > 100 блоков | Тихо отклонить (защита памяти) |
| Далеко впереди, разрыв ≤ 100 | block.num > head, родитель неизвестен, разрыв ≤ 100 | Разрешить в fork_db (кешируется в unlinked index) |
| Следующий блок напрямую | block.previous == head_block_id() | Всегда разрешено |
Порог разрыва в 100 блоков предотвращает разбухание памяти от мёртвых форков, допуская нормальную обработку блоков вне порядка при P2P-синхронизации.
Переключение форка
Когда узел переключается на другой форк:
pop_block()удаляет текущий головной блок; его транзакции перемещаются в_popped_tx.- Повторять до достижения общего предка.
- Применять блоки нового форка по порядку от общего предка до новой головы.
~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. Когда недостающий родитель наконец прибывает:
_push_block(parent)связывает родителя с цепочкой._push_next(parent)итерирует_unlinked_indexв поиске дочерних блоковparent.- Дочерние блоки перемещаются в
_indexи рекурсивно связываются. fork_db._headможет продвинуться на несколько блоков за один вызов (запускает путь линейного продолжения).
Мягкий бан пиров на основе страйков
Пиры не получают немедленный бан за отправку не связываемых блоков. Счётчик накапливается:
| Путь | Порог | Условие сброса |
|---|---|---|
| Нормальная работа: не связываемый блок на уровне головы или ниже | 20 страйков | Допустимый блок от того же пира |
| Путь синхронизации: общее отклонение блока | 20 страйков | Допустимый блок от того же пира |
| Мёртвый форк / блок слишком старый | Немедленный бан | — |
Честные пиры могут восстановиться после временных ошибок (перезагрузка снимка, гонки таймингов, кратковременные micro-fork).
Тайминг производства блока валидатором
Плагин validator использует таймер с интервалом 250мс и упреждением 250мс:
- Таймер срабатывает каждые 250мс (выровнен по 250мс границам системных часов, минимальный сон 50мс).
maybe_produce_block()вычисляетnow = NTP_time + 250ms.get_slot_at_time(now)находит текущий слот.- Если слот принадлежит настроенному валидатору и
|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) > 0 | not_time_yet |
| Запланированный валидатор в нашем настроенном наборе | not_my_turn |
| Ненулевой ключ подписи on-chain | not_my_turn |
| Приватный ключ для ключа подписи в памяти | no_private_key |
| Участие в сети ≥ порога (до HF12) | low_participation |
| ` | scheduled_time - now |
| Нет конкурирующего блока на той же высоте в fork_db | fork_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 протокол):
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_LIMIT | 200 мс | Максимальное время повторного применения ожидающих транзакций после добавления блока |
CHAIN_BLOCK_GENERATION_POSTPONED_TX_LIMIT | 5 | Максимум последовательных превышающих транзакций, пропускаемых при генерации |
CHAIN_BLOCK_SIZE | 65536 байт | Жёсткий лимит размера блока |
maximum_block_size | Динамический (медиана валидаторов) | Мягкий лимит размера блока |
CHAIN_BLOCK_INTERVAL | 3 с | Интервал производства блоков |
Префиксы отладочных логов
| Префикс | Значение |
|---|---|
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, Разрешение форков, Узел-валидатор.