ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
Шекелей пачку Jolah Molivski
Ваши хорошие привычки разработчика приводят к тому, что вы пишете неэффективные смарт-контракты. Для типичных языков программирования единственными затратами, связанными с изменением состояния и вычислениями, являются время и электроэнергия, используемая оборудованием. Однако для EVM-совместимых языков, таких как Solidity и Vyper, эти действия явно стоят денег . Эта стоимость выражается в собственной валюте блокчейна (ETH для Etheruem, AVAX для Avalanche и т. д.), которую можно рассматривать как товар, используемый для оплаты этих действий. Стоимость вычислений, переходов между состояниями и хранения называется газом . Газ используется для определения приоритетов транзакций, в качестве сопротивления Сивиллы и для предотвращения атак, связанных с проблемой остановки .
Эти нетипичные затраты приводят к шаблонам проектирования программного обеспечения, которые кажутся неэффективными и странными в типичных языках программирования. Чтобы уметь распознавать эти закономерности и понимать, почему они приводят к экономической эффективности, вы должны сначала иметь базовое представление о виртуальной машине Ethereum, то есть EVM.
Блокчейн — это конечный автомат . Блокчейны постепенно выполняют транзакции, которые переходят в какое-то новое состояние. Следовательно, каждая транзакция в блокчейне — это переход состояния.
Простые блокчейны, такие как Биткойн, изначально поддерживают только простые переводы. Напротив, цепочки, совместимые со смарт-контрактами, такие как Ethereum, реализуют два типа учетных записей: внешние учетные записи и контрактные учетные записи, чтобы поддерживать сложную логику.
Внешние учетные записи контролируются пользователями с помощью закрытых ключей и не имеют связанного с ними кода, в то время как контрактные учетные записи контролируются исключительно связанным с ними кодом. Код EVM хранится в виде байт в виртуальном ROM.
EVM обрабатывает выполнение и обработку всех транзакций в базовой цепочке блоков. Это стековая машина, в которой размер каждого элемента стека составляет 256 бит или 32 байта. EVM встроен в каждый узел Ethereum и отвечает за выполнение байт-кода контракта.
EVM хранит данные как в Storage / долговременная память, точнее постояная, так как при развертывании контракта записываеется в блокчейн и, как известно, что туда попало - там навсегда и осталось / , так и в memory / типа оперативной памяти / . Storage используется для постоянного хранения данных, а memory используется для хранения данных во время вызовов функций. Вы также можете передавать аргументы функции как данные вызова, которые действуют аналогично выделению в память, за исключением того, что данные не поддаются изменению.
Смарт-контракты пишутся на языках более высокого уровня, таких как Solidity, Vyper или Yul, и впоследствии разбиваются на байт-код EVM с помощью компилятора. Однако бывают случаи, когда более эффективно использовать байт-код непосредственно в коде.
Байт-код EVM записывается в шестнадцатеричном формате. Это язык, который виртуальная машина может интерпретировать. Это чем-то похоже на то, как процессоры могут интерпретировать только машинный код.
Пример байт-кода Solidity
Весь байт-код Ethereum можно разбить на ряд операндов и кодов операций. Коды операций — это предопределенные инструкции, которые EVM интерпретирует и впоследствии может выполнить. Например, код операции ADD представлен как 0x01 в байт-коде EVM. Он удаляет два элемента из стека и помещает результат.
Количество элементов, удаляемых из стека и помещаемых в стек, зависит от кода операции. Например, имеется тридцать два кода операции PUSH: от PUSH1 до PUSH32. PUSH* добавляет в стек * байтовый элемент размером от 0 до 32 байт. Он не удаляет никаких значений из стека и добавляет одно значение. Напротив, код операции ADDMOD представляет операцию сложения по модулю и удаляет три элемента из стека, а затем помещает результат в стек. Примечательно, что только коды операций PUSH поставляются с операндами.
Коды операций предыдущего примера байт-кода
Каждый код операции представляет собой один байт и имеет разную стоимость. В зависимости от кода операции эти затраты либо фиксированы, либо определяются по формуле. Например, код операции ADD стоит 3 газа. А SSTORE, который сохраняет данные в хранилище, стоит 20 000 газа, когда значение хранилища устанавливается на ненулевое значение, и стоит 5000 газа, когда значение переменной хранилища установлено равным нулю или остается неизменным.
Стоимость SSTORE на самом деле зависит от того, был ли осуществлен доступ к значению или нет. Полную информацию о расходах SSTORE и SLOAD можно найти здесь
Понимание кодов операций EVM чрезвычайно важно для минимизации потребления газа и, в свою очередь, снижения затрат для вашего конечного пользователя. Поскольку стоимость, связанная с кодами операций EVM, произвольна, различные шаблоны кодирования, позволяющие достичь одного и того же результата, могут привести к значительному увеличению затрат. Знание того, какие коды операций являются самыми дорогими, поможет вам свести к минимуму и избежать их использования, когда это не нужно. Просмотрите документацию Ethereum для получения полного списка кодов операций EVM и связанных с ними затрат на газ.
Код операции MUL стоит 5 газа и используется для выполнения умножения. Например, арифметическое действие 10 * 10 будет стоить 5 газа.
Код операции EXP используется для возведения в степень, а его стоимость газа определяется по формуле: если показатель степени равен нулю, код операции стоит 10 газа. Однако, если показатель степени больше нуля, это стоит 10 газа + 50-кратное количество байтов в показателе степени.
Поскольку байт состоит из 8 бит, один байт используется для представления значений от 0 до 2⁸-1, два байта используются для представления значений от 2⁸ до 2¹⁶-1 и т. д. Например, 10¹⁸ будет стоить 10 + 50 * 1 = 60 газа, а 10³⁰⁰ будет стоить 10 + 50 * 2 = 160 газа, поскольку для представления 18 требуется один байт, а для представления 300 — два байта.
Из вышеизложенного ясно, что в определенные моменты следует использовать умножение вместо возведения в степень. Вот конкретный пример:
И inefficcientSquare, и efficcientSquare устанавливают переменную состояния x в квадрат самой себя. Однако арифметика inefficcientSquare стоит 10 + 1 * 50 = 60 газа, а арифметика efficcientSquare стоит 5 газа.
По причинам, помимо указанных выше затрат на арифметику, inefficcientSquare стоит примерно на 200 газа больше, чем efficcientSquare среднем .
Хорошо известно, что кэширование данных приводит к гораздо более высокой производительности при масштабировании. Однако кэширование данных на EVM чрезвычайно важно и приведет к существенной экономии газа даже при небольшом количестве операций.
Коды операций SLOAD и MLOAD используются для загрузки данных из хранилища и памяти. MLOAD всегда стоит 3 газа, в то время как стоимость SLOAD определяется по формуле: SLOAD стоит 2100 газа для первоначального доступа к значению во время транзакции и стоит 100 газа для каждого последующего доступа. Это означает, что загружать данные из memory на ≥97% дешевле, чем из storage.
Ниже приведен пример кода и потенциальная экономия газа:
Контракт storageExample имеет две функции: inefficcientSum и efficcientSum.
Обе функции принимают _array , представляющий собой массив целых чисел без знака. Они оба устанавливают переменную состояния контракта, sumOfArray , в сумму значений в _array .
inefficcientSum использует саму переменную состояния для своих вычислений. Помните, что переменные состояния, такие как sumOfArray , хранятся в хранилище .
efficcientSum создает в памяти временную переменную tempVar , которая используется для вычисления суммы значений в _array . Затем sumOfArray присваивается значению tempVar .
efficcientSum более чем на 50 % эффективнее по газу, чем inefficcientSum, при передаче массива только из 10 целых чисел без знака.
Эта эффективность зависит от количества вычислений: efficcientSum - 300 % эффективнее по газу, чем inefficcientSum, при передаче массива из 100 целых чисел без знака.
Код операции CREATE используется при создании новой учетной записи с соответствующим кодом (например, смарт-контракт). Он стоит не менее 32 000 газа и является самым дорогим кодом операции в EVM.
По возможности лучше свести к минимуму количество используемых смарт-контрактов. Это отличается от типичного объектно-ориентированного программирования, в котором разделение классов поощряется для повторного использования и ясности.
Ниже приведен код для создания «хранилища» с использованием объектно-ориентированного подхода. Каждое хранилище содержит uint256, который задается в его конструкторе.
Каждый раз, когда вызывается createVault() , создается новый смарт-контракт , хранящееся в Vault , определяется аргументом, переданным в createVault(). Затем адрес нового Vault сохраняется в массиве factory.
Теперь вот код, который выполняет ту же цель, но использует сопоставление / mapping / вместо создания нового смарт-контракта:
При каждом вызове createVault() его аргумент сохраняется в отображении, а его идентификатор определяется переменной состояния nextVaultId, которая увеличивается при каждом вызове createVault() .
Эта разница в реализации приводит к резкому снижению затрат на газ.
Функция createVault() от EfficcientVaults на 61% эффективнее и стоит примерно на 76 300 единиц газа меньше, чем в среднем у InefficcientVaults.
Следует отметить, что в определенные моменты желательно создать новый контракт внутри контракта, и обычно это делается для неизменности и эффективности. Стоимость транзакции для всех взаимодействий с контрактом будет увеличиваться с размером контракта . Поэтому, если вы планируете хранить огромные объемы данных в сети, вероятно, лучше разделить эти данные с помощью отдельных контрактов. Однако, если это не так, следует избегать создания новых контрактов.
SSTORE — это код операции EVM для сохранения данных в хранилище. SSTORE стоит 20 000 газа при установке значения хранилища ненулевым с нуля и 5000 газа, когда значение хранилища установлено равным нулю.
Из-за этой стоимости хранение данных в сети крайне неэффективно и дорого. Его следует избегать, когда это возможно.
Эта практика наиболее распространена с NFT. Разработчики будут хранить метаданные NFT (его изображение, атрибуты и т. д.) в децентрализованной сети хранения, такой как Arweave или IPFS, вместо того, чтобы хранить их в блокчейне. Единственные данные, которые хранятся в сети, — это ссылка на метаданные в соответствующей децентрализованной сети хранения. Эта ссылка доступна для запроса с помощью функции tokenURI() , которая есть во всех ERC721, содержащих метаданные.
Для примера возьмем смарт-контракт Bored Ape Yacht Club . Вызов функции tokenURI() с идентификатором tokenId, равным 0, возвращает следующую ссылку: ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0.
Если вы перейдете по этой ссылке, вы найдете файл JSON, содержащий метаданные BAYC #0:
Эти атрибуты легко проверить в OpenSea :
Следует также отметить, что некоторые структуры данных просто невозможно реализовать в EVM из-за стоимости хранения. Например, представление графа с использованием матрицы смежности было бы совершенно невозможным из-за его пространственной сложности O(V²).
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
Шекелей пачку Jolah Molivski
Ваши хорошие привычки разработчика приводят к тому, что вы пишете неэффективные смарт-контракты. Для типичных языков программирования единственными затратами, связанными с изменением состояния и вычислениями, являются время и электроэнергия, используемая оборудованием. Однако для EVM-совместимых языков, таких как Solidity и Vyper, эти действия явно стоят денег . Эта стоимость выражается в собственной валюте блокчейна (ETH для Etheruem, AVAX для Avalanche и т. д.), которую можно рассматривать как товар, используемый для оплаты этих действий. Стоимость вычислений, переходов между состояниями и хранения называется газом . Газ используется для определения приоритетов транзакций, в качестве сопротивления Сивиллы и для предотвращения атак, связанных с проблемой остановки .
Эти нетипичные затраты приводят к шаблонам проектирования программного обеспечения, которые кажутся неэффективными и странными в типичных языках программирования. Чтобы уметь распознавать эти закономерности и понимать, почему они приводят к экономической эффективности, вы должны сначала иметь базовое представление о виртуальной машине Ethereum, то есть EVM.
Что такое EVM?
Блокчейн — это конечный автомат . Блокчейны постепенно выполняют транзакции, которые переходят в какое-то новое состояние. Следовательно, каждая транзакция в блокчейне — это переход состояния.
Простые блокчейны, такие как Биткойн, изначально поддерживают только простые переводы. Напротив, цепочки, совместимые со смарт-контрактами, такие как Ethereum, реализуют два типа учетных записей: внешние учетные записи и контрактные учетные записи, чтобы поддерживать сложную логику.
Внешние учетные записи контролируются пользователями с помощью закрытых ключей и не имеют связанного с ними кода, в то время как контрактные учетные записи контролируются исключительно связанным с ними кодом. Код EVM хранится в виде байт в виртуальном ROM.
EVM обрабатывает выполнение и обработку всех транзакций в базовой цепочке блоков. Это стековая машина, в которой размер каждого элемента стека составляет 256 бит или 32 байта. EVM встроен в каждый узел Ethereum и отвечает за выполнение байт-кода контракта.
EVM хранит данные как в Storage / долговременная память, точнее постояная, так как при развертывании контракта записываеется в блокчейн и, как известно, что туда попало - там навсегда и осталось / , так и в memory / типа оперативной памяти / . Storage используется для постоянного хранения данных, а memory используется для хранения данных во время вызовов функций. Вы также можете передавать аргументы функции как данные вызова, которые действуют аналогично выделению в память, за исключением того, что данные не поддаются изменению.
Смарт-контракты пишутся на языках более высокого уровня, таких как Solidity, Vyper или Yul, и впоследствии разбиваются на байт-код EVM с помощью компилятора. Однако бывают случаи, когда более эффективно использовать байт-код непосредственно в коде.
Байт-код EVM записывается в шестнадцатеричном формате. Это язык, который виртуальная машина может интерпретировать. Это чем-то похоже на то, как процессоры могут интерпретировать только машинный код.
Что такое опкоды EVM?
Весь байт-код Ethereum можно разбить на ряд операндов и кодов операций. Коды операций — это предопределенные инструкции, которые EVM интерпретирует и впоследствии может выполнить. Например, код операции ADD представлен как 0x01 в байт-коде EVM. Он удаляет два элемента из стека и помещает результат.
Количество элементов, удаляемых из стека и помещаемых в стек, зависит от кода операции. Например, имеется тридцать два кода операции PUSH: от PUSH1 до PUSH32. PUSH* добавляет в стек * байтовый элемент размером от 0 до 32 байт. Он не удаляет никаких значений из стека и добавляет одно значение. Напротив, код операции ADDMOD представляет операцию сложения по модулю и удаляет три элемента из стека, а затем помещает результат в стек. Примечательно, что только коды операций PUSH поставляются с операндами.
Каждый код операции представляет собой один байт и имеет разную стоимость. В зависимости от кода операции эти затраты либо фиксированы, либо определяются по формуле. Например, код операции ADD стоит 3 газа. А SSTORE, который сохраняет данные в хранилище, стоит 20 000 газа, когда значение хранилища устанавливается на ненулевое значение, и стоит 5000 газа, когда значение переменной хранилища установлено равным нулю или остается неизменным.
Стоимость SSTORE на самом деле зависит от того, был ли осуществлен доступ к значению или нет. Полную информацию о расходах SSTORE и SLOAD можно найти здесь
Почему важно понимать коды операций EVM?
Понимание кодов операций EVM чрезвычайно важно для минимизации потребления газа и, в свою очередь, снижения затрат для вашего конечного пользователя. Поскольку стоимость, связанная с кодами операций EVM, произвольна, различные шаблоны кодирования, позволяющие достичь одного и того же результата, могут привести к значительному увеличению затрат. Знание того, какие коды операций являются самыми дорогими, поможет вам свести к минимуму и избежать их использования, когда это не нужно. Просмотрите документацию Ethereum для получения полного списка кодов операций EVM и связанных с ними затрат на газ.
Использование умножения вместо возведения в степень: MUL против EXP
Код операции MUL стоит 5 газа и используется для выполнения умножения. Например, арифметическое действие 10 * 10 будет стоить 5 газа.
Код операции EXP используется для возведения в степень, а его стоимость газа определяется по формуле: если показатель степени равен нулю, код операции стоит 10 газа. Однако, если показатель степени больше нуля, это стоит 10 газа + 50-кратное количество байтов в показателе степени.
Поскольку байт состоит из 8 бит, один байт используется для представления значений от 0 до 2⁸-1, два байта используются для представления значений от 2⁸ до 2¹⁶-1 и т. д. Например, 10¹⁸ будет стоить 10 + 50 * 1 = 60 газа, а 10³⁰⁰ будет стоить 10 + 50 * 2 = 160 газа, поскольку для представления 18 требуется один байт, а для представления 300 — два байта.
Из вышеизложенного ясно, что в определенные моменты следует использовать умножение вместо возведения в степень. Вот конкретный пример:
Код:
contract squareExample {uint256 x;constructor (uint256 _x) {
x = _x;
}
function inefficcientSquare() external {
x = x**2;
}
function efficcientSquare() external {
x = x * x;
}
}
И inefficcientSquare, и efficcientSquare устанавливают переменную состояния x в квадрат самой себя. Однако арифметика inefficcientSquare стоит 10 + 1 * 50 = 60 газа, а арифметика efficcientSquare стоит 5 газа.
По причинам, помимо указанных выше затрат на арифметику, inefficcientSquare стоит примерно на 200 газа больше, чем efficcientSquare среднем .
Кэширование данных: SLOAD и MLOAD
Хорошо известно, что кэширование данных приводит к гораздо более высокой производительности при масштабировании. Однако кэширование данных на EVM чрезвычайно важно и приведет к существенной экономии газа даже при небольшом количестве операций.
Коды операций SLOAD и MLOAD используются для загрузки данных из хранилища и памяти. MLOAD всегда стоит 3 газа, в то время как стоимость SLOAD определяется по формуле: SLOAD стоит 2100 газа для первоначального доступа к значению во время транзакции и стоит 100 газа для каждого последующего доступа. Это означает, что загружать данные из memory на ≥97% дешевле, чем из storage.
Ниже приведен пример кода и потенциальная экономия газа:
Код:
contract storageExample {uint256 sumOfArray;
function inefficcientSum(uint256 [] memory _array) public {
for(uint256 i; i < _array.length; i++) {
sumOfArray += _array[i];
}
}
function efficcientSum(uint256 [] memory _array) public {
uint256 tempVar; for(uint256 i; i < _array.length; i++) {
tempVar += _array[i];
} sumOfArray = tempVar;
}
}
Контракт storageExample имеет две функции: inefficcientSum и efficcientSum.
Обе функции принимают _array , представляющий собой массив целых чисел без знака. Они оба устанавливают переменную состояния контракта, sumOfArray , в сумму значений в _array .
inefficcientSum использует саму переменную состояния для своих вычислений. Помните, что переменные состояния, такие как sumOfArray , хранятся в хранилище .
efficcientSum создает в памяти временную переменную tempVar , которая используется для вычисления суммы значений в _array . Затем sumOfArray присваивается значению tempVar .
efficcientSum более чем на 50 % эффективнее по газу, чем inefficcientSum, при передаче массива только из 10 целых чисел без знака.
Эта эффективность зависит от количества вычислений: efficcientSum - 300 % эффективнее по газу, чем inefficcientSum, при передаче массива из 100 целых чисел без знака.
Избегайте объектно-ориентированного программирования: код операции CREATE
Код операции CREATE используется при создании новой учетной записи с соответствующим кодом (например, смарт-контракт). Он стоит не менее 32 000 газа и является самым дорогим кодом операции в EVM.
По возможности лучше свести к минимуму количество используемых смарт-контрактов. Это отличается от типичного объектно-ориентированного программирования, в котором разделение классов поощряется для повторного использования и ясности.
Ниже приведен код для создания «хранилища» с использованием объектно-ориентированного подхода. Каждое хранилище содержит uint256, который задается в его конструкторе.
Код:
contract Vault {
uint256 private x;
constructor(uint256 _x) {
x = _x;
}
function getValue() external view returns (uint256) {
return x;}
} // end of Vaultinterface IVault { function getValue() external view returns (uint256);} // end of IVault
contract InefficcientVaults {
address[] public factory;
constructor() {}
function createVault(uint256 _x) external {
address _vaultAddress = address(new Vault(_x));
factory.push(_vaultAddress);
}
function getVaultValue(uint256 vaultId) external view returns (uint256) {
address _vaultAddress = factory[vaultId];
IVault _vault = IVault(_vaultAddress);
return _vault.getValue();
}
}
Каждый раз, когда вызывается createVault() , создается новый смарт-контракт , хранящееся в Vault , определяется аргументом, переданным в createVault(). Затем адрес нового Vault сохраняется в массиве factory.
Теперь вот код, который выполняет ту же цель, но использует сопоставление / mapping / вместо создания нового смарт-контракта:
Код:
contract EfficcientVaults {// vaultId => vaultValue
mapping (uint256 => uint256) public vaultIdToVaultValue;// the next vault's id
uint256 nextVaultId;function createVault(uint256 _x) external {
vaultIdToVaultValue[nextVaultId] = _x;
nextVaultId++;
}function getVaultValue(uint256 vaultId) external view returns (uint256) {
return vaultIdToVaultValue[vaultId];
}} // end of EfficcientVaults
При каждом вызове createVault() его аргумент сохраняется в отображении, а его идентификатор определяется переменной состояния nextVaultId, которая увеличивается при каждом вызове createVault() .
Эта разница в реализации приводит к резкому снижению затрат на газ.
Функция createVault() от EfficcientVaults на 61% эффективнее и стоит примерно на 76 300 единиц газа меньше, чем в среднем у InefficcientVaults.
Следует отметить, что в определенные моменты желательно создать новый контракт внутри контракта, и обычно это делается для неизменности и эффективности. Стоимость транзакции для всех взаимодействий с контрактом будет увеличиваться с размером контракта . Поэтому, если вы планируете хранить огромные объемы данных в сети, вероятно, лучше разделить эти данные с помощью отдельных контрактов. Однако, если это не так, следует избегать создания новых контрактов.
Хранение данных: SSTORE
SSTORE — это код операции EVM для сохранения данных в хранилище. SSTORE стоит 20 000 газа при установке значения хранилища ненулевым с нуля и 5000 газа, когда значение хранилища установлено равным нулю.
Из-за этой стоимости хранение данных в сети крайне неэффективно и дорого. Его следует избегать, когда это возможно.
Эта практика наиболее распространена с NFT. Разработчики будут хранить метаданные NFT (его изображение, атрибуты и т. д.) в децентрализованной сети хранения, такой как Arweave или IPFS, вместо того, чтобы хранить их в блокчейне. Единственные данные, которые хранятся в сети, — это ссылка на метаданные в соответствующей децентрализованной сети хранения. Эта ссылка доступна для запроса с помощью функции tokenURI() , которая есть во всех ERC721, содержащих метаданные.
Для примера возьмем смарт-контракт Bored Ape Yacht Club . Вызов функции tokenURI() с идентификатором tokenId, равным 0, возвращает следующую ссылку: ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/0.
Если вы перейдете по этой ссылке, вы найдете файл JSON, содержащий метаданные BAYC #0:
Эти атрибуты легко проверить в OpenSea :
Следует также отметить, что некоторые структуры данных просто невозможно реализовать в EVM из-за стоимости хранения. Например, представление графа с использованием матрицы смежности было бы совершенно невозможным из-за его пространственной сложности O(V²).