Мы изучаем данные Ethereum и проблемы их использования.

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

Понимание виртуальной машины Ethereum (EVM)
Что такое EVM?
Как поддерживается состояние EVM?
Что не данные EVM существуют?

Проблемы с использованием данных Ethereum
Код контракта в сети представляет собой скомпилированный байт-код
Исходный код контракта часто закрыт
Контракты вызывают другие контракты (и наблюдаемость плохая)
Код контракта больше не является неизменным
Хранилище контрактов может быть трудно исследовать
— «Если это только что произошло, это может не Произошло"

Заключение

Понимание виртуальной машины Ethereum (EVM)

Что такое ЭВМ?

Виртуальную машину Ethereum (EVM) иногда называют мировым компьютером. Тысячи узлов по всему миру выполняют один и тот же код для поддержания одного и того же состояния.

Как поддерживается состояние EVM?

Рисунок 1 иллюстрирует управление состоянием EVM и структуры данных. Мы начинаем с произвольного состояния n-1, в котором экземпляр EVM имеет три типа данных состояния:

  1. Остатки на счетах (в ETH) и одноразовые номера. На рисунке 1 показаны два адреса (0xa5…37 и 0x93…af), каждый из которых содержит 5 ETH. У каждого адреса также есть одноразовый номер, который не показан, который представляет количество его транзакций.
  2. Код контракта. На рисунке 1 показан псевдокод для простого контракта, развернутого по адресу 0x93…af, который поддерживает переменную хранения x и увеличивает ее каждый раз, когда учетная запись получает перевод не менее 1 ETH. .
  3. Контрактное хранение. На рис. 1 показано хранилище контрактов для 0x93…af в виде словаря пар ключ-значение, включая запись x : 1.

Давайте рассмотрим пример. Аккаунт 0xa5…37 отправляет 1 ETH на контракт, 0x93…af, а отправленная/ожидающая транзакция находится в мемпуле, пока не будет включена в блок. Примерная транзакция стоит 0,1 ETH в газе. Часть этого сжигается согласно EIP1559. После того как предлагающий создаст Block n, валидаторы проверят его законность. Новый блок распространяется по сети, и принимающие узлы затем запускают код, запускаемый включенными транзакциями, чтобы обновить свое состояние до State n. Поскольку был получен 1 ETH, x увеличивается в хранилище с 1 до 2, а в журнале квитанции, связанной с транзакцией, создается событие. При переходе из состояния n в состояние n+1 отправляется только 0,5 ETH. Код контракта выполняется, но не увеличивает x и не генерирует событие, поскольку не выполняется условие if.

Мы рекомендуем Погружение в мировое состояние Эфириума Тимоти МакКаллума и Углубление в Эфириум: как данные хранятся в Эфириуме? васа.

Какие негосударственные данные EVM существуют?

Блоки содержат транзакции, которые вызывают изменение состояния. Может быть интуитивно понятно, что блок похож на diff, используемый в программном обеспечении git, поскольку он записывает набор изменений. При этом это несколько неполное различие: блок записывает только транзакции, которые инициируют изменения состояния, а не влияние выполнения на состояние, которое может включать множество внутренних транзакций между контрактами.

Экземпляры EVM генерируют квитанцию ​​для каждой обработанной транзакции. Хотя квитанции находятся вне цепочки и хранятся на каждом узле, их можно проверить: хэш каждой квитанции включается в блок. Квитанция, в свою очередь, содержит журналы/события, которые представляют собой данные, испускаемые смарт-контрактами, вызываемыми в транзакциях. В примере кода контракта emit Event создаст журнал с данными, указанными в смарт-контракте.

Мы рекомендуем Документацию по EVM от Ethereum.org; Эфириум, смарт-контракты и мировой компьютер от ConsenSys; и страницу О EVM на сайте evm.codes.

Проблемы с использованием данных Ethereum

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

