May 19, 2022

EVM для задротов 4

Всем привет! С вами Тёма!

Сегодня мы продолжим углубляться в EVM и пытаться в нем разобраться!

По своей сути данная статья является переводом и выжимкой вот этого парня ТЫК

Так же хочу предупредить, новичкам в программировании скорее всего будет сложно понять происходящее, поэтому если вы действительно хотите все впитать, то читайте неспешно и вместе с гуглом

А перед прочтением обязательно ознакомьтесь с:

  • первой частью тут - ТЫК
  • со второй частью тут - ТЫК
  • с третьей статьей тут - ТЫК

Начнем!

Мы собираемся изучить архитектуру Ethereum, его структуры данных и заглянуть внутрь клиента «Go Ethereum» (Geth)

Мы начнем с данных, содержащихся в блоке Ethereum, а после перейдем к хранилищу конкретного контракта. Мы закончим рассмотрением реализации опкодов SSTORE и SLOAD в Geth

Архитектура эфира

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

Вместо того, чтобы рассматривать диаграмму в целом, мы проанализируем ее по частям. А пока давайте сосредоточимся на «Block N Header» и содержащихся в нем полях

Block Header

Хэдер блока содержит ключевую информацию о блоке Ethereum. Ниже приведен фрагмент «Block N Header» вместе с полями данных. Взгляните на этот блок 14698834 на etherscan и посмотрите, какие поля вы там видите

Хэдер блока содержит следующие поля:

  • Prev Hash — хеш Keccak родительского (прошлого) блока
  • Nonce — используется для доказательства выполнения работы
  • Timestamp — время записи блока по UNIX
  • Uncles Hash — хэш Keccak uncle-блоков (тут подробнее о них)
  • Beneficiary — адрес майнера, который смайнил блок и который соответсвенно будет вознагражден
  • LogsBloom — фильтр Блума из двух полей, адреса и лога в квитанциях
  • Difficulty — Скалярное значение сложности предыдущего блока
  • Extra data — 32 байта доп данных, относящихся к этому блоку
  • Block Num — скалярное значение количества блоков-предков
  • Gas Limit — Скалярное значение текущего лимита использования газа на блок
  • Gas Used — скалярное значение общего количества газа, потраченного на транзакции в этом блоке
  • Mix Hash — 256-битное значение, используемое с одноразовым номером для доказательства выполнения вычислений
  • State Root — Keccak хэш корневого узла дерева состояний (после выполнения)
  • Transaction Root — Keccak хэш корневого узла дерева транзакций
  • Receipt Root — Keccak хэш корневого узла дерева квитанций

Более подробно о всех этих полях - ТУТ

Давайте посмотрим, как эти поля соотносятся с кодовой базой клиента Geth. Мы рассмотрим структуру «Header», определенную в block.go, которая представляет заголовок блока.

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

Для этого нам нужно сосредоточиться на поле «State root» в заголовке блока

State root

«State root» действует как дерево Меркла (тут подробнее) в том смысле, что это хэш, который зависит от всех фрагментов данных, которые лежат под ним. Если какая-либо часть данных изменится, то корень также изменится.

Структура данных под «State root» представляет собой Merkle Patricia Trie, в которой хранится пара «ключ-значение» для каждой учетной записи Ethereum в сети, где ключ — это адрес ethereum, а значение — объект учетной записи Ethereum (На самом деле ключ — это хэш адреса Ethereum, а значение — это учетная запись Ethereum, закодированная RLP, однако пока мы можем пропустить это)

Ниже приведен раздел диаграммы «Ethereum Architecture», который представляет Merkle Patricia Trie для «State root»

Merkle Patricia Trie — это нетривиальная структура данных, поэтому в этой статье мы не будем углубляться в нее (но можно дополнительно посмотреть тут). Вместо этого мы можем изучить модель mapping ключа адреса с учетной записью Ethereum

Ethereum Account

Учетная запись Ethereum — это согласованное представление адреса Ethereum. Он состоит из 4 предметов:

  • Nonce - количество транзакций, совершенных аккаунтом
  • Balance - баланс счета в Wei
  • Code Hash — хэш байт-кода, хранящегося в контракте/аккаунте.
  • Storage Root — Keccak хэш корневого узла хранилища trie (после выполнения)

Мы видим все это в данном фрагменте исходного изображения архитектуры Ethereum

Опять же, мы можем перейти к кодовой базе Geth и найти соответствующий файл state_account.go и структуру, определяющую «Ethereum account», называемую StateAccount

Мы снова видим, что значения, указанные в кодовой базе, соответствуют нашей схеме

