Эта статья является частью первой из двух серий, в которых исследуется механизм хранения виртуальной машины Ethereum (EVM).

Часть 2 посвящена инструменту декодирования хранилища, написанному с использованием концепций, изложенных в этой статье.

Память Ethereum

EVM позволяет выполнять код смарт-контракта. Состояние контракта или память хранятся по адресу контракта. Это хранилище можно представить как массив, подобный структуре данных бесконечной длины, расположенный по адресу контракта. Механизм хранения гарантирует отсутствие конфликтов в местах хранения и следует набору правил. Используя эти правила, мы можем расшифровать состояние любого контракта. Для декодирования данных, хранящихся на карте, необходимо знать используемые ключи. Расшифровка данных контракта выполняется с помощью вызова RPC eth_getStorageAt.

Положение слота

Положение переменной в массиве хранения смарт-контракта определяется порядком, в котором она отображается в коде, и размером переменной. Эта позиция называется слотом. Если переменная меньше 256 бит, EVM пытается уместить более одной переменной в пространство и, следовательно, более одной переменной может занимать пространство одного слота в массиве хранения. Карта или массив всегда будут занимать один слот. Расположение элементов массивов и карт соответствует набору специальных правил хеширования, которые будут рассмотрены в этой статье. Эти правила также описаны в документации Ethereum.

В таблице ниже (таблица 1) представлена ​​краткая сводка правил распределения, которым следует EVM. Мы рассмотрим два примера контрактов и расшифруем их, используя правила, приведенные в таблице 1.

Простой пример с 256-битными переменными

Сначала давайте посмотрим на простой пример со всеми переменными 256 бит (длина 32 байта). Это позволяет нам смотреть на распределение без учета упаковки переменных.

Обратите внимание, что при применении хэша keccack к числам, число должно быть дополненным 0 64-битным значением.

Вся расшифровка выполняется с помощью RPC-вызова ethereum eth_getStorageAt, обозначенного в статье как GetStorageAt. Любая языковая оболочка, такая как nethereum или web3j, может использоваться для вызова этого RPC api.

На следующей диаграмме (рисунок 1) показано, как выполняются вызовы GetStorageAt по адресу контракта и передаваемому ему значению позиции. Цифры в левой части рисунка 1 - это позиции переменных. Для базовых типов (uint, string и т. Д.) Эту позицию можно передать в GetStorageAt для получения значений переменных. Для массива позиция вернет длину массива.

Индекс массива декодируется путем передачи хэша Keccack в GetStorageAt для индекса 0. Каждый последующий индекс массива располагается в хеш-значении, суммированном с позицией. Это можно рассматривать как доступ к указателю на массив и увеличение его позиции для поиска каждого элемента, как в C или C ++.

Карты немного сложнее. Значение позиции, передаваемое GetStoragetAt для каждого ключа, представляет собой хэш ключа keccack и позицию объявления карты. Для многомерных карт значения хэша Keccack рекурсивно вызываются для ключей и положения переменной. См. Пример на рисунке 1 для пояснения.

Теперь давайте рассмотрим пример, в котором происходит упаковка переменных. При упаковке следует помнить следующее:

  1. Это применимо только к базовым типам переменных (uint128, string, int и т. Д.) В порядке появления. EVM упакует столько переменных в 256-битное пространство в том порядке, в котором переменные перечислены в коде.
  2. Каждая переменная карты и массива займет новый слот.
  3. Отображение переменных массива будет соответствовать правилам упаковки. То есть, если размер элемента меньше 256 бит, множественный индекс массива будет занимать один слот в массиве хранения.

Эти правила также описаны в документации Ethereum.

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

Наследование

Примечание о наследовании. Когда контракт наследует другие контракты, переменные хранения базовых контрактов занимают первые слоты массива хранения в порядке наследования. После этого появятся переменные хранилища подкласса.

Заключение

Как уже отмечалось, мы можем использовать правила, описанные в этой статье, для декодирования памяти смарт-контракта Ethereum. Часть 2 расскажет об инструменте, который был написан с использованием правил, описанных в этой статье.

Получайте лучшие предложения по программному обеспечению прямо в свой почтовый ящик

Первоначально опубликовано на https://inuka.dev 30 марта 2020 г.