Можно было бы ожидать, что будет легко получить доступ и интерпретировать все данные в общедоступной цепочке блоков, такой как Ethereum. Увы, это не так. Давайте рассмотрим, насколько сложны эти распределенные системы.

Код контракта в цепочке представляет собой скомпилированный байт-код.

Разработчики смарт-контрактов пишут удобочитаемый код, часто с богатыми комментариями и спецификациями, такими как Natspec, на таких языках, как Solidity (подобный JS) и Vyper (Pythonic). Есть также опции более низкого уровня, такие как Юл(+).

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

Исходный код контракта часто является закрытым

Разработчик смарт-контрактов может сделать свои контракты Solidity/Vyper/Yul общедоступными, отправив их в такие сервисы, как Etherscan, которые компилируют отправленные материалы и проверяют, соответствует ли полученный байт-код тому, что находится в состоянии EVM. Но многие разработчики сохранят исходный код своего смарт-контракта в тайне либо потому, что видят ограниченные преимущества в его обнародовании, либо потому, что опасаются, что исходный код может выявить возможные проблемы безопасности или эксплойты.

Другой объект исходного кода контракта, который может быть или не быть общедоступным, — это Двоичный интерфейс приложения (ABI), который сообщает другим службам, как они могут вызывать контракт и как интерпретировать его события/журналы. Без ABI может быть сложно вызвать контракт или интерпретировать его журналы. Вы можете угадать хотя бы частичный ABI для контракта: определите его общую цель и предположите, что его функции и события такие же, как и у стандартных шаблонов контрактов (например, ERC20, ERC721) для этой цели. Или вы можете использовать базу данных сигнатур функций, например sig.eth.samczsun или 4byte.directory. Также можно вывести части ABI путем анализа дизассемблированного байт-кода (например, whatsabi), если байт-код был сгенерирован стандартным компилятором. Вот пример контракта, написанного на Yul, который был эксплуатирован за ~$1 млн злоумышленником, который реконструировал декомпилированную сборку, чтобы найти неиспользуемую функцию утверждения без стандартных проверок. Таким образом, вы можете вызывать функции и интерпретировать журналы, пока вы можете работать с частями ABI, которые соответствуют этим функциям и событиям.

Контракты вызывают другие контракты (и наблюдаемость плохая)

Каждое вычисление смарт-контракта на EVM начинается с подписанной транзакции. В настоящее время только внешние учетные записи (EoAs) могут подписывать транзакции. Но смарт-контракт, вызываемый транзакцией, может вызывать другие контракты, которые, в свою очередь, могут вызывать другие контракты. Эти вызовы между контрактами часто называют внутренними транзакциями. Практически любая умеренно сложная система смарт-контрактов будет включать внутренние транзакции.

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

Есть несколько вариантов этих схем: Можно использовать прокси исключительно как обновляемый реестр, который перенаправляет пользователя на другой контракт. Альтернативный подход заключается в использовании шаблона DELEGATECALL, в котором код из вызываемого контракта выполняется в хранилище вызывающего контракта, что позволяет обновлять функциональные возможности в том же хранилище состояния.

Важно отметить, что эти внутренние транзакции не записываются явно в блоки. Для их восстановления необходимо использовать метод debug_traceTransaction или настроить собственный клиент EVM. Последний подход используют такие сервисы, как Etherscan и Tenderly.

Мы рекомендуем DelegateCall: вызов другой функции контракта в Solidity от zeroFruit, а также Доступ к внутренним транзакциям контракта и Инструментирование EVM на Ethereum. Обмен стеками.

Код контракта больше не является неизменным

Исходный код CREATE Ethereum развертывает контракт на адрес на основе хэша, который включает адрес развертывателя и его одноразовый номер, количество транзакций, инициированных учетной записью. Средства могут быть отправлены на контракт до или после его развертывания. Код контракта и хранилище могут быть удалены, а баланс отправлен назначенной стороне, если вызывается инструкция SELFDESTRUCT, но ничего нельзя повторно развернуть по этому адресу с помощью CREATE, потому что одноразовый номер исходного развертывателя увеличился бы.

