Состояние узла
Раздел показал, как узел хранит блоки, деревья ячеек и снимки состояния — данные сети как таковой. Кроме этого узлу нужно помнить о себе самом: где он сейчас остановился в цепочке, с какой точки начал синхронизацию и к какой именно сети принадлежит его база.
За это отвечает хранилище состояния узла: плоский набор из шести фиксированных значений без вспомогательных структур.
Шесть значений в одной колонке
Хранилище состояния узла держит все свои данные в колонке state базы CoreDb, под ключами-константами:
| Ключ | Что хранит |
|---|---|
last_mc_block | последний зафиксированный мастер-блок |
init_mc_block | мастер-блок, с которого узел начал синхронизацию |
instance_id | идентификатор экземпляра хранилища |
zerostate_id | идентификатор нулевого состояния |
zerostate_proof | доказательство нулевого состояния в виде BOC |
pending_persistent_state | отметка о незавершённом сохранении устойчивого состояния |
Других таблиц, файлов или индексов у этого хранилища нет: чтения и записи — точечные операции по перечисленным ключам. Объект создаётся один раз при открытии хранилища узла и оттуда раздаётся всем потребителям.
Последний мастер-блок
Только фиксация очередного мастер-блока модулем продвижения по блокам и однократная запись в конце холодного старта, когда выбран блок, с которого узел начинает работу, двигают значение last_mc_block вперёд. Шардовые блоки на это значение не влияют вовсе: их фиксация лишь помечает собственный дескриптор блока.
У ключа нет метода «удалить» или «откатить назад» — значение только перезаписывается. Когда модуль продвижения по блокам читает последний мастер-блок для собственного состояния, а ключ ещё ни разу не был записан, чтение подменяет его идентификатором нулевого состояния. Для узла без собственной истории это означает, что в этой точке он стоит на нулевом состоянии. Другие читатели ключа реагируют на его отсутствие иначе, как описано ниже в разделах о холодном старте и состоянии синхронизации.
Холодный или тёплый старт
Наличие last_mc_block — единственный признак, по которому узел на запуске решает, тёплый перед ним старт или холодный:
- значение есть — холодный старт не запускается вовсе, узел сразу продолжает работу с этого мастер-блока;
- значения нет — узел проходит холодный старт, и уже его результат становится точкой продолжения.
В обоих случаях узел получает один и тот же ответ на вопрос «с какого блока начинать», просто разными путями.
Наблюдение за последним мастер-блоком
Хранилище не просто отдаёт последний мастер-блок по запросу — на него можно подписаться. Канал создаётся при открытии хранилища и сразу заполняется тем значением, что уже лежит в базе. Поэтому подписчик, взявший приёмник в любой момент жизни узла, увидит в нём актуальное значение.
Приёмник по своей природе не поднимает уведомление на уже увиденное текущее значение, поэтому тот, кому важно именно стартовое состояние, обязан прочитать его сам сразу после получения приёмника, а не ждать первого изменения.
Порядок записи фиксирован: сначала значение попадает в базу, и только потом об этом узнают подписчики. Уведомление уходит на каждую запись, включая повторную запись того же идентификатора. Это единственный ключ хранилища состояния узла, который сам уведомляет о новом значении: остальные пять можно только запросить.
Начальная точка синхронизации
В отличие от last_mc_block, ключ init_mc_block пишется только во время холодного старта и после него не двигается никогда — это нижняя граница истории, с которой узел начинал.
Сразу после подготовки нулевого состояния узел безусловно записывает его идентификатор. Так ключ гарантированно оказывается заполнен, даже если дальше что-то пойдёт не так. Затем, пока холодный старт скачивает цепочку ключевых блоков, значение переписывается на каждом проверенном и признанном устойчивым ключевом блоке — в итоге в ключе остаётся тот устойчивый ключевой блок, с которого узел фактически начал синхронизацию.
Состояние синхронизации узла
Сравнение init_mc_block и last_mc_block даёт состояние синхронизации узла — вычисляемый признак того, где узел находится относительно собственной начальной точки. Значение не хранится отдельно, а пересчитывается заново при каждом обращении:
- seqno обоих значений совпадают — узел стоит ровно на той точке, с которой начал, и по блокам ещё не двигался;
- seqno последнего мастер-блока больше начального — узел уже применял блоки после начальной точки;
- любого из двух ключей нет либо последний оказался меньше начального — состояние синхронизации не определено.
Перечисление состояний синхронизации содержит только два варианта. Третья строка — это не третий вариант перечисления, а отсутствие значения: на уровне API не различить, вызвано ли оно тем, что одного из ключей нет, или тем, что последний мастер-блок оказался меньше начального. Второй случай в норме недостижим: last_mc_block двигается только вперёд от начальной точки, а init_mc_block после холодного старта не меняется никогда. Состояние синхронизации не отвечает на вопрос, догнал ли узел сеть: сравнение идёт только с собственной начальной точкой узла, а не с текущей головой цепочки.
Нулевое состояние сети
Идентификатор нулевого состояния и его доказательство пишутся одной атомарной пачкой: оба ключа сразу, либо ни одного. zerostate_id занимает 68 байт — seqno, затем root hash и file hash, те же поля, что задают нулевое состояние в глобальной конфигурации сети. zerostate_proof хранит то же доказательство, закодированное в BOC.
Записанный идентификатор служит проверкой, что узел работает над той сетью, для которой создана его база. При холодном старте сначала seqno начальной точки узла (init_mc_block, а если его нет — идентификатор нулевого состояния из конфигурации) обязан быть не меньше seqno нулевого состояния из глобальной конфигурации, иначе старая база не может быть переиспользована после хардфорка. Затем, если в базе уже записан zerostate_id, он обязан в точности совпадать с нулевым состоянием из той же конфигурации, иначе это чужая сеть на той же базе. Обе проверки происходят в одном месте кода холодного старта и останавливают запуск, не трогая данные: молчаливого «переиспользуем что есть» здесь нет.
Доказательство нулевого состояния нужно узлам, которые стартовали с устойчивого состояния и не хранят ячейки нулевого состояния целиком: оно служит им якорем доверия при проверке цепочки ключевых блоков и подменяет собой само нулевое состояние там, где до него не добраться. Из доказательства можно собрать виртуальное состояние с конфигурацией сети и списком шардов, не имея полного набора его ячеек.
Хранилище отдаёт доказательство готовой ячейкой и исходными байтами BOC. Им пользуются, в частности:
- проверка доказательств блоков, когда предыдущий ключевой блок проверяемого мастер-блока и есть нулевое состояние;
- собственный запуск RPC-состояния, когда ближайший предыдущий ключевой блок оказался нулевым состоянием, а данных блока ещё нет;
- раздача по сети, где узел просто отдаёт сохранённые байты в ответ на запрос.
Симметрично на холодном старте узел может скачать это же доказательство у другого узла, проверить его против root hash из глобальной конфигурации и записать себе, а затем синхронизироваться дальше, вообще не имея полного нулевого состояния.
Идентификатор экземпляра хранилища
instance_id — 16 случайных байт, которые узел выписывает себе один раз, при первом создании базы, и больше никогда не меняет: метода перезаписи или сброса у этого ключа нет. Значение живёт столько же, сколько сам каталог базы: новый идентификатор появляется ровно тогда, когда база создана заново, будь то первый запуск узла или запуск после удаления данных. Прочитать его можно только тогда, когда ключ уже есть: хранилище считает его присутствие гарантированным и не предусматривает случая, когда базы с идентификатором ещё нет.
Идентификатор не связан ни с ключом узла, ни с его сетевым адресом: это метка не личности узла в сети, а конкретного экземпляра его локального хранилища. Через неё внешние индексы, построенные поверх данных узла, обнаруживают, что база под ними пересоздана. Так, RPC-индекс аккаунтов держит собственную копию идентификатора и на старте сравнивает её со значением из состояния узла. Расхождение однозначно говорит, что данные узла заново залиты (например, из устойчивого состояния), и заставляет RPC полностью переиндексировать аккаунты мастерчейна и всех шардов, прежде чем записать себе новый идентификатор.
Устойчивые состояния
Файловое хранилище снимков состояния на ключевых блоках — два вида (состояние шарда, состояние очереди), каталоги-поколения, критерий устойчивости ключевого блока и отсрочка пригодности для загрузки, сохранение и переиспользование готового файла, ротация каталогов и раздача чанками другим узлам сети.
Сборка мусора
Как хранилище узла избавляется от устаревших данных — три независимых фоновых процесса для архивов, блоков и состояний шардов, у каждого своя граница отсечения и свой момент срабатывания, ручной запуск через управляющий сокет и защита блоков и состояний, ещё нужных для сборки устойчивых состояний, от преждевременного удаления.