Хранение данных

Состояние узла

NodeStateStorage — шесть фиксированных значений в одной колонке базы CoreDb: последний и начальный мастер-блок, идентификатор и доказательство нулевого состояния, идентификатор экземпляра хранилища, отметка о незавершённом сохранении устойчивого состояния. Решение о холодном или тёплом старте, состояние синхронизации узла и наблюдение за последним мастер-блоком через канал уведомлений.

Раздел показал, как узел хранит блоки, деревья ячеек и снимки состояния — данные сети как таковой. Кроме этого узлу нужно помнить о себе самом: где он сейчас остановился в цепочке, с какой точки начал синхронизацию и к какой именно сети принадлежит его база.

За это отвечает хранилище состояния узла: плоский набор из шести фиксированных значений без вспомогательных структур.

Шесть значений в одной колонке

Хранилище состояния узла держит все свои данные в колонке 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 полностью переиндексировать аккаунты мастерчейна и всех шардов, прежде чем записать себе новый идентификатор.