Нам нужны наши смарт-контракты для обработки учетных записей с несколькими ролями. Как это сделать наиболее эффективно?
Учетные записи и роли - предыстория
Во время разработки и тестирования смарт-контрактов доступа к данным 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, роль 1
- Добавление учетной записи 1, роль 2
- Добавление учетной записи 1, роль 3
- Добавление учетной записи 2, роль 3
- Добавление учетной записи 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 элементов.