Далее нам нужно углубиться в поле «Storage Root» в учетной записи Ethereum

Storage Root

Storage Root очень похож на State Root тем, что под ним находится еще одно Merkle Patricia trie

Разница в том, что на этот раз ключи — это слоты для хранения, а значения — это данные в каждом слоте

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

Ниже приведен раздел диаграммы «Ethereum Architecture», который представляет Merkel Patricia Trie для «Storage Root»

Как и прежде, «Storage Root» — это корневой хеш, на который влияет изменение каких-либо базовых данных (контрактное хранилище)

Любое изменение в хранилище контракта повлияет на «Storage Root», который, в свою очередь, повлияет на «State Root», который, в свою очередь, повлияет на «Block Header»

На этом этапе статьи мы достигли нашей цели — перенеслись с блока эфириума на хранилище отдельного контракта!

Следующая часть статьи — это глубокое погружение в кодовую базу Geth. Мы кратко рассмотрим, как инициализируется хранилище контрактов и что происходит, когда вызываются коды операций SSTORE и SLOAD

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

StateDB → stateObject → StateAccount

Для начала нам нужен новый контракт. Совершенно новый контракт означает совершенно новый StateAccount

Прежде чем мы начнем, есть 3 структуры, с которыми мы будем взаимодействовать:

  • StateAccount

StateAccount — это представление Ethereum для «Ethereum accounts»

  • stateObject

StateObject представляет «Ethereum account», которая модифицируется

  • StateDB

StateDB в протоколе Ethereum используются для хранения чего-либо в дереве Меркла. Это общий интерфейс запроса для получения контрактов и Ethereum accounts

Давайте посмотрим, как эти 3 элемента взаимосвязаны и как они соотносятся с тем, что мы обсуждали

  1. В структуре StateDB мы видим, что в ней есть поле stateObjects, которое представляет собой mapping адресов с stateObjects (помните, что “State Root” Merkle Patricia Trie представлял собой mapping адресов Ethereum с учетными записями Ethereum, а stateObject — это учетная запись Ethereum, которая модифицируется)
  2. stateObject, мы видим, что в нем есть поле данных типа StateAccount (помните, что ранее в этой статье мы сопоставили учетную запись Ethereum с StateAccount в Geth)
  3. Структура StateAccount, мы уже видели эту структуру, она представляет учетную запись Ethereum, а поле Root представляет «Storage Root», который мы обсуждали ранее

На этом этапе некоторые части головоломки начинают собираться вместе. Теперь у нас есть контекст, чтобы увидеть, как инициализируется новый «Ethereum account» (StateAccount)

Инициализация новой учетной записи Ethereum (StateAccount)

Чтобы создать новый StateAccount, нам нужно будет взаимодействовать с файлом statedb.go и структурой StateDB

В StateDB есть функция createObject, которая создает новый объект stateObject и передает в него пустой StateAccount. Это фактически создает пустой «Ethereum account»

На приведенной ниже диаграмме показана логика кода

  1. В StateDB есть функция createObject, которая принимает адрес Ethereum и возвращает stateObject (помните, что stateObject представляет изменяемую учетную запись Ethereum)
  2. Функция createObject вызывает функцию newObject, передавая в stateDB адрес и пустой StateAccount (помните, что StateAccount = учетная запись Ethereum), она возвращает stateObject
  3. В возвращаемых значениях функции newObject мы видим ряд полей, связанных с stateObject, адресом, данными, dirtyStorage и т.д.
  4. Поле данных stateObject сопоставляется с пустым входом StateAccount в функции. Обратите внимание, что нулевые значения заменяются в StateAccount в строках 103–111
  5. Возвращается stateObject, который содержит инициализированный StateAccount в качестве поля данных

Итак, у нас есть пустой stateAccount, что нам делать дальше?

Мы хотим сохранить некоторые данные, и для этого нам нужно использовать код операции SSTORE

SSTORE

Прежде чем мы углубимся в реализацию SSTORE в Geth, давайте быстро напомним себе, что делает SSTORE

Он извлекает 2 значения из стека, сначала 32-байтовый ключ, затем 32-байтовое значение и сохраняет это значение в указанном слоте памяти, определяемом ключом

