Crypto Development
December 19, 2022

Неочевидные 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
}

И вот тут-то я и столкнулся с проблемой.

Remix IDE test

Проблема

Поскольку мы не можем возвращать из метода в 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;
}

И ведь самое интересное, что код компилируется, линтер не выдает ошибку! Вроде все чисто!

Remix IDE - no linter errors

А вот при исполнении вылетает Panic Exception!

Remix IDE debugger

Решение

Несколько раз проверив весь код, а так же логом пройдясь почти по каждой строчке, я смирился с тем, что мое первое подозрение верно - что-то не то с массивом. Если бы у меня был простой, не 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 человека, и если честно - последнее сообщение в чате было неделю назад. Посмотрим, что из этого выйдет, но если я случайно заинтересовал тебя мой читатель - напиши мне!