Обновление Constantinople представило CREATE2, позволяющее разработчикам развертывать контракт по детерминированному адресу, если на этом адресе еще нет развернутого кода. Он похож на CREATE тем, что включает адрес развертывателя в хэш для адреса развертывания, но отличается тем, что одноразовый номер развертывателя заменяется кодом инициализации контракта и выбранным пользователем начальным числом («солью»). Контракты CREATE никогда не будут конфликтовать с контрактами CREATE2 из-за разделения доменов: CREATE2 помещает 0xFF в качестве первого входного байта в хеш.

CREATE2 также позволяет «метаморфические контракты», когда код по заданному адресу может быть изменен. Рисунок 3 и приведенные ниже инструкции иллюстрируют логику:

  1. Создайте фабрику смарт-контрактов, т. е. контракт, который создает и развертывает контракты, чтобы служить вашим развертывателем для метаморфического контракта. Дайте этой фабрике возможность развернуть самоуничтожающийся метаморфический контракт с помощью CREATE2. Код инициализации метаморфического контракта, созданного фабрикой, должен указать ей скопировать код контракта реализации, адрес которого указан в хранилище фабрики.
  2. Создайте контракт реализации v1 с кодом, который вы хотите использовать в метаморфическом контракте. Поместите адрес контракта на реализацию v1 в хранилище фабрики. Используйте фабрику смарт-контрактов для развертывания метаморфического контракта, который вызовет фабрику в своей инициализации, чтобы найти адрес контракта реализации, код которого будет скопирован с помощью EXTCODECOPY.
  3. Если вы хотите обновить реализацию, то есть преобразовать метаморфический контракт: а) разверните новую логику в контракте реализации v2, б) вызовите SELFDESTRUCT в метаморфическом контракте, в) замените указатель из контракта реализации v1 на v2 в хранилище фабрики, d ) передислокируйте метаморфический контракт с фабрикой. Поскольку деплойер, код инициализации и соль остаются неизменными, новый код из контракта реализации v2 будет развернут по тому же адресу, что и ранее, что приведет к метаморфозе.

CREATE2 также имеет серьезные последствия для безопасности: пользователь может быть уверен в влиянии своей транзакции с определенным адресом контракта только для того, чтобы обнаружить, что код по этому адресу был обновлен. Существует также некоторый риск этого с прокси-структурами, но прокси-структуры более четкие / более обнаруживаемые, поскольку пользователь может, по крайней мере теоретически, видеть, что они указывают на новые обновления.

Вы даже не можете чувствовать себя в полной безопасности с контрактом, созданным с помощью CREATE. Вызовите этот контракт C и предположим, что он был создан с помощью CREATE по контракту B, который, в свою очередь, был создан с помощью CREATE2 по контракту A. Владелец A может обновить B с помощью SELFDESTRUCT и CREATE2, сбрасывая его одноразовый номер в процессе. Затем одноразовый номер B может быть увеличен до того уровня, в котором он был при создании C, а затем C может быть обновлен с помощью SELFDESTRUCT и CREATE, вызванных B. С этими «каскадными воскрешениями» вы подвергаетесь некоторому риску неожиданных обновлений контракта, если вы повторное взаимодействие с любым контрактом, который находится ниже по течению от контракта, созданного с помощью CREATE2, что нелегко обнаружить.

Эти и другие соображения мотивируют EIP-4758, текущее предложение деактивировать SELFDESTRUCT, изменив его на SENDALL, что позволит восстановить средства без удаления кода или хранилища. Он является кандидатом на включение в предстоящий шанхайский форк, и на форуме Ethereum Magicians идет увлекательная дискуссия.

