Перейти к содержанию

Обновление схемы данных и миграции

Раздел предназначен для планирования релизов смарт-контракта, в которых меняется формат хранимых данных или параметров действий. Он задаёт допустимые приёмы и типичные ошибки, чтобы выкатка нового WASM не привела к порче уже существующего состояния в сети.

В зоне внимания: совместимость бинарного представления строк таблиц и аргументов действий с новой версией кода и ABI.
Вне раздела: резервное копирование узла, репликация SHIP, миграции внешних баз и прочая инфраструктура вокруг цепочки.


Предмет: какие данные подлежат согласованию с новой версией контракта

Контракт в модели EOSIO/COOPOS накапливает состояние — данные, которые переживают отдельный вызов действия. Их обычно размещают в таблицах: в коде это контейнеры eosio::multi_index и при необходимости eosio::singleton. Узел сохраняет строки в глобальном состоянии цепочки; учёт ресурсов чаще всего ведёт через RAM, но для понимания миграций важен не биллинг сам по себе, а то, что запись остаётся в хранилище состояния до явного изменения или удаления.

С точки зрения узла каждая строка таблицы — последовательность байтов, полученная при сериализации структуры с атрибутом [[eosio::table]] по правилам, согласованным с ABI контракта. При следующем действии WASM снова десериализует эти байты в поля C++.

Публикация обновлённого контракта заменяет исполняемый модуль (WASM). Содержимое таблиц автоматически не перекодируется: в цепочке по-прежнему лежат байты, записанные предыдущими версиями. Новый код обязан либо читать их в прежнем формате, либо выполнить описанную в этом разделе миграцию (включая очистку или перенос), либо оформить расширение так, чтобы старые короткие записи оставались валидны (например, опциональный хвост в binary_extension). Иначе возможны ошибки разбора, некорректные значения или нестабильное поведение без явного assert.

Миграция схемы в смысле данного документа — это проектное решение и набор шагов деплоя, обеспечивающие согласованность уже лежащих в состоянии цепочки байтов с новой структурой строки в исходниках и с обновлённым ABI для клиентов.


Базовое правило (без него всё ломается)

Сериализация идёт в порядке полей в структуре. Старые строки в цепочке — это «снимок» этого порядка на момент записи.

Допустимо без специальных приёмов Недопустимо без миграции или очистки таблицы
Удалить таблицу / все строки и задеплоить новую структуру Поменять местами поля
Задеплоить новый контракт на другой аккаунт с пустыми таблицами Вставить новое поле между существующими
Добавить новое поле только в конец и оформить его как binary_extension (см. ниже) Изменить тип или смысл уже существующего поля «на том же месте» (было uint32, стало name и т.п.)
Расширять std::variant только добавлением новых вариантов в конец списка Удалять или переставлять варианты в уже развёрнутом variant

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


С чего начать при планировании релиза

  1. Нужны ли старые строки? Если нет — проще всего очистить таблицу или выкатить логику на новый аккаунт.
  2. Если нужны — выберите стратегию: «хвост опционален» (binary_extension), «одно поле — несколько форматов во времени» (variant), или «копирование в новую таблицу».
  3. ABI обновите вместе с кодом; имеет смысл сравнивать старый и новый ABI инструментом cdt-abidiff, чтобы не пропустить расхождение с клиентами.

Стратегия 1: можно обнулить или перенести данные

Подходит для тестовых сетей, черновых контрактов или когда пользователи заранее согласны потерять содержимое таблицы.

  1. Удалить все строки проблемной таблицы или перенести контракт на новый аккаунт без старых данных.
  2. Задеплоить WASM с новой структурой.

Не путать с «просто задеплоил новый wasm»: если таблица не пуста, старые байты останутся.


Стратегия 2: сохранить строки — расширение в конец (binary_extension)

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

Когда использовать: добавили одно или несколько новых полей в конец строки таблицы или добавили новый аргумент в конец списка параметров действия.

Как делать:

  • Новое поле — последнее в struct (или новый параметр — последний в сигнатуре действия).
  • Тип — eosio::binary_extension<T> (не вставлять «в середину»).
  • В ABI у такого поля тип помечается суффиксом $ (например "uint64$"): клиентам это сигнал, что значение может отсутствовать в старых данных.

В коде проверяйте наличие: if (поле) { … поле.value() … }.

Пример — новый параметр действия:

// Было:
[[eosio::action]] void regpkey(eosio::name primary_key);

// Стало: второй параметр только в конце и в binary_extension
[[eosio::action]] void regpkey(
   eosio::name primary_key,
   eosio::binary_extension<eosio::name> secondary_key);

Ограничения: не усложняйте вложенность (массивы, наследование, variant внутри variant без понимания сериализации). Детали — в binary_extension.hpp вашей версии CDT.


Стратегия 3: одно поле — несколько форматов (std::variant)

Идея: в одной ячейке строки со временем может лежать значение разного типа; номер активного варианта и полезная нагрузка кодируются по правилам ABI.

Новая таблица с нуля — можно объявить поле как std::variant<…>. Позже расширять только конец списка типов (добавлять новые варианты в конец), не переставляя и не удаляя старые.

Таблица уже в проде — новое «поле-вариант» обычно добавляют как последнее поле строки в виде eosio::binary_extension<std::variant<…>>, чтобы старые короткие строки по-прежнему читались.

eosio::binary_extension<std::variant<int8_t, uint16_t, uint32_t>> payload;

Не стоит вкладывать binary_extension внутрь списка альтернатив variant, если нет жёсткой необходимости и глубокого разбора схемы — высокий риск рассинхрона ABI и фактических байтов.


Стратегия 4: вторая таблица и перенос строк

Нужна, когда «просто дописать хвост» недостаточно: меняется первичный ключ, ломается порядок полей, нужна другая модель индексов и т.д.

Вариант без длительного простоя: в коде одновременно объявляются старая и новая таблицы; при чтении или отдельными действиями строки переписываются из старой в новую, после чего старая запись удаляется. Пока перенос не завершён, логика должна учитывать оба хранилища.

Вариант с окном обслуживания: выкатывается версия контракта, которая только гоняет миграцию (пакетами, с учётом лимита времени транзакции), затем финальный деплой без старой таблицы.

Любая такая схема требует продуманного порядка деплоя и, при необходимости, коммуникации с пользователями API.


Связь с ABI и клиентами

Строки таблиц и аргументы действий описаны в ABI. После изменения структур пересоберите ABI и проверьте клиенты (кошелёк, SDK, индексаторы). Иначе они будут слать JSON со старыми полями или читать таблицу по устаревшей схеме.

Полезные материалы:

Пошаговые сценарии с cleos set contract и примерами исходников — в репозитории CDT; перед продакшеном воспроизведите миграцию на тестовой сети с тем же лимитом транзакций, что и в бою.