Неочевидные Solidity Memory Arrays
Всем привет, я Дима из Sumsub. Год назад один очень хороший человек посоветовал мне начать вести блог, писать о том о сем... В основном, для того, чтобы не чувствовать, что "дни проходят один за одним, и ничего не происходит", ведь происходит много всего, решается много задач, находится много решений, порой неочевидных.
Ведь действительно, порой нам приходится находить совсем неочевидное решение какой-либо проблемы, на это уходят силы, на это уходит время, а решение просто "остается в памяти". А вдруг, кто-то столкнулся с подобной задачей, а вдруг, кто-то нашел более изящное решение...
И вот, наконец то я созрел. Была проблема, с которой я столкнулся, которую я решил (не факт, что изящно), и ей я хочу поделиться с вами!
Контекст
Я писал код контракта, который отвечает за уровни доступа программ к данным пользователей. Контракт хранил список программы, в виде адресов, а так же уровни доступа и запросы на их получение в виде маппингов.
mapping(uint256 => mapping(address => uint256)) private _accesses;
В контракте был метод для получения списка программ, которые запросили доступ к данным пользователя, который возвращал массив структур с указанием адреса клиента и уровнем.
struct AccessType { address client; uint256 level; } function getRequests(address account) external view returns(AccessType[] memory) { // code }
И вот тут-то я и столкнулся с проблемой.
Проблема
Поскольку мы не можем возвращать из метода в Solidity маппинги, мне нужно пройтись по массиву программ и проверить уровень доступа для каждой из них для указанного пользователя. Звучит крайне просто и логично.
function getAccessLevel_WRONG(address user) public view returns(AccessLevel[] memory) { AccessLevel[] memory result; uint256 j = 0; for(uint256 i = 0; i < programs.length; i++) { address program = programs[i]; uint8 accessLevel = accessLevels[user][program]; if (accessLevel > 0) { result[j] = AccessLevel( program, accessLevel ); j++; } } return result; }
И ведь самое интересное, что код компилируется, линтер не выдает ошибку! Вроде все чисто!
А вот при исполнении вылетает Panic Exception!
Решение
Несколько раз проверив весь код, а так же логом пройдясь почти по каждой строчке, я смирился с тем, что мое первое подозрение верно - что-то не то с массивом. Если бы у меня был простой, не memory, массив, я бы просто добавлял в него элементы через .push, но тут так не прокатит. Так же я мог бы из функции вернуть 2 значения, в первом указывать адреса, а во втором - уровни, но это тоже как-то не тру.
Минимальные знания низкоуровневых языков говорят мне, что массивы без указанной длинны - не бро, а с указанной - бро. Ок, давайте проверим. Предположим, что у нас добавлена только одна программа и мы заменим:
-- function getAccessLevel_WRONG(address user) public view returns(AccessLevel[] memory) { AccessLevel[] memory result; ++ function getAccessLevel_WRONG(address user) public view returns(AccessLevel[1] memory) { AccessLevel[] memory result = new AccessLevel[](1);
Вуаля! Все заработало... Штош, похоже, мне таки придется посчитать сначала длину всего массива. Meh... Мне кажется, что я пишу дополнительный код, который будет тратить процессорное время выполнения view функции, но на самом деле это просто необходимость, с которой мне надо смириться.
function getAccessLevel_CORRECT(address user) public view returns(AccessLevel[] memory) { uint256 length = 0; for(uint256 i = 0; i < programs.length; i++) { address program = programs[i]; uint8 accessLevel = accessLevels[user][program]; if (accessLevel > 0) { length++; } } AccessLevel[] memory result = new AccessLevel[](length); uint256 j = 0; for(uint256 i = 0; i < programs.length; i++) { address program = programs[i]; uint8 accessLevel = accessLevels[user][program]; if (accessLevel > 0) { result[j] = AccessLevel( program, accessLevel ); j++; } } return result; }
Мне еще предстоит разобраться, с глубинным поведением этого кода, но смело можно сказать три вещи:
- Solidity плохо умеет в динамические массивы, с этим надо смириться, но это не проблема;
- Линтер не отработал, а компилятор схавал - это уже плохо. Я привык доверять Remix IDE и её(его? IDE - это он или она?) инструментам. Вот по компилятору - да, тут еще есть вопросы, думаю что Remix IDE сможет мне помочь в этом своим дебаггером. Ну или придется погонять код в Ganache, посмотрим.
- TDD рулит. Ясен красен тесты я пошел писать не сразу! Да, сначала этот код оказался в сети, а уже потом я увидел ошибку, и если честно - пару недель игнорил ее, потому что у меня были еще и другие методы для получения чуть более конкретных данных. Но вот именно написание теста, параллельно с переписыванием кода помогло мне разобраться, что именно идет не так.
Код с контрактом и тестами я залил на github, так что велкам посмотреть.
Послесловие
Мне еще предстоит разобраться, что именно идет не так в этом коде, вероятно кто-то из вас, читателей (ну я надеюсь кто-то это читает), знает в чем причина! дайте мне знать, я с радостью приму ваше решение. Не скажу, что потратил на решение проблемы много времени, наверное, потому что изначально предполагал проблему с длинной массива. Но вот сейчас я дописываю этот текст, и уже чувствую, что время не потрачено впустую, а значит все не напрасно.
После-послесловие
Есть идея: создать сообщество блокчейн разработчиков, где каждый сможет делиться своим опытом, в виде статей или в виде диалога. Чтобы каждый участник сообщества был значим, чтобы были общие проекты, чтобы совместно поднимать ноды. А поверх всего этого DAO, ну и шаттл на луну. Ну это все идеи, которые на данном этапе находятся на стадии зарождения. Нас пока 4 человека, и если честно - последнее сообщение в чате было неделю назад. Посмотрим, что из этого выйдет, но если я случайно заинтересовал тебя мой читатель - напиши мне!