Мы рекомендуем Инструмент для обнаружения метаморфических смарт-контрактов Майкла Блау; Не все коды равны для Create2 Майкла Фрёвиса и Райнера Бёме, The Promise and Peril of Metamorphic Contracts от 0age, A postmortem on the Parity Multi-Sig Library Self-Destruct от Parity Technologies и Мультиподписной взлом Parity, в результате которого было украдено 153 037 ETH от Parity Hack Trace.

Контрактное хранилище может быть трудно исследовать

Хранилище контрактов — это словарь пар ключ-значение. Слоты обычно назначаются переменным в том порядке, в котором они появляются в коде контракта, с некоторыми нюансами, связанными с массивами динамического размера, отображениями и вложенностью типов. Даже если вы знаете исходный код контракта, вы часто не можете получить полную версию состояния контракта, копаясь в хранилище, если вы точно не знаете, что ищете. Чтобы интерпретировать слоты хранилища в прокси-контрактах, вам нужно знать исходный код контракта реализации на момент интересующего вас изменения хранилища. Если вы не знаете исходный код, вы действительно будете блуждать слепо. Представление переменной в хранилище зависит как от ее типа, так и от компилятора языка высокого уровня. Для солидности:

  • Однозначные переменные фиксированного размера:Если первые две объявленные переменные в контракте являются однозначными и их сумма размеров превышает 32 байта, их можно получить через вызов RPC к любому узлу Ethereum с eth_getStorageAt(contract_address, 0x…0) и eth_getStorageAt(contract_address, 0x…1) соответственно. Две переменные с общим размером меньше или равным 32 байтам будут упакованы вместе в первый слот памяти.
  • Однозначные переменные динамического размера:строка и байты не ограничены 256 битами одного слота хранилища. Если длина переменной с динамическим размером меньше или равна 31 байту, ее значение упаковывается вместе с длиной в слот хранения: значение сохраняется в старших байтах и ​​length * 2 хранится в младшем байте. Если размер переменной составляет 32 байта или больше, то в младшем байте слота сохраняется только length * 2 + 1, а само значение сохраняется, начиная с keccak256(storage_slot). и охватывая столько слотов хранения, сколько необходимо. Операции * 2 и * 2 + 1 над длиной спроектированы таким образом, что мы знаем, что четное значение представляет короткую переменную, которую можно получить из старших байтов тот же слот, тогда как нечетное значение предполагает, что значение переменной хранится, начиная с keccak256(storage_slot).
  • Многозначные переменные фиксированного размера. Предположим, что третья объявленная переменная — uint256[2]. Два его значения можно найти с помощью ключей 0x…2 и 0x…3 соответственно.
  • Массивы динамического размера: если следующая объявленная переменная является массивом динамического размера, размер массива сохраняется в 0x…4, а затем нулевой индекс этого элемента Массив хранится в хеше этого слота, т. е. keccak256(0x…4). Элемент индекса k этого массива хранится в keccak256(0x…4) + (k * elementSize) .
  • Сопоставления: если следующая объявленная переменная является сопоставлением, ей присваивается значение 0x…5, но слот для хранения остается пустым. Вместо этого для ключа x, определенного в сопоставлении, его значение сохраняется в слоте keccak256(x,0x…5). Обратите внимание, что это означает, что нельзя перебирать сопоставления в хранилище контрактов, не зная всех ключей, существующих в сопоставлении. Например, вы не можете определить всех держателей токенов ERC20, просматривая балансы (mapping(address, uint256)) в хранилище контрактов, потому что вам нужно знать адрес держателей токенов, чтобы найти их баланс.
  • Комбинации типов. Там, где типы рекурсивно вложены друг в друга, местоположение значения определяется путем рекурсивного применения описанных выше структур.

Мы рекомендуем Понимание хранилища смарт-контрактов Ethereum Стива Маркса; Расшифровка хранения контракта Ethereum Инука Гунавардана; и Представление данных в Solidity Гарри Альтмана.

