EVM часть 4: хранение mapping

В предыдущей статье мы оптимизировали контракт с одним полем. Это боле было типа unit. Тип uint можно считать примитивным. Он имеет конечную длину — 32 байта. Его легко записать и считать в  storage. Однако, как мы знаем, в смарт-контракта встречаются и более сложные типы. Например, ассоциативный массив или mapping. И, чтобы уметь оптимизировать реальный смарт-контракты, нам потребуется понять, как работать c mapping. Для этого нам потребуется ответить на три вопроса:

  1. Как создавать mapping
  2. Как записывать
  3. Как считывать

Для того, чтобы разобраться во всем, мы возьмем маппинг простых типов. А именно: mapping(address -> uint).

Предыдущая статья. Следующая статья.

Полный список статей по теме тут.

Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.

Как в теории работает mapping

Каждый контракт в EVM имеет свой storage, который может адресовать до 2 в 256 слотов размером в 32 байта. Это невообразимо огромное пространство. Естественно, физически такого объема нет. EVM выделяется слоты по требованию. Если слот пустой, Solidity возвращает нули. Если вы попытаетесь что-то записать в пустой слот, то только тогда Solidity реально выделит физическую память. И только за это выделение вы заплатите газ.

Для любого маппинга Solidity обязывает делать пустой слот, так называемый маркер-слот. Мы просто запоминаем, что такой-то слот в  storage у нас есть маркер слот для маппинга. Поскольку слот пустой, то физический EVM его не выделяет. А значит для создания mapping’а ничего записывать в storage не нужно. Зачем нужен этот маркер слот — мы узнаем чуть позже.

Как записать само значение в mapping? Очевидно, что значение нужно записать в слот storage. Но по какому адресу? Если Вы знакомы с устройством ассоциативных массивов в других языках, то догадаетесь, что к ключу применяется хэш-функция. В Solidity принята хэш-функция Keccak256. Т.е. разумно предположить, что запись в мэппинг происходит так:

  1. Вычисляем адрес слота:  keccak256(key)
  2. По вычисленному адресу записываем значение

Представьте теперь, что у нас два маппинга:

И в оба маппинга мы пытаемся записать значение:

А поскольку ключ у нас один — 0xfd629caaDc426DaE4Fe97693741D106E75dbA09e, то в обоих случаях мы получим одинаковый хэш. А значит значения будет записано в один и тот же слот для обоих маппингов. И только последнее записанное значение будет актуальным.

Выходит, что наше предположение о том, что для вычисления слота нам достаточно взять хэш от ключа не верно? Не совсем. Нам просто нужно добавить при вычисление ключа еще какое-нибудь значение. Это значение должно соответствовать как-то  нашему маппингу. И быть для него уникальным. Помните, мы говорили выше про маркер-слот. При расчете хэша добавляем адрес маркер-слота к ключу:

Адрес слота для значения = keccak256(marker-slot-address + key)

Теперь адреса для значений у каждого маппинга будут уникальными.

Итак, в теории все понятно. Давайте теперь разберемся с практикой.

Работа с маппингом на практике

Мы уже знаем, что для инициализации пустого маппинга нам ничего не нужно, кроме как запомнить адрес marker слота в storage. Пусть этот слот будет — 0x0.

Прежде чем разобраться с чтением, нам нужно что-то записать.

Помните, в первой статье мы говорили, о том, что первые четыре слота в памяти зарезервированы. (ссылка на документацию) Мы уже знаем для чего третий слот — это указатель на свободную память. А теперь узнаем, для чего первые два слота! Слоты с 0 по 0x20 и c 0x20 по 0x40 используются для работы с инструкциями хэширования. Туда записываются данные для хэширования.

Запись в маппинг

  1. Загружаем в память по адресу 0x0 (область для работы с хэшированием) ключ 0xdac17f958d2ee523a2206206994597c13d831ec7:
  2. Загружаем во второй слот 0x20 адрес слота-маркера в storage. У нас он 0x0. Поэтому записываем по адресу 0x20 значение нашего слота 0x0.
  3. Выполняем хэширование

    Теперь хэш находятся на стэке.
  4. Записываем данные. Хэш находится на вершине стэка. Осталось положить в стэк значение. Затем поменять местами с хэшом, поскольку для команды SSTORE на верхушке стэка должен быть адрес — куда писать. А второе значение в стэке — что записать.

Все. Данные записаны в маппинг. Приступаем к чтению.

Чтение из маппинга

Сначала нам нужно вычислить адрес, откуда будем читать. Код будет ровно таким же, как и в предыдущем пункте:

Теперь на стэке у нас есть хэш — адрес слота. Осталось только прочитать данные по этому адресу из storage в стэк.

Все. Теперь в стэке прочитанное значение.

Резюме

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

Предыдущая статья. Следующая статья.

Полный список статей по теме тут.

Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.

Добавить комментарий