Ниже приведен поток кода Geth для кода операции SLOAD, давайте посмотрим, что он делает

  1. Начнем с файла instructions.go, который определяет все опкоды EVM. В этом файле мы находим функцию «opSstore»
  2. Переменная scope, которая передается в функцию, содержит данные контракта, такие как стек, память и т. д. Мы извлекаем 2 значения из стека и помечаем их в loc (сокращение от location) и в val (сокращение от value)
  3. Затем два значения, извлеченные из стека, используются в качестве входных данных вместе с адресом контракта для функции SetState, связанной с StateDB. Функция SetState использует адрес контракта, чтобы проверить, существует ли объект stateObject для этого контракта, и если нет, то он будет создан. Затем он вызывает SetState для этого stateObject, передавая в базу данных StateDB ключ и значение
  4. Для StateObject функция SetState выполняет некоторые проверки fake storage и того, изменилось ли значение, а затем запускает добавление журнала
  5. Если вы посмотрите на комментарий кода о структуре журнала, то увидите, что журнал используется для отслеживания изменений состояния, чтобы их можно было отменить в случае исключения выполнения или запроса на изменение
  6. После обновления журнала вызывается для storageObject функцию setState с ключом и значением. Это обновляет dirtyStorage в storageObject

Итак, мы обновили dirtyStorage в stateObject с ключом и значением. Что это на самом деле означает и как это связано со всем, что мы узнали до сих пор

Начнем с определения dirtyStorage в коде

  1. dirtyStorage определяется в структуре stateObject, имеет тип Storage и описывается как «Записи хранилища, которые были изменены в ходе выполнения текущей транзакции»
  2. Тип хранилища, соответствующий dirtyStorage, представляет собой простой mapping common.Hash с common.Hash
  3. Hash type — просто массив байтов длины HashLength
  4. HashLength — константа, определенная как 32

Вам должен быть знаком mapping с 32-байтовым ключом и 32-байтовым значением. Именно так мы концептуально рассматривали хранилище контрактов в "EVM для задротов 3"

Возможно, вы заметили pendingStorage и originStorage в stateObject прямо над полем dirtyStorage. Все они связаны, во время финализации dirtyStorage копируется в pendingStorage, который, в свою очередь, копируется в originStorage при обновлении дерева

После обновления дерева «Storage Root» в StateAccount, также будет обновлен во время «коммита» StateDB. Это записывает новое состояние в базу данных дерева в памяти

SLOAD

Снова давайте быстро напомним себе, что делает SLOAD

Он извлекает 1 значение из стека, 32-байтовый ключ, который представляет слот для хранения и возвращает хранящееся там 32-байтовое значение

Ниже приведен поток кода Geth для кода операции SLOAD, давайте посмотрим, что он делает

  1. Опять же, мы начинаем с файла instructions.go, где мы можем найти функцию «opSload». Мы берем местоположение (слот для хранения) для SLOAD с вершины стека, используя peek
  2. Мы вызываем функцию GetState для StateDB, передавая адрес контракта и место хранения. GetState получает объект stateObject, связанный с этим адресом контракта. Если stateObject не равен нулю, он вызывает GetState для этого stateObject
  3. Функция GetState для stateObject выполняет проверку fakeStorage, а затем проверяет dirtyStorage
  4. Если dirtyStorage существует, то вернет значение положения ключа в mapping dirtyStorage (dirtyStorage представляет самое актуальное состояние контракта, поэтому мы пытаемся сначала вернуть его)
  5. В противном случае вызовет функцию GetCommitedState, чтобы найти значение в дереве хранилища. Снова проверяется наличие fakeStorage
  6. Если pendingStorage существует, то вернет значение по местоположению ключа в pendingStorage mapping
  7. Если ничего из вышеперечисленного не вернулось, то перейдите в originStorage и извлечет значение оттуда

Вы заметите, что функция сначала пыталась вернуть dirtyStorage, затем pendingStorage, а только потом originStorage. Это имеет смысл, поскольку во время выполнения dirtyStorage является наиболее актуальным mapping, за которым следует pendingStorage, а затем originStorage

Одна транзакция может манипулировать одним слотом хранилища несколько раз, поэтому мы должны убедиться, что у нас есть самое последнее значение

Давайте представим, что SSTORE происходит перед SLOAD, в том же слоте и в той же транзакции. В этой ситуации dirtyStorage будет обновлен в SSTORE, а в SLOAD будет возвращен

Теперь у вас есть понимание того, как SSTORE и SLOAD реализованы на уровне клиента Geth. Как они взаимодействуют с объектами состояния и хранилища, а так же как обновление слота хранилища связано с «мировым состоянием» Ethereum

Это было интенсивно, но ты справился. Я предполагаю, что эта статья оставила у вас больше вопросов, чем было до того, как вы начали, но это часть удовольствия от криптодева)

Надеюсь статья была интересной и понятной!

Мой телеграмм канал - https://t.me/ortomich_crypto