Нам нужны наши смарт-контракты для обработки учетных записей с несколькими ролями. Как это сделать наиболее эффективно?

Учетные записи и роли - предыстория

Во время разработки и тестирования смарт-контрактов доступа к данным Solidity (S-DAC) от Datona Lab нам часто приходится работать с учетными записями с несколькими ролями, такими как владелец контракта и владелец данных или аудитор данных и регулятор данных.

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

Обсуждение

Наши смарт-контракты Solidity часто содержат конечные автоматы, как указано в статье State Machines in Solidity. Всякий раз, когда пользователь пытается перевести смарт-контракт из одного состояния в другое, он проверяет, есть ли у пользователя разрешение на это. Разрешение зависит от текущего состояния машины и роли пользователя.

    ...
    function startVerification() public {
        require(currentState == State.READY_FOR_VERIFICATION);
        require(accountHasRole(msg.sender, Role.VERIFIER));
        currentState = State.VERIFYING;
    }
    ...

В этой статье основное внимание уделяется эффективной реализации accountHasRole , обеспечиваемой смарт-контрактом.

Мы рассматриваем эти возможные реализации:

Name       Description
map/map    account mapping and role mapping
map/d31    account mapping and dynamic value-array of 31 bytes
map/set    account mapping and set

Динамические массивы значений описаны автором в статье Динамические массивы значений в Solidity.

В приведенных ниже смарт-контрактах для упрощения мы используем константы вместо перечислений.

Реализация смарт-контрактов

Мы эффективно предоставляем функциональность в базовом классе. Он предназначен для наследования для обеспечения желаемой функциональности. Фактически это перехват наследования вместо композиции объекта.

карта / карта

Вот полезный файл импорта, содержащий базовый контракт, который предоставляет функции добавления и проверки:

contract BaseAccountRoles { // an account has many roles
    mapping(address => mapping(uint => bool)) accountRoles;
    
    function accountAddRole(address account, uint role) internal {
        accountRoles[account][role] = true;
    }
    function accountHasRole(address account, uint role) internal 
    view returns (bool) {
        return accountRoles[account][role];
    }
}

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

map / d31

Вот еще один полезный файл импорта, содержащий базовый контракт, который предоставляет функции добавления и проверки:

import "Bytes31fun.sol"
contract BaseAccountRoles31 { // an account has many roles
    using Bytes31fun for bytes32;
    
    mapping(address => bytes32) accountRoles;
    
    function addAccountRole(address account, uint role) internal {
        accountRoles[account] = accountRoles[account].push(role);
    }
    function confirmAccountRole(address account, uint role) internal 
    view returns (bool) {
        return accountRoles[account].find(role);
    }
}

В этом контракте используется библиотека для расширения типа Solidity bytes32 для предоставления push () и find (). Метод описан в статье автора Динамические массивы значений в Solidity.

Функция сопоставления Solidity используется для сопоставления заданного адреса с переменной bytes32, которая содержит состав каждой заданной роли, которую предоставляет адрес.

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

карта / набор

Вот последний полезный файл импорта, содержащий базовый контракт, который предоставляет функции добавления и проверки:

contract BaseAccountRolesSet { // an account has many roles
    mapping(address => uint) accountRoleSet;
    
    function addAccountRole(address account, uint role) internal {
        accountRoleSet[account] |= 1 << role;
    }
    function confirmAccountRole(address account, uint role) internal 
    view returns (bool) {
        return (accountRoleSet[account] & (1 << role)) != 0;
    }
}

Функция сопоставления Solidity используется для сопоставления заданного адреса с переменной, которая используется как набор для хранения каждой заданной роли, которую предоставляет адрес.

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

Возможность загадочного изображения:

Тестирование

Мы сделали все тестируемые функции общедоступными, чтобы измерить стоимость строительства, и создали тестовую систему, которая будет измерять потребление газа:

  1. Создание базового контракта
  2. Добавление учетной записи 1, роль 1
  3. Добавление учетной записи 1, роль 2
  4. Добавление учетной записи 1, роль 3
  5. Добавление учетной записи 2, роль 3
  6. Добавление учетной записи 3, роль 3

Код смарт-контракта Solidity выглядит следующим образом:

import "GasCost.sol"
contract GasAccountRolesSet is GasCost {
    using StringLib for string;
    uint constant ROLE1 = 0;
    uint constant ROLE2 = 1;
    uint constant ROLE3 = 2;
    address constant ACCOUNT1 = 0x1111111111...1111111111;
    address constant ACCOUNT2 = 0x2222222222...2222222222;
    address constant ACCOUNT3 = 0x3333333333...3333333333;
    BaseAccountRolesSet con;
    function _create() private {
        con = new BaseAccountRolesSet();
    }
    function create() public {
        gasCostFun("constructor", _create);
    }
    function gasCost(string memory name, address account, uint role)
    internal {
        uint u0 = gasleft();
        con.addAccountRole(account, role);
        uint u1 = gasleft();
        name = name.concat(" add: ", (u0 - u1).niceDecimal());
        u0 = gasleft();
        require(con.confirmAccountRole(account, role));
        u1 = gasleft();
        name = name.concat(" has: ", (u0 - u1).niceDecimal());
        report(name);
    }
    function add11() public {
        gasCost("add11", ACCOUNT1, ROLE1);
    }
    
    function add12() public {
        gasCost("add12", ACCOUNT1, ROLE2);
    }
    
    function add13() public {
        gasCost("add13", ACCOUNT1, ROLE3);
    }
    
    function add23() public {
        gasCost("add23", ACCOUNT2, ROLE3);
    }
    
    function add33() public {
        gasCost("add33", ACCOUNT3, ROLE3);
    }
}

Расход газа

Написав решения и тесты смарт-контрактов, мы измерили расход газа по методике, описанной в статье автора Функции твердости газа.

Компилятор - Istanbul v0.6.12.

Создание договоров и добавление записей

На графике видно, что динамический массив значений, в частности, страдает от большого расхода газа на создание дополнительного кода в библиотеке, который обрабатывает динамические массивы значений.

Добавление записей

Предполагая, что стоимость строительства не имеет значения (что может быть, если ваше решение продолжает добавлять данные), потребление газа для примеров операций составляет:

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

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

Проверка записей

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

Вот расход газа:

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

Другие возможности

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

Если ваше решение требует создания адресов учетных записей, это вполне реальная возможность.

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

Выводы

Отличная функция отображения в Solidity очень эффективна.

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