Обновление кода смарт контрактов и схемы хранения с помощью EIP-2535 “Diamonds”

Данная статья является переводом: https://link.medium.com/KyzboqVXlsb

bubbalex
7 min readAug 10, 2022

Введение

Код смарт-контракта Ethereum неизменяем: его нельзя изменить после развертывания. Это сочетается с доверительной природой блокчейна, но затрудняет разработку и обслуживание смарт-контрактов. Кроме того, размер кода смарт-контракта ограничен 24 КБ.

Помимо кода, каждый смарт-контракт имеет собственное внутреннее хранилище — массив 256-битных слотов. Solidity, самый популярный язык программирования смарт-контрактов Ethereum, назначает переменные состояния слотам хранения и заботится о низкоуровневых деталях. Однако существует множество случаев, когда разработчик может захотеть изменить расположение хранилища после развертывания, например, добавить переменную или оптимизировать операции чтения/записи в хранилище.

Для смягчения этих проблем в EIP-2535 предлагается сложная схема прокси, которая позволяет добавлять, заменять и удалять функции и практически устраняет любые ограничения на размер кода; а также инновационная схема хранения, устойчивая к столкновениям и очень гибкая.

В следующих разделах мы рассмотрим основы EIP-2535 и то, как он позволяет модернизировать код. Затем мы рассмотрим, как он позволяет обновлять компоновку хранилища. В конце я представлю некоторые предостережения, связанные с задачей обновления динамических полей, а также предложу способы их устранения.

EIP-2535 и функции обновления

Хотя смарт-контракты имеют неизменяемый код, они могут вызывать функции из других смарт-контрактов. В частности, они могут делать это с сохранением контекста хранения с помощью вызовов делегатов. Эта возможность является основополагающей для создания продвинутых прокси-контрактов.

Прокси-контракт, предложенный в EIP-2535, а именно Diamond, имеет очень мало кода и является полностью универсальным. Между тем, все специфические для приложения функции реализуются контрактами, называемыми Facets. При построении Diamond раскрывает только две функции: функцию diamondCut, которая позволяет владельцу Diamond добавлять, заменять и/или удалять реализации функций, и функцию fallback, которая динамически перенаправляет вызовы функций на соответствующие фасеты.

EIP также определяет интерфейс под названием Diamond Loupe, который позволяет проводить интроспекцию функций, которые могут быть вызваны через функцию fallback. Это может быть полезно для разработчиков при тестировании обновлений и для пользователей, чтобы сделать функциональность Diamond прозрачной. Более того, возможность обновления является необязательной функцией, так как функция diamondCut может быть удалена в любое время.

EIP-2535 также предлагает схему хранения данных под названием Diamond Storage, которая группирует переменные состояния в структуры. Чтобы избежать коллизий, эти структуры хранятся в местах, заданных хэшами очень описательных строк (например, mytoken.diamond.storage). Такая компоновка стала возможной только после того, как в Solidity 0.6.4 было разрешено ссылаться на структуры в произвольных местах хранения.

Функция diamondCut также позволяет владельцу Diamond указать дополнительный вызов функции, который будет делегирован после добавления, замены и/или удаления реализаций функций. Согласно спецификации, “это выполнение выполняется для инициализации данных, настройки или удаления чего-либо необходимого или более не нужного”.

Photo by wu yi on Unsplash

Модернизация схем хранения

EIP-2535 позволяет обновлять функции довольно простым способом, но мало что сказано об обновлении схем хранения. Чтобы привести пример того, что мы имеем в виду, предположим, что мы развертываем контракт, который принимает ромбовидную схему и использует следующую структуру.

struct DiamondStorage1 {
uint32 ts;
uint32 a;
uint64 b;
uint128 c;
mapping (address => uint32) m;
}

Обратите внимание, что ts, a, b и c плотно упакованы в один и тот же 256-битный слот памяти. Предположим, что ts хранит метку времени последнего взаимодействия с контрактом. Мы знаем, что беззнаковое 32-битное целое число может хранить временные метки UNIX только примерно до 2106 года. Поэтому, если бы мы хотели отложить это событие на миллиарды лет, мы могли бы представить ts, например, 64 битами. Учитывая это, обратно совместимая структура выглядела бы следующим образом.

struct DiamondStorage2 {
uint32 _ts; // deprecated
uint32 a;
uint64 b;
uint128 c;
mapping (address => uint32) m;
uint64 ts; // new field!
}

Важно отметить, что мы решили перенести ts в конец структуры, а не расширять ее на месте, потому что это может изменить место хранения некоторых полей, следующих за _ts. В частности, если отображение m будет перемещено в другое место хранения, то каждая запись m[k] тоже будет перемещена. Чтобы инициализировать новое поле ts, нам нужно сначала развернуть контракт со следующей функцией. Предположим, что diamondStorage возвращает указатель на структуру в правильном месте хранения.

function upgrade() external {
DiamondStorage2 storage ds2 = diamondStorage();
ds2.ts = uint64(ds2._ts); // expand `ts` in a new field
}

Нам также необходимо заменить функции, которые ссылаются на устаревшее поле (uint32 _ts), на более новые версии, которые ссылаются на новое поле (uint64 ts). Однако если есть функции, которые из-за _ts передают параметр или возвращаемое значение типа uint32, мы можем сохранить обратную совместимость с контрактами, которые их вызывают.

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

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

