Используйте библиотеку Ethers.js для доступа к строкам, динамическим массивам, сопоставлениям, структурам и переменным, упакованным в байты.

*Перейдите ко второй половине статьи, чтобы ознакомиться с примерами кода Ethers.js, или посетите этот репозиторий GitHub, чтобы просмотреть полный код и тестовые файлы.

Эфириум и состояние смарт-контракта

Данные на виртуальной машине Ethereum (EVM) организованы с использованием модифицированной структуры данных Merkle Patricia Trie. Каждый блок в цепочке блоков ссылается на четыре попытки: [глобальное] дерево состояний, дерево хранения, дерево транзакций и дерево получения. Дерево состояния содержит данные EOA (внешней учетной записи) в виде сопоставления адресов с балансами ETH, тогда как данные смарт-контракта хранятся в дереве хранилища, которое указывает на дерево состояния.

Данные смарт-контрактов в дереве хранилища представляют постоянное состояние контрактов и могут быть изменены транзакциями, которые обновляют глобальное состояние. В смарт-контракте Solidity динамические переменные хранятся в хранилище, которое является постоянным. Любые переменные, инициализированные в памяти, являются временными и будут стерты перед выполнением следующего вызова внешней функции. Кроме того, постоянные переменные, которые нельзя изменить, не занимают места для хранения и, следовательно, потребляют меньше газа.

Схема хранения смарт-контрактов

Каждый смарт-контракт в виртуальной машине Ethereum (EVM) имеет собственное постоянное пространство для хранения, содержащее 32-байтовые слоты в сопоставлении пар ключ-значение (ключ и значение имеют размер 32 байта).

Переменные фиксированного размера размером 32 байта

32-байтовые переменные фиксированного размера, такие как строки, uint256 и int256, назначаются отдельным слотам хранения в том порядке, в котором они перечислены в смарт-контракте. В контракте StorageLayoutOne константная переменная hello не имеет слота для хранения, поскольку ее нельзя изменить. Переменные numOne, goodbye, и num используют слоты хранения 0x0, 0x1 и 0x2 соответственно.

contract StorageLayoutOne {
​ ​ ​​string constant hello = "hello world"; // no storage
​ ​ ​​uint256 numOne = 1; // slot 0x0
​ ​ ​​string goodbye = "goodbye world"; // slot 0x1
​ ​ ​​int256 num; // slot 0x2
}

Переменные фиксированного размера ‹ 32 байта

Переменные фиксированного размера размером менее 32 байт будут по возможности упакованы в один слот хранения. В контракте StorageLayoutTwo переменные lock, byteX, bytesY и bytesZ будут все должно быть упаковано в слот 0x0 (1+1+4+16 = 22 байта). Следующая переменная, bytesA, хранится в слоте 0x1, поскольку не помещается в предыдущий. Наконец, переменные bytesB и bytesC упаковываются в слот 0x2.

contract StorageLayoutTwo {
​ ​ ​​bool lock; // slot 0x0
 ​ ​​byte byteX; // slot 0x0
​ ​ ​​bytes4 bytesY; // slot 0x0
​ ​ ​​bytes16 bytesZ; // slot 0x0
​​​ ​ ​​bytes28 bytesA; // slot 0x1
​​​ ​ ​​bytes16 bytesB; // slot 0x2
​​​ ​ ​​bytes16 bytesC; // slot 0x2
}

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

Переменные с динамическим размером

Переменные с динамическим размером, такие как динамические массивы и сопоставления, размер которых может превышать 32 байта, хэшируются в устойчивые к коллизиям ячейки памяти с использованием хэш-алгоритма keccak-256, который псевдослучайным образом выбирает позицию в диапазоне 2²⁵⁶ слотов памяти. Если вам интересно, 2²⁵⁶ =

115792089237316195423570985008687907853269984665640564039457584007913129639936

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

Динамические массивы начинаются с места хранения, определяемого хэшем слота. Последующие элементы массива располагаются рядом с предыдущим элементом. Когда элементы массива имеют размер 16 байт или меньше, применяются правила упаковки байтов.

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

В контракте StorageLayoutThree, arrayOfNums, слот 0x0 (дополненный до 32 байтов) хешируется, чтобы найти место хранения первого элемента, arrayOfNums[0]. Для userBalances адрес пользователя объединяется со слотом 0x1 и хешируется, чтобы указать местоположение значения.

contract StorageLayoutThree { ​
​ ​ uint[] public arrayOfNums; // slot 0x0 => keccak256(0x0)
​ ​ ​​mapping(address => uint256) public userBalances;
 ​ // slot 0x1 => keccak256(key + 0x1)
}

Кроме того, отображения часто вложены в другие отображения и могут содержать структуры. Точно так же массивы могут быть вложены в другие массивы, а также можно вкладывать сопоставления внутри массивов. При работе с вложенными структурами данных расположение данных можно найти с помощью вложенных хэшей keccak-256. Примеры этого приведены в руководстве по кодированию ниже в этой статье.

Переменные, не относящиеся к хранению

Константы, перечисления, определения структур, события и определяемые пользователем ошибки не занимают места для хранения.

Учебник по коду с Ether.js

Библиотека Ethers-JavaScript предоставляет множество полезных инструментов для взаимодействия со смарт-контрактами в блокчейне Ethereum, включая утилиты для прямого доступа к переменным хранилища, которые будут объяснены с примерами кода ниже. Полный код можно найти в этом репозитории GitHub вместе с примером смарт-контракта и файлами модульного тестирования.

Чтобы установить Ether.js, введите следующую команду в терминал корневого каталога вашего проекта:

npm install ethers

Определить многократно используемые константы Ethers.js

Вот несколько определений констант, которые нужно добавить в начало нашего файла (файлов) JavaScript, которые помогут писать более чистый код:

Струны

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

Числа

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

Функция getUint256 будет использоваться в приведенных ниже функциях сопоставления.

Сопоставления

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

Например, сопоставление адресов [EOA] с балансами [uint256] потребует аргументов: слот хранилища, адрес контракта и адрес EOA. Слот разрезается на 2, чтобы удалить «0x», так как ключ уже содержит «0x», указывающий шестнадцатеричное число.

Сопоставления со структурами

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

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

Сопоставления с вложенными сопоставлениями в структурах

Эта последняя функция отображения имеет дело с отображением внутри структуры внутри отображения. Сравнивая аргументы с предыдущим примером, «тип» был заменен на «nestedKey». Нам не нужен тип, так как отображение сопоставляет один тип с другим типом, что означает, что конечное значение будет одного типа. В этом случае предполагается, что окончательное значение равно Uint256.

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

Байт-упакованные слоты

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

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

В JavaScript сложно работать с большими числами, поэтому эта функция рекурсивно вызывается для размеров байтов, превышающих 6 байт, и возвращает конкатенированный результат полной переменной.

Функция getBytePackedVar будет использоваться в функции динамического массива ниже.

Динамические массивы

Массивы хэшируются в место хранения, где все элементы расположены непрерывно. Как только головка массива найдена, последующие элементы могут быть найдены с помощью слота или сдвига битов. Если элементы имеют размер 16 байт или меньше, слоты хранения будут упакованы побайтно.

Заключение

Понимание хранилища смарт-контрактов Solidity важно для написания газоэффективного, безопасного и оптимизированного для данных кода. Ethers.js предоставляет множество полезных методов, которые можно использовать для доступа к переменным хранилища в постоянном состоянии смарт-контракта. Использование или изменение приведенных выше примеров кода в ваших собственных развернутых смарт-контрактах поможет вам лучше понять уровень хранения EVM.