Обновление схемы данных и миграции¶
Раздел предназначен для планирования релизов смарт-контракта, в которых меняется формат хранимых данных или параметров действий. Он задаёт допустимые приёмы и типичные ошибки, чтобы выкатка нового 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 |
Если сомневаетесь — считайте, что любое изменение уже существующих полей требует либо очистки данных, либо отдельной процедуры переноса (вторая таблица).
С чего начать при планировании релиза¶
- Нужны ли старые строки? Если нет — проще всего очистить таблицу или выкатить логику на новый аккаунт.
- Если нужны — выберите стратегию: «хвост опционален» (
binary_extension), «одно поле — несколько форматов во времени» (variant), или «копирование в новую таблицу». - ABI обновите вместе с кодом; имеет смысл сравнивать старый и новый ABI инструментом
cdt-abidiff, чтобы не пропустить расхождение с клиентами.
Стратегия 1: можно обнулить или перенести данные¶
Подходит для тестовых сетей, черновых контрактов или когда пользователи заранее согласны потерять содержимое таблицы.
- Удалить все строки проблемной таблицы или перенести контракт на новый аккаунт без старых данных.
- Задеплоить 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<…>>, чтобы старые короткие строки по-прежнему читались.
Не стоит вкладывать binary_extension внутрь списка альтернатив variant, если нет жёсткой необходимости и глубокого разбора схемы — высокий риск рассинхрона ABI и фактических байтов.
Стратегия 4: вторая таблица и перенос строк¶
Нужна, когда «просто дописать хвост» недостаточно: меняется первичный ключ, ломается порядок полей, нужна другая модель индексов и т.д.
Вариант без длительного простоя: в коде одновременно объявляются старая и новая таблицы; при чтении или отдельными действиями строки переписываются из старой в новую, после чего старая запись удаляется. Пока перенос не завершён, логика должна учитывать оба хранилища.
Вариант с окном обслуживания: выкатывается версия контракта, которая только гоняет миграцию (пакетами, с учётом лимита времени транзакции), затем финальный деплой без старой таблицы.
Любая такая схема требует продуманного порядка деплоя и, при необходимости, коммуникации с пользователями API.
Связь с ABI и клиентами¶
Строки таблиц и аргументы действий описаны в ABI. После изменения структур пересоберите ABI и проверьте клиенты (кошелёк, SDK, индексаторы). Иначе они будут слать JSON со старыми полями или читать таблицу по устаревшей схеме.
Полезные материалы:
- Таблицы состояния — как объявлять и наполнять таблицы.
- ABI контракта — секции
structs,tables, генерация. - Рекомендации — имена и ресурсы.
Пошаговые сценарии с cleos set contract и примерами исходников — в репозитории CDT; перед продакшеном воспроизведите миграцию на тестовой сети с тем же лимитом транзакций, что и в бою.