Проблема распределения памяти хранилища в Solidity
Начав разбираться с разработкой смарт-контрактов, обнаружил интересную особенность распределения данных постоянного хранилища контракта, уже который день не дающую мне спокойно заниматься своим делом.
Как хранятся данные смарт-контракта в хранилище
В адресном пространстве хранилища (2**256 слов размером 32 байта), все постоянные переменные контракта фиксированного размера хранятся в самом его начале (самые младшие адреса). Однако, язык Solidity предоставляет возможность хранить и динамические переменные, которые могут менять свой размер. Язык позволяет использовать две разновидности таких переменных, динамический массив и отображение.
В Solidity (и в других языках смарт-контрактов, с которыми мне удалось бегло ознакомиться) для хранения динамических данных используется хитрый трюк, связанный с функцией криптографического хеширования keccak (в смарт-контрактах используется keccak256, которая возвращает хеш размером 256 бит).
Всякий раз, когда требуется получить адрес области памяти, которая должна быть использована для хранения данных массива, адрес начала данных массива определяется по функции keccak, примененной к адресу переменной этого массива. Для хранения же значения отображения с определенным ключом, функция keccak применяется для комбинации из адреса переменной этого отображения, и переданного ключа.
Поскольку функция хеширования keccak специально предназначена для криптографии, такие адреса обычно располагаются достаточно далеко друг от друга, чтобы пересечения данных в динамических переменных контракта не произошло.
Так в чем проблема?
Проблема в том, что же делать, если пересечение данных все-таки случилось.
Вероятность такого события - ничтожная, учитывая ширину адреса, с одной стороны, и объем данных, характерных для контрактов, с другой. Фактически, насколько можно понять из доступной на сегодня информации, до сих пор в сетях, поддерживающих EVM, нет ни одного зафиксированного случая пересечения ключей, выработанных посредством keccak на основании разных исходных данных.
Однако, начиная работать со смарт-контрактами, мне бы не хотелось стать тем счастливчиком, который наткнется на пересечение ключей в своем смарт-контракте первым. Насколько я понимаю, никакой диагностики пересечения ключей (и тем более, пересечения массивов, разбросанных функцией keccak в адресном пространстве хранилища) не предусмотрено, а результат такого пересечения для контракта - катастрофический, сравнимый с доступом к неинициализированной памяти в C++.
Чрезвычайно низкая вероятность пересечения данных совершенно не утешает. В отсутствие диагностики пересечения, невозможно даже обнаружить его вовремя, не говоря уже о том, чтобы предусмотреть какую-то его разумную обработку. Более того, даже диагностировав пересечение ключей, мало что можно сделать, чтобы исправить проблему - ведь повторение последовательности исходных обращений к контракту приведет лишь к повторному возникновению того же пересечения.
Что делать?
Использование способов распределения пространства памяти, применяемых в традиционных файловых системах и системах распределения оперативной памяти, может помочь, хотя и скорее всего, увеличит затраты ресурсов на доступ к отдельному слоту хранилища. Многосвязные списки, сбалансированные деревья, битовые карты занятости, использованные с учетом специфики доступа к хранилищу смарт-контракта, сделают это хранилище полностью безопасным и надежным для хранения данных любой ценности.