Сборка блока
Коллатор решает, что пора собирать очередной блок, и готовит рабочее состояние очередной попытки ещё до того, как начинается сама сборка (см. статью «Жизненный цикл коллатора»). У сборки есть чёткий вход и выход. На входе она получает рабочее состояние (данные мастерчейна, предыдущее состояние шарда, состояние чтения сообщений) и отдельно от него кэш якорей, а на выходе отдаёт кандидат блока либо результат отменённой или прерванной попытки. Эта статья описывает устройство самого конвейера сборки: от выноса тела коллации в отдельный поток до финализации собранного блока.
Тело коллации в отдельном пуле потоков
Собственно сборку блока коллатор ведёт не в основном потоке узла: он готовит вход и передаёт всю работу замыканию, запущенному в отдельном пуле потоков. Внутри замыкания нет ни одной точки ожидания — это обычный синхронный код, который занимает поток этого пула целиком, пока блок не будет собран. Так длинная счётная работа не блокирует цикл событий узла. Задачи разбираются в порядке постановки, поэтому раньше запущенная сборка не ждёт, пока освободится место для позже запущенной.
Состояние чтения сообщений и кэш якорей — изменяемые данные, которые до сборки принадлежат коллатору, а замыкание должно быть независимым от остального узла. Поэтому оба переезжают в него по значению, а обратно возвращаются вместе с результатом сборки при любом исходе: успехе, ошибке или отмене. Сразу после этого коллатор ставит кэш якорей себе на место. Оба состояния при этом транзакционны: изменения фиксируются только при удачном исходе и записанном дифе очереди, а на любом другом исходе откатываются. Подробнее об этой транзакции рассказано в статье «Чтение сообщений».
Отменить уже идущую сборку сбросом задачи нельзя: она выполняется вне цикла событий узла. То, как это устроено на уровне флага отмены и в каких именно точках конвейер его проверяет, разобрано в статье «Жизненный цикл коллатора». Для этой статьи важно только то, что отмена — тоже один из исходов замыкания, наравне с успехом и ошибкой, и что она не откатывает работу молча: транзакции состояния читателя и кэша якорей возвращают всё как было.
Три фазы конвейера
Внутри этого пула сборка блока проходит как последовательность трёх фаз: подготовка, исполнение, финализация. Переход к следующей фазе забирает предыдущую целиком во владение, поэтому вернуться на шаг назад нельзя. Каждая фаза передаёт следующей только то, что ей нужно. Подготовка отдаёт исполнению читателя сообщений и собранного исполнителя сообщений, исполнение — финализации сам исполнитель с накопленными в нём аккаунтами и результат своей работы.
Один проход коллации — фиксированная последовательность шагов:
- подчистить кэш якорей выше времени цепочки блока и сверить автора блока;
- фаза подготовки — создать исполнителя сообщений и читателя сообщений;
- только в мастерчейне: тик-транзакции, затем транзакции возврата комиссий и эмиссии;
- исполнить входящие сообщения группами;
- только в мастерчейне: ток-транзакции;
- перейти в фазу финализации и забрать читателя сообщений;
- финализировать читателя сообщений, получить диф очереди с сообщениями и сериализовать диф;
- посчитать новую позицию обработки и длину хвоста дифов очереди;
- параллельно финализировать блок и подготовить диф очереди к записи.
Подготовка блока
Первый шаг сверяет автора блока и подчищает кэш якорей. Время цепочки следующего блока приходит в коллацию снаружи, а кэш якорей может содержать более новые якоря: если шард форсировал сборку мастер-блока, ему неизвестно, сколько якорей мастерчейн уже успел импортировать. Поэтому кэш якорей транзакционно подрезается: с его хвоста удаляются все якоря со временем цепочки строго больше времени блока. Автором блока становится автор последнего оставшегося после подрезки якоря, а если такого нет — самый ранний якорь истории.
Здесь же задаются и другие исходные величины блока, которые дальше конвейер только использует:
- случайное зерно блока — хеш от тройки «шард, seqno, время цепочки»: у любого узла с тем же входом получится то же самое зерно, что обеспечивает воспроизводимость коллации при верификации;
- начальное логическое время блока — максимум из логического времени предшественников (последнего мастер-блока и предыдущего блока своего шарда, а для мастер-блока ещё и топ-блоков шардов), округлённый вверх до кратного шагу выравнивания в миллион логических единиц, а первое время, доступное транзакциям блока, равно этому значению плюс единица;
- время самого блока — время цепочки якоря, разложенное на секунды и миллисекунды. Часы узла в коллации участвуют только в метриках расхождения.
Ещё до исполнения хотя бы одной транзакции заполняется и часть балансового итога блока — то, что не зависит от исхода исполнения: награда за создание блока, а в мастерчейне ещё эмиссия, возврат комиссий валидаторам и сожжённое (сжигание разрешено только в мастерчейне). Возврат комиссий валидаторам происходит не в каждом мастер-блоке. Если в конфигурации блокчейна не задан адрес сборщика комиссий, возврат отключён совсем. Если адрес задан, но сумма накопленных и пришедших из шардов комиссий вместе с наградой за создание блока меньше порога, возврат в этом блоке пропускается, и деньги остаются накапливаться до следующего раза. Порог считается от константы, равной миллиарду нанотокенов, домноженной на текущий коэффициент цены газа мастерчейна: при подорожании газа порог возврата растёт вместе с ним. Эмиссия складывается из двух источников: добора дополнительных валют до целевого объёма обращения по параметру 7 конфигурации блокчейна и разовой эмиссии, назначенной на конкретный seqno мастер-блока параметром 50.
Фаза подготовки собирает исполнителя сообщений. Конфигурация блокчейна из данных мастер-блока разбирается один раз, привязанная ко времени блока, а из неё берётся набор флагов возможностей сети. Исполнителю передаются параметры конструирования исполнителя (случайное зерно, время и логическое время блока, публичные библиотеки мастерчейна, несколько жёстко включённых поведений и ещё несколько, включаемых по флагам возможностей), а также шард, нижняя граница логического времени, аккаунты предыдущего состояния и параметры расчёта единиц работы исполнения. Аккаунты передаются наблюдаемыми: их чтение учитывается деревом обращений к ячейкам предыдущего состояния, и благодаря этому меркл-обновление потом будет знать, какие ячейки состояния блок действительно прочитал.
Исполнение сообщений
Фаза исполнения предоставляет четыре метода, но в блоке шарда из них используется только один: исполнение входящих сообщений группами. В мастерчейне последовательность полная: сначала тик-транзакции служебных контрактов, затем специальные транзакции возврата комиссий и эмиссии, затем группы входящих сообщений и в конце ток-транзакции. Ни тик-, ни ток-, ни специальные транзакции параллельно не исполняются, только последовательно: тик- и ток-транзакции идут по списку адресов, а транзакции возврата и эмиссии исполняются каждая по своему единственному адресу получателя из конфигурации блокчейна.
Тик- и ток-транзакции создаются по списку фундаментальных адресов плюс адрес контракта конфигурации, и только для тех, у кого в специальных флагах состояния выставлен соответствующий флаг: нет аккаунта или нет флага — адрес молча пропускается. Транзакции возврата и эмиссии, наоборот, коллатор создаёт сам: искусственным внутренним сообщением от нулевого адреса мастерчейна на адрес получателя из конфигурации блокчейна (сборщик комиссий или минтер), с суммой, посчитанной ещё на подготовке. Пропуск такой транзакции, в отличие от тик- и ток-транзакций, считается ошибкой коллации: раз сумма ненулевая, транзакция обязана состояться.
Исходящие сообщения каждого из этих шагов сразу отдаются читателю сообщений как новые. Сообщения, порождённые тиком и специальными транзакциями, могут быть исполнены здесь же, в группах этого же блока. А вот сообщения, порождённые ток-транзакциями, в текущий блок уже не попадут: исполнение групп к этому моменту закончено, и они уйдут в диф очереди.
Исполнение входящих сообщений — это цикл: взять у читателя сообщений следующую группу, исполнить её, учесть результат. У цикла три причины штатной остановки, проверяемые в трёх разных точках:
- до взятия группы — логическое время подошло к границе окна блока;
- после попытки взять группу — читатель сообщений сообщил, что сообщений для сбора больше нет;
- после исполнения группы — достигнут жёсткий лимит блока.
Четвёртого штатного варианта нет: если группа не пришла, но читателю ещё есть что читать, цикл идёт на следующую итерацию. А если до взятия группы пришёл сигнал отмены — коллация вообще не выходит из цикла обычным путём, а откатывается целиком.
Проверка по логическому времени защищает окно блока: оно тянется от начального логического времени блока до него же плюс шаг выравнивания в миллион, и группа не должна перешагнуть верхнюю границу окна. Запас считается грубо: константа максимального сдвига логического времени на одно сообщение (256) умножается на число сообщений в слоте группы, и как только текущее логическое время доходит до границы снизу, цикл прекращает брать новые группы. На практике до этой границы почти никогда не доходит — лимиты блока и исчерпание сообщений срабатывают раньше.
Саму группу сообщений исполнитель сообщений исполняет параллельно по аккаунтам-получателям: он берёт группу как карту «аккаунт → его сообщения», проходит по ней в параллель и на каждый аккаунт исполняет его сообщения одно за другим, строго в том порядке, в каком они лежат. Так порядок сообщений на одном аккаунте сохраняется, а зависимость между разными аккаунтами снимается.
При этом один общий счётчик двигает логическое время блока: он инициализируется первым доступным транзакциям значением и после каждой успешно исполненной транзакции только растёт. Перед исполнением конкретной транзакции счётчик ещё раз поднимается по границе своего аккаунта. Так одно и то же логическое время не используется на одном аккаунте повторно. Но все аккаунты одной группы стартуют от одной и той же общей границы и не видят изменений друг друга, поэтому логические времена транзакций разных аккаунтов одной группы могут пересекаться по значениям: порядок гарантирован только внутри аккаунта.
Внешнее сообщение может не дать транзакции на этой фазе не только из-за более ранней просрочки. Срок жизни внешнего сообщения проверяется дважды ещё до исполнения: целым якорем при чтении и поштучно в буфере при сборе группы, по порогу от времени цепочки якоря. Просроченное сообщение до исполнения просто не доходит (подробнее — в статье «Чтение сообщений»). На самой же фазе исполнения причины две:
- пустой аккаунт-получатель: если он пуст, все внешние сообщения его подгруппы пропускаются без обращения к исполнителю транзакций;
- отказ самого исполнителя транзакций: сообщение тоже не попадает в блок, но потраченный на попытку газ всё равно учитывается в единицах работы группы.
Для внутреннего сообщения такого пути нет: отказ исполнителя транзакций, не связанный с внешним сообщением, поднимается наверх как ошибка коллации.
Каждая успешно исполненная транзакция сразу раскладывается по словарям блока. Входящее сообщение попадает в один из видов записи в зависимости от типа сообщения и его происхождения:
- сообщение возврата или эмиссии — немедленное;
- внешнее — отдельный вид записи;
- внутреннее из более раннего блока — финальное, а если оно пришло из своего же шарда, рядом отмечается ещё и его выбытие из очереди;
- внутреннее, порождённое в этом же блоке, заменяет свою прежнюю запись «новое» на «немедленное».
Исходящие сообщения раскладываются проще: внутреннее становится записью «новое» и одновременно уходит в новые сообщения блока. Внешнее исходящее получает отдельный вид записи.
Лимиты блока, о которые в конце концов спотыкается цикл исполнения, устроены не совсем так, как формально называются их параметры. С тройкой границ, прочитанной из конфигурации блокчейна (параметр 22 для мастерчейна, параметр 23 для остальных рабочих цепочек), сравниваются три счётчика: число различных аккаунтов с исполненной транзакцией по входящему сообщению, суммарный газ транзакций, попавших в блок (газ пропущенных внешних сообщений сюда не идёт), число обработанных транзакций и входящих сообщений. Это переопределение унаследованного смысла: параметр, который формально называется предельным размером блока в байтах, в Tycho считает не байты, а число разных аккаунтов, а параметр предельной дельты логического времени считает не логическое время, а число элементов блока. У каждого лимита три уровня — недогрузка, мягкая и жёсткая граница, но в самой коллации запрашивается только жёсткая: группа, на которой лимит перешагнут, исполняется целиком, поэтому жёсткий лимит может быть превышен ровно на одну группу.
Граница с исполнением
Коллатор сам не исполняет транзакции: он передаёт управление отдельному исполнителю транзакций одним вызовом на каждую транзакцию. Для этого он собирает исполнителя с параметрами и конфигурацией блокчейна, задаёт нижнюю границу логического времени и вызывает один из двух методов — исполнение обычной транзакции по сообщению или тик- либо ток-транзакции без сообщения. Успешный результат коллатор тут же фиксирует.
Обратно приходит новое состояние аккаунта, его краткое описание (баланс, библиотеки, признак существования), сама транзакция, её метаданные (комиссии, список исходящих сообщений, следующее логическое время) и сожжённая сумма. Отдельно, через инспектора исполнения, коллатор забирает суммарный израсходованный газ и изменения публичных библиотек: инспектор отдаёт их как есть, в порядке действий, а сворачивает уже коллатор, применяя транзакцию к аккаунту. Из всего результата коллатор оставляет себе только то, что нужно дальше: транзакцию, исходящие сообщения, газ, следующее логическое время и сожжённое.
Фазы транзакции, работа виртуальной машины, расчёт комиссий и газа — уже не тема коллации, а домен исполнения. Коллатор знает только интерфейс вызова и время, которое он занял.
Финализация блока
Прежде чем закрыть читателя сообщений, коллатор подгружает дифы обновившихся топ-блоков чужих шардов: для мастер-блока это топ-блоки шардов, которые в него входят, для блока шарда — топ-блоки всех остальных шардов, отмеченных в мастерчейне как обновившиеся, плюс сам последний мастер-блок. По этим данным читатель сообщений решает, каких получателей перевести в низкоприоритетную партицию (см. статью «Чтение сообщений»). Отсутствие дифа хотя бы одного из них коллацию не роняет, а штатно прерывает. Дальше читатель сообщений закрывается и отдаёт диф очереди с сообщениями и действующие параметры исполнения сообщений. Диф сериализуется, но в очередь пока не пишется.
С этого момента конвейер делает две независимые тяжёлые работы одновременно: финализирует сам блок и готовит диф очереди к записи, считая его статистику и проверяя незакоммиченную часть очереди. Диф очереди записывается уже за пределами тела коллации и только если вся коллация удалась и не была отменена. Провал записи откатывает и состояние чтения, и кэш якорей.
Финализация блока сама раскладывается на параллельные ветви. На верхнем уровне это словарь входящих сообщений, словарь исходящих сообщений и обновление аккаунтов — самая тяжёлая из них. Оба словаря сообщений строятся из уже отсортированных данных и поэтому не требуют досортировки.
Обновление аккаунтов режется ещё на уровень глубже: словарь аккаунтов делится по глубине из конфигурации узла на «виртуальные шарды» по префиксу адреса, каждый кусок правится в своём потоке, а куски сливаются обратно в один словарь. Для мастерчейна такой резки нет вовсе, там глубина принудительно нулевая. На этом же проходе, но только на мастерчейне, если среди изменённых аккаунтов оказался контракт конфигурации, из него разбираются новые параметры конфигурации блокчейна. Там же дифы публичных библиотек всех аккаунтов сливаются в один общий диф.
Для мастер-блока в финализации дополнительно собирается часть нового состояния мастерчейна: новая конфигурация блокчейна, если контракт конфигурации изменился, переопределение генезиса мемпула, если оно новее сохранённого, описания шардов и их комиссии, обновление ссылки на последний ключевой блок и обновление глобального баланса. Блок объявляется ключевым именно на этом шаге, если изменилась конфигурация или обновился генезис мемпула.
На этом же шаге, но только в ключевом блоке, считается ещё и расписание смены набора валидаторов. Его правила — тема верификации и ротации валидаторов, не сборки блока.
Переход состояния «старое → новое» кладётся в блок как меркл-обновление. Оно строится по дереву обращений, которое всю коллацию записывало, какие ячейки предыдущего состояния были прочитаны. Для параллельной сборки словарь аккаунтов обеих сторон тоже режется, но отдельной глубиной — по умолчанию на единицу большей, чем глубина резки при обновлении аккаунтов.
Собранный блок сразу сериализуется в BOC переиспользуемым буфером: заголовок берётся из общего на коллатор кэша, а сама запись сериализации идёт параллельно. Идентификатор блока появляется уже из результата — root hash как хеш корневой ячейки, file hash как хеш сериализованных байтов.
Балансовый итог блока досчитывается до полного уже после сборки словарей аккаунтов и сообщений. Если в конфигурации узла включена дополнительная проверка, его можно на месте сверить набором простых правил, например «ненулевой возврат комиссий требует транзакции возврата» или «объявленная награда за создание совпадает с ожидаемой по конфигурации». Проверка дублирует уже сделанную работу и стоит времени на каждом блоке, поэтому её место — отладка и расследование расхождений, а не постоянная работа узла.
Кандидат блока
Результат сборки упаковывается в кандидат блока — сам блок вместе с дифом очереди, позициями обработки, временем цепочки и служебными метаданными. Кандидат уходит менеджеру коллации вместе с идентификаторами предыдущих блоков и топ-блоков шардов, автором, полным балансовым итогом, обработанным якорем и информацией о консенсусе, а также признаками того, что блок ключевой и что в ключевом мастер-блоке изменилась конфигурация консенсуса.
Номер мастер-блока, на который будет ссылаться этот блок, у мастер-блока равен его собственному seqno, а у блока шарда — seqno ещё не собранного следующего мастер-блока. Информация о консенсусе берётся из состояния до блока: ключевой блок может обновить расписание сессии, а подписи под самим блоком относятся к сессии, которая его подписывала.
Единицы работы и возврат в цикл событий узла
В конце финализации коллатор складывает единицы работы блока: накопленные с последнего якоря, потраченные на набор групп сообщений, на само исполнение и на финализацию. Сумма не растёт бесконечно: она обрезается потолком, привязанным к допустимому отставанию обработки якорей от консенсуса. Поэтому один тяжёлый блок не может накопить единиц работы больше, чем нужно на импорт всех якорей, которые коллатор вообще имеет право не обработать, плюс один запас. Модель расчёта единиц работы — тема отдельной статьи. Здесь фиксируется только точка, где посчитанные величины складываются и ограничиваются.
На этом тело коллации в пуле потоков завершается. Дальше, уже в асинхронной части, снова в цикле событий узла, коллатор запускает сохранение нового состояния в фоновую задачу: адаптер состояния узла получает предыдущий и новый идентификаторы блока, метаданные, меркл-обновление, само состояние и подсказку с размером блока и числом новых ячеек. Коллатор собирает результат сборки для менеджера коллации и запускает подготовку рабочего состояния следующей попытки — устройство этого шага разобрано в статье «Жизненный цикл коллатора». Данные коллации, которые могут быть очень большими, уходят на уничтожение в фоновом потоке: так освобождение памяти не отнимает время у цикла событий узла.
Значения по умолчанию
Сборка блока читает часть полей секции collator конфигурации узла. При пропуске поля оно принимает значение по умолчанию.
| Параметр | Значение по умолчанию |
|---|---|
accounts_split_depth | 4 |
merkle_split_depth | 5 |
check_value_flow | false |
accounts_split_depth задаёт глубину резки словаря аккаунтов при обновлении аккаунтов в финализации, merkle_split_depth — ту же глубину при построении меркл-обновления состояния. check_value_flow включает дополнительную проверку балансового итога собранного блока.
Чтение сообщений
Как коллатор набирает группы сообщений из буферов, помнит между блоками место чтения, дозаполняет буферы после перезапуска и переводит перегруженные аккаунты в низкоприоритетную партицию.
Финализация блока
Внутреннее устройство финализации блока в коллаторе - вложенные уровни параллелизма, меркл-обновление состояния, сборка дополнительной части состояния мастерчейна для ключевого блока и расписание смены набора валидаторов.