Следующий список помогает упорядочить некоторые типы обновлений, которые возможны с помощью EIP-2535, с учетом ограничений масштабируемости Ethereum.

  • Добавление функций
    Укажите селекторы функций для добавления и адрес фасета.
  • Удаление функций
    Укажите селекторы функций для удаления.
  • Замена адреса фасета функций
    Укажите селекторы функций и адрес нового фасета.
  • Добавление полей
    Добавьте поля в конец какой-либо существующей структуры или создайте с ними новую структуру.
  • Удаление полей
    Удалите поля. Если в структуре нет динамических полей или динамические поля достаточно малы, можно также создать новую структуру без таких полей и скопировать оставшиеся поля.
  • Изменение типа статических полей
    (например, с 32-битных на 64-битные целые числа).

    Уничтожьте старое поле, добавьте новое в конец структуры и перепишите содержимое старого поля в новое.
  • Изменение типа элементов массива
    (например, с 32-битных на 64-битные целочисленные массивы)

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

    Если количество записей отображения достаточно мало, удалите старое поле, добавьте новое в конец структуры и скопируйте отображение в новое место элемент за элементом.
Photo by Dawn McDonald on Unsplash

Предостережения и ограничения

Операции над динамическими полями не всегда можно выполнить в одной транзакции из-за ограничения на газ в блоке. Чтобы представить это в перспективе, слишком оптимистичная верхняя граница для количества записей в хранилище до превышения лимита газа в блоке составляет 1500. Возможным смягчением может быть разделение задачи на несколько независимых транзакций, каждая из которых не превышает лимит газа в блоке.

Если динамическое поле не изменяется никакими функциями, то обновление не составит труда. Например, допустим, мы хотим изменить поле отображения m из предыдущего примера. Мы можем обесценить его и добавить новое поле в конец структуры, как в следующем фрагменте кода:

struct DiamondStorage3 {
uint32 _ts;
uint32 a;
uint64 b;
uint128 c;
mapping (address => uint32) _m; // deprecated
uint64 ts;
mapping (address => uint256) m; // new field!
}

А затем мы можем преобразовывать записи отображения постепенно, не влияя на поведение контракта, который учитывает только старые поля. Чтобы иметь возможность разделить обновление на несколько транзакций, мы разбиваем набор ключей, для которых в связке есть допустимая запись, на несколько несовпадающих подмножеств. Чтобы дать некоторую мотивацию этой модернизации, предположим, что мы хотим умножить каждое значение на некоторую константу c.

function upgrade(address[] calldata keys, uint256 c) external {
DiamondStorage3 storage ds3 = diamondStorage();
for (uint i; i < keys.length; ++i) {
address key = keys[i];
ds3.m[key] = c * uint256(ds3._m[key]);
}
}

После инициализации нового поля отображения m всеми допустимыми ключами, мы можем заменить функции, ссылающиеся на старое поле, новыми версиями, ссылающимися на новое поле.

С другой стороны, если обновляемое динамическое поле может быть изменено пользователями, мы хотим избежать параллелизма чтения и записи. Дешевым и грязным решением было бы удалить функции, ссылающиеся на отображение, внести все необходимые изменения, а затем добавить новые версии функций. Однако это нарушит контракты, которые зависят от Diamond во время обновления, и даже может быть использовано злонамеренным владельцем Diamond в качестве DoS-атаки на такие контракты.

Заключение

EIP-2535 позволяет добавлять, удалять и заменять реализации функций, а также выполнять произвольный код на контексте хранения Diamond после каждого обновления. Это позволяет обновлять код и схему хранения неограниченно и гибко.

Однако при обновлении динамических полей необходимо учитывать некоторые предостережения, поскольку их размер может быть произвольно большим, и их обход может превысить лимит газа в блоке. Одним из возможных способов смягчения является разделение задачи обновления динамического поля на несколько транзакций, каждая из которых потребляет меньше лимита. Эта техника отлично работает для полей, которые не могут быть изменены пользователями. Однако в обратном случае владелец Diamond вынужден запретить пользователям изменять такие поля во время обновления, что может привести к временному разрыву контрактов и приложений, зависящих от Diamond.

О Cartesi

Блокчейн ОС — это децентрализованная инфраструктура второго уровня, которая поддерживает Linux и основные компоненты программного обеспечения. Впервые разработчики могут создавать масштабируемые смарт-контракты с помощью богатых программных инструментов, библиотек и привычных сервисов, преодолевая разрыв между обычным программным обеспечением и блокчейном.

Cartesi позволяет миллионам новых стартапов и их разработчикам использовать The Blockchain OS и внедрять приложения Linux. Благодаря революционной виртуальной машине, optimistic rollups и боковым цепочкам, Cartesi открывает путь для разработчиков всех видов, чтобы создать следующее поколение приложений для блокчейна.

Добро пожаловать в The Blockchain OS, дом для того, что будет дальше.

Ссылки на Cartesi

Telegram Анонсы | Telegram | Discord (сообщество разработчиков)| Reddit | Twitter | Facebook | Instagram | Youtube | Github | Cartesi Improvement Proposal (CIP) | Сайт

От автора перевода

Если вам понравился данный перевод и вы хотите поддержать нас вы можете поделиться им со своими друзьями и подписаться на наши соц. сети. Спасибо за поддержку!

Fiat is a Bubble

Telegram Канал | Telegram Сообщество | Medium | Twitter

--

--

bubbalex

Graphic designer, blockchain enthusiast and Cartesi Ambassador