Hовая версия движка (2.4.0) — Aurora продолжает расширять возможности масшабирования EVM
Несмотря на неприметный номер версии, этот релиз оказался очень значимым для нескольких наших партнеров, и, таким образом, помог и продолжает помогать миссии команды Aurora по расширению экосистемы Ethereum. Итак, начнем!
Мы рады объявить о выходе обновления движка EVM Aurora, версии 2.4.0. Несмотря на неприметный номер версии, этот релиз оказался очень значимым для нескольких наших партнеров, и, таким образом, помог и продолжает помогать миссии команды Aurora по расширению экосистемы Ethereum. Итак, начнем!
В этой статье:
- Краткое напоминание
- Почему это обновление важно и для пользователей, и для разработчиков?
- Aurigami
- Технические подробности
1) Краткое (и полезное) напоминание
Aurora построена на блокчейне NEAR, который во многом отличается от Ethereum 1.0. Для нашей сегодняшней темы важно следующее отличие: протокол NEAR ограничивает количество действий, которые может выполнять каждая транзакция, в то время как протокол Ethereum устанавливает ограничения только для каждого блока (хотя пользователи могут устанавливать ограничения для своих индивидуальных транзакций).
Следовательно, Aurora должна (в теории) вместить целый блок Ethereum в одну транзакцию NEAR. Это довольно сложно, но в версии движка 2.4.0 мы оптимизировали производительность, что приближает нас к этой амбициозной цели.
2) Почему это обновление важно и для пользователей, и для разработчиков?
Многие defi-проекты сталкивались с ошибкой Exceeded the maximum amount of gas allowed to burn per contract
(«Превышено максимальное количество газа, разрешенного для сжигания на один контракт»), и были сбиты с толку, потому что «газ» и «контракт» — это термины Ethereum.
Однако на самом деле эта ошибка исходила от транзакции NEAR. Под «газом» подразумевался газ NEAR (не газ EVM), а «контракт» относился к смарт-контракту движка Aurora на NEAR. Учитывая этот контекст, смысл сообщения об ошибке становится ясен: есть максимальное количество газа, которое контракт может сжечь за одну транзакцию, и при попытке провести эти defi-транзакции, наш движок превышает его.
А теперь отличные новости. С выходом версии движка 2.4.0 проблема постепенно решается — мы уменьшили потребление газа движком в 2 раза, что открывает некоторые очень важные варианты использования для разработчиков и облегчает жизнь пользователям, т.к. теперь вероятность случайно превысить лимит вдвое ниже.
В частности, объем вычислений EVM, который может поместиться в одну транзакцию, теперь достаточен, чтобы разблокировать некоторые варианты использования defi, которые ранее были просто невозможны в Aurora.
3) Aurigami
Мы хотим поблагодарить Aurigami, одного из наших defi-партнеров, за предоставление ресурсов для разработки, которые помогли нам найти дополнительные возможности для оптимизации, что в итоге привело к этому новому релизу.
Благодаря выходу движка 2.4.0 Aurigami смогли запуститься в тестовой сети Aurora. Поскольку они являются платформой для заимствования и кредитования, их система должна работать максимально гладко и бесперебойно, и нынешнее обновление было для этого необходимо. В глобальном смысле это еще более важно, поскольку такой тип платформ имеет решающее значение для децентрализованной финансовой инфраструктуры экосистемы Aurora.
Мы рады видеть, что наша экосистема продолжает расти, и с нетерпением ждем будущих обновлений, ведь они откроют еще больше вариантов использования defi, которые вы знаете и любите в Ethereum!
4) Технические подробности
Если вы все еще здесь, это здорово, потому что впереди самое интересное! Перейдем к технической части.
Начнем с краткого обзора того, что такое движок Aurora и как он работает. Наш движок — это написанный на Rust смарт-контракт на блокчейне NEAR. Он содержит полный интерпретатор EVM, чтобы иметь возможность выполнять транзакции точно так же, как Ethereum, а также всю вспомогательную логику для валидации транзакций перед выполнением (проверку подписи, одноразового номера (nonce), баланса счета по сравнению с ценой газа и т. д.).
Когда вы отправляете транзакцию на конечную точку Aurora RPC, наша инфраструктура превращает для движка вашу подписанную транзакцию Ethereum в транзакцию NEAR. Это означает, что каждая транзакция Aurora становится транзакцией NEAR и, следовательно, должна следовать правилам протокола NEAR. Как упоминалось выше, именно здесь возникает проблема газа NEAR (а не Ethereum, как объяснялось ранее).
Естественно, возникает вопрос: «Как сделать движок более эффективным, чтобы он мог выполнять больше работы EVM при том же количестве газа NEAR?»
Это непростой вопрос (и мы продолжаем работу по оптимизации), но для успеха Aurora он является важнейшим. К счастью для нас, основные разработчики NEAR и разработчики Aurigami, одного из заинтересованных defi-проектов, помогли нам с поиском ответов.
Оказалось, что есть несколько простых решений, которые в комплексе значительно меняют ситуацию.
-
Обновление до более свежей версии Rust Компилятор Rust хорош в оптимизации производительности кода и постоянно совершенствуется. Простой переход на более позднюю версию Rust уже дал примерно 1% улучшения практически без дополнительных усилий.
-
Использование обратного (little-endian) представления чисел в стеке EVM Технически EVM рассчитана на использование кодировки с прямым порядком байтов (big-endian), однако в большинстве современных архитектур используется обратный (little-endian). Постоянное переключение порядка байтов чисел, входящих и выходящих из стека для арифметических операций, было довольно затратным по сравнению с использованием обратного порядка байтов в стеке и переключением на прямой только при необходимости (например, при записи в хранилище или возврате результата).
-
Упрощение проверки является ли аккаунт пустым Аккаунт Ethereum считается «пустым», если его одноразовый номер (nonce) и баланс равны 0 и на нем не развернут код. Соответственно, если хотя бы одно из этих условий не выполняется, нам не нужно проверять остальные.
-
Кэширование значений, считываемых из состояния NEAR контрактом движка (#438) (#446) Это улучшение оказало наибольшее влияние на расход газа движком. Ниже мы подробнее поговорим о причинах.
Все эти изменения вместе привели к тому, что движок стал потреблять примерно в два раза меньше газа NEAR, чем потреблял раньше при выполнении определенных defi-транзакций. Этого двукратного улучшения было достаточно, чтобы Aurigami смогли запуститься в тестовой сети Aurora.
Чтобы понять, почему кэширование было так важно, нам нужно немного узнать о том, как NEAR представляет состояние сети и как определяется стоимость газа. Как и многие блокчейны, NEAR использует trie (префиксное дерево) для хранения состояния, поскольку это позволяет создавать довольно компактные доказательства того, что хранится в блокчейне. Каждый контракт в NEAR имеет собственное хранилище «ключ-значение» для своего состояния, и оно встроено в полное trie состояния NEAR.
NEAR очень аккуратно устанавливает стоимость газа. Цель состоит в том, чтобы гарантировать время финализации блока в 1 секунду, что обеспечивает UX, схожий с web2, с почти мгновенными подтверждениями транзакций. Для этого ни один блок не должен обрабатываться более 1 секунды, и, в частности, все транзакции в блоке тоже должны укладываться в это время. Поскольку газ — это мера вычислительной работы, идея проста: нужно убедиться, что стоимость газа тщательно вымеряется таким образом, чтобы 1 секунда работы, выполняемой транзакцией, соответствовала фиксированному количеству газа, а затем установить лимиты намного ниже этого значения.
Чтобы обеспечивалась такая строгая связь между стоимостью газа и реальным временем, практически у всех операций, которые транзакция может заставить ноду выполнять, должна быть отдельная стоимость. Например, есть стоимость read_base
— стоимость выполнения любого чтения состояния контракта, а есть еще read_byte
— стоимость за каждый прочитанный байт сверх базовой стоимости. Возвращаясь к деталям состояния, есть также стоимость touching_trie_node
— стоимость, добавляемая для каждой ноды префиксного дерева состояния, к которой необходимо обратиться, чтобы прочитать или записать значение из хранилища.
Как оказалось, эти затраты на I/O составляли значительную часть всего газа, используемого движком, в некоторых случаях более 50%. Поэтому уменьшение необходимых операций чтения состояния благодаря введению кэширования было чистым выигрышем. Несмотря на то, что само кэширование имеет свою стоимость (не говоря о том, что объем используемой им памяти также имеет стоимость), это все равно меньше суммы, сэкономленной за счет сокращенного количества операций чтения.
Помимо этого, оказалось, что некоторые типы значений требуют другой логики кэширования. Одно конкретное значение — «generation» — является деталью реализации движка. С каждым адресом связано такое значение. Это значение позволяет нам «удалить» весь адрес без особых затрат на I/O — мы просто увеличиваем generation и игнорируем любое состояние, связанное с более низкими значениями generation.
Это означает, что чтение значения из хранилища контракта EVM (которое выглядит как однократное чтение для разработчиков на уровне Solidity) оказалось многократным, потому что движку нужно сначала прочитать generation. Однако generation не меняется в ходе транзакции (если аккаунт удаляется и создается заново в той же транзакции, эти изменения на самом деле кэшируются в памяти интерпретатором EVM, поэтому значение generation, записанное в состоянии NEAR, не меняется в течение транзакции). Более того, одна транзакция, как правило, не затрагивает множество разных адресов (даже сложные defi-транзакции редко вызывают более 10 различных контрактов EVM). Поэтому мы можем кэшировать соответствие адреса с generation при каждом чтении, и именно так это было реализовано.
С другой стороны, сам поиск в хранилище контрактов EVM не может быть полностью кэширован без значительных накладных расходов, во-первых, поскольку значения потенциально могут быть намного больше, чем 32-битное число, представляющее generation, и во-вторых, потому что ключей может быть гораздо больше. Если транзакция вызывает 10 различных контрактов, и каждый из них должен считать 10 значений из хранилища, то получится 100 различных ключей для кэширования вместе с их значениями.
Кроме того, неясно, полезно ли это кэширование, так как определенное значение из хранилища контракта не может быть прочитано несколько раз, тогда как значение generation для адреса, по сути, гарантировано потребуется нам несколько раз. Таким образом, тот же вид кэша, который мы использовали для значений generation, не подходил для более общего кэширования. Зато для этого отлично подошел другой тип кэша. Из-за определенного недостатка интерпретатора EVM (с чем мы еще будем разбираться в будущем) оказалось, что чтение значения, которое на уровне Solidity выглядело однократным чтением, на уровне движка оказалось последовательными повторными чтениями этого значения. Были примеры, когда одно и то же значение можно было прочитать 3 или 4 раза подряд! К счастью, проблема последовательных повторных чтений может быть решена с помощью самого простого LRU-кэша размера 1, что и было реализовано.
На сегодня все, спасибо всем, кто дочитал! Надеюсь, вам понравился этот детальный рассказ о движке Aurora. Мы продолжаем работать над оптимизацией и расширять возможности Aurora, чтобы открывать для вас все больше и больше вариантов использования. Если вы владеете Rust, и вам интересен наш проект, загляните на нашу страницу вакансий — будем рады сотрудничеству.