Если бы это только что случилось, этого могло бы и не случиться

Форки и реорганизации означают, что недавние блоки, которые кажутся частью консенсуса в определенный момент времени, могут в конечном итоге быть отброшены, возможно, вместе с некоторыми транзакциями и обновлениями, которые должны быть в них указаны. На рис. 4 показан ответвление, в котором появляются два конкурирующих допустимых блока (Блок n: A и Блок n: B), которые разрешаются только тогда, когда появляется дополнительный блок на одном из блоков. две цепи. В этом случае транзакции в Блоке n: A, которые не были также включены в Блок n: B, в конечном итоге будут удалены даже после того, как некоторые узлы, просмотревшие Block n: A, поскольку консенсус мог представить пользователю транзакцию как успешную.

Форки и реорганизации бывают разной степени серьезности. Консенсусный процесс Proof of Work может столкнуться с предсказуемой неоднозначностью, когда по сети одновременно распространяются 2+ действительных конкурирующих блоков. Они могут сохраняться в течение нескольких блоков, но разрешатся без вмешательства, когда одна цепочка станет больше другой. Иногда могут возникать более длинные ответвления, если связь в сети ухудшилась.

Консенсус может вообще не работать, если на узлах в сети могут работать разные клиенты или разные версии одного и того же клиента. Хотя все они должны следовать одним и тем же правилам консенсуса, сбой консенсуса может произойти, если один клиент или версия клиента отклоняются. Одним из примеров этого был хардфорк, созданный обновлением geth в конце 2020 года.

Существуют также преднамеренные хардфорки, часто для обновления согласованных протоколов. Ethereum.org ведет их историю. Особенно спорным примером стал первый запуск Ethereum DAO в 2016 году, только для того, чтобы злоумышленник обнаружил уязвимости в его смарт-контракте и выкачал 55 миллионов долларов. Сообщество Ethereum разделилось во мнениях относительно того, как реагировать: они могли просто откатить состояние Ethereum до того состояния, в котором оно было до того, как произошло злоупотребление, тем самым оправдав жертвы и лишив нападавших их доходов. Многие использовали этот подход, но другие считали, что такие скоординированные действия против заявленного протокола консенсуса нарушают децентрализацию, лежащую в основе философии Ethereum. Таким образом, возник хард-форк, который сохраняется и по сей день, между сообществом, которое вернулось, и теми, кто отказался вернуться, где цепочка, возникшая в результате последнего, называется Ethereum Classic.

Переход Ethereum от Proof of Work к Proof of Stake существенно снизил частоту и размер реорганизаций. До слияния одна из наших нод, работающая на периферии, как правило, подвергалась реорганизации в среднем примерно раз в час. Большинство включало только один блок, но были и более длительные реорганизации с некоторой частотой. Процент блоков в цепочке консенсуса, которые явно ссылаются на реорганизованные блоки, называется ommer/uncle rate: он работал немногим более 5% до слияния. После слияния эта скорость автоматически равна нулю, поскольку блоки не ссылаются на оммеры. В течение нескольких месяцев после слияния, когда мы запускали узел на периферии, мы ежедневно сталкивались с примерно двумя реорганизациями. Каждая из реорганизаций после слияния, свидетелями которых мы были, включала один блок. Мы рекомендуем статью Сэма Льюиса Исследование причин реорганизации в Proof-of-Stake Ethereum Сэма Льюиса, чтобы узнать об этом подробнее.

Заключение

Ethereum — это общедоступный компьютер, состояние которого является результатом коллективных действий его пользователей. Хотя теоретически каждый пользователь, имеющий доступ к узлу, имеет доступ к этому состоянию, на практике при использовании этих данных возникает множество проблем.

Спасибо Yoav Weiss, Michael Zhu, Dan Boneh и членам команды smlXL за советы и отзывы для этой публикации.

Ответьте на это сообщение, если у вас есть вопросы.

Мы нанимаем!