Понимание структур данных, из которых состоит блокчейн, помогает нам придумывать творческие способы анализа этих данных.
**Автор:**NOXX
Скомпилировать: сбросить
Навигация по цепочке данных — важный навык для любого, кто хочет понять пространство Web3. Понимание структур данных, составляющих блокчейн, помогает нам думать о творческих способах анализа этих данных. В то же время эти данные в цепочке составляют большую часть доступных данных. Этот пост будет посвящен ключевой структуре данных в EVM, квитанции транзакции и связанному с ней журналу событий.
Зачем вести журнал
Прежде чем мы начнем, давайте кратко поговорим о том, почему нам нужно использовать журналы событий как разработчику солидности:
Журнал событий является более дешевым вариантом хранения данных, к которому не нужно обращаться по контракту, а также может реконструировать сохраненное состояние, тестируя определенные переменные в смарт-контракте, индексируя переменные.
Ведение журнала событий — это способ запуска приложения Web3, прослушивающего определенный журнал событий.
Узлам EVM не нужно хранить журналы вечно, и они могут сэкономить место, удалив старые журналы. Контракты не имеют доступа к хранилищу журналов, поэтому узлам они не нужны для выполнения контрактов. С другой стороны, хранение контракта требуется для выполнения и поэтому не может быть удалено.
Эфириум блокирует Merkle Root
В части 4 мы углубились в структуру Ethereum, особенно в корневую часть состояния Merkle. State Root — это один из трех корней Merkle, включенных в заголовок блока. Два других — это Transaction Root и Receipt Root.
Для ввода данных для создания этой структуры мы будем ссылаться на блок 15001871 в Ethereum, который содержит 5 транзакций с соответствующими квитанциями и отправленными журналами событий.
заголовок блока
Мы начнем с 3 частей в заголовке блока: Корень транзакции, Корень квитанции и Цвет журналов (краткое введение в заголовок блока можно просмотреть в части 4).
Источник:
В клиенте Ethereum в разделе Transaction Root и Receipt Root Merkle Patricia Tries содержит все данные транзакций и данные квитанций в блоке. В этой статье мы сосредоточимся только на всех транзакциях и квитанциях, к которым может получить доступ узел.
Информация заголовка блока 15001871, найденная через узел Ethereum, выглядит следующим образом:
logsBloom в заголовке блока — это ключевая структура данных, о которой будет сказано далее в этой статье. Сначала давайте начнем с данных, расположенных в корне транзакции, Transaction Trie.
Транзакция дерева транзакций
Transaction Trie – это набор данных, который генерирует transactionRoot и записывает векторы запросов транзакций. Векторы запросов транзакций – это фрагменты информации, необходимые для выполнения транзакции. Поля данных, содержащиеся в транзакции, следующие:
Тип — тип транзакции (традиционная транзакция LegacyTxType, введение AccessListTxType EIP-2930, введение DynamicFeeTxType EIP-1559)
ChainId — идентификатор цепочки EIP155 транзакции
Data - входные данные транзакции
AccessList - список доступа для транзакций
Газ - лимит газа сделки
GasPrice - цена газа сделки
GasTipCap — поощрительная надбавка для майнеров, у которых единица газа за транзакцию превышает базовую плату за первый пакет, maxPriorityFeePerGas в Geth определяется EIP1559.
GasFeeCap - верхний предел комиссии газа за единицу транзакции, maxFeePerGas в Geth (GasFeeCap ≥ baseFee + GasTipCap)
Value - количество торгуемого Ethereum
Nonce - одноразовый номер создателя торгового счета
Кому — адрес получателя транзакции. Для транзакций создания контракта To возвращает нулевое значение.
RawSignaturues — Значения подписи V, R, S данных транзакции
Разобравшись с приведенными выше полями данных, давайте взглянем на первую транзакцию блока 15001871.
С помощью запроса Geth ethclient вы можете видеть, что и ChainId, и AccessList имеют «omitempty», что означает, что если поле пусто, оно будет опущено в ответе, чтобы уменьшить или сократить размер сериализованных данных.
источник кода:
Эта транзакция представляет собой перевод токенов USDT на адрес 0xec23e787ea25230f74a3da0f515825c1d820f47a. Адрес To — это адрес контракта ERC20 USDT 0xdac17f958d2ee523a2206206994597c13d831ec7. Через ВХОДНЫЕ ДАННЫЕ мы видим, что сигнатура функции 0xa9059cbb соответствует функции Transfer (Address, UINT256), а 42,251 USDT (точность 6) на 0x2b279b8 (45251000) передается на 0xEC23E787EA25230F на 0xEC23E787EA25230F. Адрес DA0F515825C1D820F47A.
Вы, возможно, заметили, что эта структура данных транзакции ничего не говорит нам о результате транзакции, так что транзакция прошла успешно? Сколько газа потребляет? Какие записи событий активируются? На этом этапе мы представим получение Trie.
Попробовать получение
Подобно тому, как в чеке о покупке записывается результат транзакции, объект в дереве чеков делает то же самое для транзакции Ethereum, но также записывает некоторые дополнительные сведения. Возвращаясь к вопросу о квитанциях о транзакциях выше, мы сосредоточимся на журналах, которые вызвали следующие события.
Снова запросите данные по цепочке 0x311b и получите квитанцию о транзакции.В это время будут получены следующие поля:
источник кода:
Type - тип транзакции (LegacyTxType, AccessListTxType, DynamicFeeTxType)
PostState(root) — StateRoot, корневой узел дерева состояний, сгенерированный после выполнения транзакции, соответствующее значение, найденное на рисунке, равно 0x, вероятно, из-за EIP98.
CumulativeGasUsed — совокупное количество газа, потребленного этой транзакцией и всеми предыдущими транзакциями в том же блоке.
Bloom(logsBloom) — фильтр Блума для журналов событий, используемый для эффективного поиска и доступа к журналам событий контрактов в блокчейне, позволяющий узлам быстро узнавать, произошло ли определенное событие в блоке, без полного разбора блока. Все поступления транзакций в блоке.
Журналы — массив объектов журнала, содержащий записи журнала, созданные событиями контракта, инициированными во время выполнения транзакции.
TxHash — хэш транзакции, связанный с чеком
ContractAddress — если транзакция предназначена для создания контракта, адрес, по которому контракт был развернут. Если транзакция не является созданием контракта, а является передачей или взаимодействием с развернутым смарт-контрактом, то поле ContractAddress будет пустым.
GasUsed - газ, потребленный этой транзакцией
BlockNumber - номер блока, в котором произошла данная транзакция
TransactionIndex — Индекс транзакции внутри блока, индекс определяет, какая транзакция выполняется первой. Эта транзакция находится в верхней части блока, поэтому индекс 0
Теперь, когда мы знаем состав квитанции транзакции, давайте подробнее рассмотрим журналы logsBloom и log array в квитанции транзакции.
Журналы событий
В коде контракта USDT в сети Ethereum мы видим, что событие Transfer объявлено в строке 86 контракта, а два входных параметра имеют ключевое слово «indexed».
(источник кода:
Когда вход события «индексирован», это позволяет нам быстро находить журналы по этому входу. Например, при использовании вышеуказанного индекса from можно получить все журналы событий типа Transfer с адресом from 0x5041ed759dd4afc3a72b8192c143f72f4724081a между блоками X и Y. Мы также можем видеть, что при вызове функции передачи в строке 138 запускается журнал событий. Стоит отметить, что текущий контракт использует более раннюю версию Solidity, поэтому ключевое слово emit отсутствует.
Вернитесь к полученным ончейн-данным:
источник кода:
Давайте углубимся в поля адресов, тем и данных.
Тема Темы
Темы — это значение индекса. Из приведенного выше рисунка видно, что в данных запроса в цепочке есть 3 индексных параметра топиков, в то время как у события Transfer всего 2 индексных параметра (от и до). Это связано с тем, что первой темой всегда является хэш сигнатуры функции события. Сигнатура функции события в текущем примере — Transfer(address, address, uint256). Хэшируя его с помощью keccak256, мы получаем результат ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.
(Онлайн-инструмент:
Когда мы запрашиваем поле from, как указано выше, но в то же время хотим ограничить тип журнала событий запроса только журналами событий типа Transfer, нам нужно фильтровать по типу события, индексируя сигнатуры событий.
У нас может быть до 4 тем, каждая тема имеет размер 32 байта (если тип параметра индекса больше 32 байт (т.е. строка и байты), фактические данные не сохраняются, а дайджест данных keccak256 хранится). Мы можем объявить 3 параметра индекса, потому что первый параметр принимается сигнатурой события. Но бывает ситуация, когда первая тема не является сигнатурой хеш-события. Это тот случай, когда объявляются анонимные события. Это открывает возможность использовать 4 параметра индекса вместо предыдущих 3, но теряет возможность индексировать имена событий. Еще одно преимущество анонимных событий заключается в том, что их развертывание менее затратно, поскольку они не навязывают дополнительную тему. Остальные темы — это значения индексов «откуда» и «куда» из события Transfer.
ДанныеДанные
Раздел данных содержит остальные (неиндексированные) параметры из журнала событий. В приведенном выше примере есть значение 0x0000000000000000000000000000000000000000000000000000000002b279b8, что составляет 45251000 в десятичном виде, что является вышеупомянутой суммой в 45,251 долларов США. Если таких параметров больше, они будут добавлены к элементу данных. В приведенном ниже примере показан случай более чем 1 неиндексированного параметра.
В текущем примере к событию Transfer добавляется дополнительное поле «tax». Допустим установлен налог 20%, тогда значение налога должно быть 45251000*20%=9050200, его шестнадцатеричное значение 0x8a1858, так как тип этого числа uint256, а тип данных 32 байта, нужно шестнадцатеричное значение заполняется 32 байтами, а результат элемента данных равен 0x000000000000000000000000000000000000000000000000000000000002b279b80000000000000000000000000000000000000 0000 00000000000000000000008a1858.
Адрес
Поле адреса — это адрес контракта, вызвавшего событие. Важное замечание об этом поле — оно будет проиндексировано, даже если оно не включено в раздел темы. Причина в том, что событие Transfer является частью стандарта ERC20, а это значит, что при необходимости отфильтровать логи событий передачи ERC20, события передачи будут получены из всех контрактов ERC20. И индексируя адрес контракта, поиск можно сузить до конкретного контракта/токена, например, USDT.
Коды операций
Наконец, есть код операции LOG. Они варьируются от LOG0, когда тем нет, до LOG4, когда тем 4. LOG3 — это то, что мы используем в нашем примере. Содержит следующее:
offset - смещение памяти, указывающее начальную позицию ввода поля данных
длина - длина данных для чтения из памяти
тема x(0 - 4) - значение темы x
(Источник:
смещение и длина определяют, где данные расположены в разделе данных в памяти.
Разобравшись со структурой журнала и тем, как индексируется тема, давайте разберемся, как осуществляется поиск элементов индекса.
Фильтры Блума Фильтры Блума
Секрет более быстрого индексирования искомых элементов — фильтр Блума.
В статье Llimllib есть хорошее определение и объяснение этой структуры данных.
«Фильтр Блума — это структура данных, которую можно использовать для определения того, находится ли элемент в коллекции. Он обладает характеристиками быстрой работы и небольшого объема памяти. Стоимость эффективной вставки и запроса заключается в том, что фильтр Блума — это данные, основанные на вероятности. структура: он может только сказать нам, что элемент определенно не находится в наборе или, возможно, в наборе. Базовая структура данных фильтра Блума — это битовый вектор».
Ниже приведен пример битового вектора. Белые ячейки представляют биты со значением 0, а зеленые ячейки представляют биты со значением 1.
Эти биты устанавливаются в 1 путем некоторого ввода и хеширования, полученное значение хеш-функции используется в качестве битового индекса, бит которого следует обновить. Приведенный выше битовый вектор является результатом применения двух разных хэшей к значению «ethereum» для получения 2-битного индекса. Хэш представляет собой шестнадцатеричное число, и чтобы получить индекс, вы берете число и конвертируете его в значение от 0 до 14. Есть много способов сделать это, например мод 14.
Обзор
С фильтром Блума для транзакций, то есть битовым вектором, его можно хешировать в Ethereum, чтобы определить, какие биты в битовом векторе нужно обновить.Входом является поле адреса и тема журнала событий. Давайте рассмотрим logsBloom в квитанции о транзакции, которая представляет собой фильтр Блума для конкретной транзакции. Транзакция может иметь несколько журналов, в которых содержится адрес/тема всех журналов.
Если вы вернетесь к заголовку блока, вы найдете еще один файл logsBloom. Это фильтр Блума для всех транзакций внутри блока. Который содержит все адреса/темы в каждом журнале для каждой транзакции.
Эти фильтры Блума выражаются в шестнадцатеричном, а не в двоичном формате. Они имеют длину 256 байт и представляют собой 2048-битный вектор. Если мы обратимся к приведенному выше примеру Llimllib, длина нашего битового вектора равна 15, а битовые индексы 2 и 13 инвертируются как 1. Давайте посмотрим, что мы получим, когда преобразуем это в шестнадцатеричный формат.
Хотя шестнадцатеричное представление не похоже на битовый вектор, в logsBloom оно похоже.
Запросы запросов
Ранее упоминался запрос: «Найти все журналы событий типа Transfer, адрес отправителя которых равен 0x5041ed759dd4afc3a72b8192c143f72f4724081a между блоками X и Y». Мы можем получить тему сигнатуры события, которая представляет тему типа Transfer и значение from (0x5041…), и определить, какие битовые индексы в фильтре Блума должны быть установлены в 1.
Если вы используете logsBloom в заголовке блока, вы можете проверить, не установлен ли какой-либо из этих битов в 1. Если нет, можно определить, что в блоке нет журналов, соответствующих условию. И если эти биты установлены, мы знаем, что соответствующий журнал, вероятно, находится в блоке. Но не совсем уверен, т.к. заголовок блока logsBloom состоит из нескольких адресов и топиков. В других журналах событий могут быть установлены биты соответствия. Вот почему фильтр Блума представляет собой вероятностную структуру данных. Чем больше битовый вектор, тем меньше вероятность возникновения коллизий битового индекса с другими журналами. Если у вас есть соответствующий фильтр Блума, вы можете использовать тот же метод для запроса logsBloom для отдельных квитанций. Когда совпадение получено, фактическая запись журнала может быть просмотрена для извлечения объекта.
Выполните вышеуказанные операции над блоками от X до Y, чтобы быстро найти и получить все журналы, соответствующие критериям. Вот как концептуально работает фильтр Блума.
Теперь давайте посмотрим на реализацию, используемую в Ethereum.
Реализация Geth — фильтры Блума
Теперь, когда мы знаем, как работает фильтр Блума, давайте узнаем, как фильтр Блума выполняет пошаговую проверку от адреса/темы до logsBloom в реальном блоке.
Прежде всего, из определения Ethereum Yellow Paper:
Источник:
«Мы определяем функцию фильтра Блума M, которая сокращает записи журнала до одного хэша размером 256 байт:
в это специализированный фильтр Блума, который устанавливает три бита в 2048 заданной произвольной последовательности байтов. Это делается путем взятия младших 11 битов каждой из первых трех пар байтов в хеше Keccak-256 последовательности байтов. "
Пример и ссылка на реализацию клиента Geth приведены ниже, чтобы упростить понимание приведенных выше определений.
Вот журнал транзакций, который мы просмотрели на Etherscan.
Первая тема — это подпись события 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, которая преобразует это значение в битовый индекс, который следует обновить.
Ниже представлена функция bloomValues из кодовой базы Geth.
Эта функция получает топик сигнатуры события, например: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef и другие данные, и возвращает битовый индекс, который необходимо обновить в фильтре Блума.
источник кода:
Функция bloomValues получает на вход топик (подпись события в примере) и хешбуф (пустой массив байт длиной 6).
Обратитесь к фрагменту желтой бумаги «Первые три пары байтов в хэше Keccak-256 последовательности байтов». Эти три пары байтов составляют 6 байтов, что соответствует длине hashbuf.
Пример данных: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.
Команда sha между строками 140–144 хэширует входные данные и загружает выходные данные в hashbuf.
Шестнадцатеричный результат вывода sha с использованием keccak256 (при использовании keccak 256 в качестве сигнатуры функции ввод имеет текстовый тип, но здесь шестнадцатеричный тип): ada389e1fc24a8587c776340efb91b36e675792ab631816100d55df0b5cf3cbc.
Текущее содержимое hasbuf [ad, a3, 89, e1, fc, 24] (шестнадцатеричный). Каждый шестнадцатеричный символ представляет 4 бита.
3 Рассчитать v1.
хешбуф [1] = 0xa3 = 10100011 для побитового И с 0x7. 0x7 = 00000111.
Байт состоит из битов 8. Если вы хотите получить битовый индекс, вам нужно убедиться, что полученное значение находится между 0 и 7 массива нулевого индекса. Используйте побитовое И для hashbuf [1] Ограничено значениями от 0 до 7. Рассчитано в примере: 10100011 & 00000111 = 00000011 = 3.
Это значение индекса бита используется с оператором сдвига битов, т. е. сдвигается на 3 бита влево, в результате чего получается 8-битовый индекс байта 00001000, для создания перевернутого бита.
v1 — это целый байт, а не фактический битовый индекс, потому что это значение будет позже подвергнуто побитовому ИЛИ в фильтре Блума. Операция ИЛИ гарантирует, что все соответствующие биты в фильтре Блума также перевернуты.
Теперь у нас есть байтовые значения, но нам все еще нужны байтовые индексы. Фильтр Блума имеет длину 256 байт (2048 бит), поэтому нам нужно знать, для какого байта выполнять побитовое ИЛИ. Значение i1 представляет этот индекс байта.
Поместите hashbuf в порядок байтов uint16 с обратным порядком байтов, что ограничивает первые 2 байта битового массива, что в примере равно 0xada3 = 1010110110100011.
Побитовое И это значение с 0x7ff = 0000011111111111. Есть 11 бит, где 0x7ff установлен в 1. Как упоминалось в желтой бумаге, «это делается путем взятия младших 11 битов каждой из первых трех пар». Это приведет к значению 0000010110100011, которое равно 1010110110100011 и 0000011111111111.
Затем сдвиньте значение вправо на 3 бита. Это преобразует 11-значное число в 8-значное число. Нам нужен байтовый индекс, а длина фильтра Блума составляет 256 байтов, поэтому значение байтового индекса должно быть в этом диапазоне. И 8-битное число может быть любым значением от 0 до 255. В нашем примере это значение 0000010110100011, сдвинутое вправо на 3 бита 10110100 = 180.
Рассчитаем наш байтовый индекс по BloomByteLength, зная, что это 256 минус вычисленные 180, минус 1. Вычтите 1, чтобы сохранить результат от 0 до 255. Это дает нам индекс байта для обновления, который в данном случае оказывается байтом 75, именно так мы вычислили i1.
Обновите битовый индекс 3 в 75-м байте фильтра Блума (0 — это индекс, поэтому 4-й бит), что можно сделать, выполнив побитовую операцию ИЛИ v1 над 75-м байтом в фильтре Блума.
Мы рассмотрели только первую пару байтов 0xada3, что было сделано снова для пар байтов 2 и 3. Каждый адрес/тема будет обновлять 3 бита в 2048-битном векторе. Как упоминалось в «Желтой книге», «фильтр Блума устанавливает три бита в 2048, учитывая произвольную последовательность байтов».
Статус пары байтов 2 обновляет индекс бита 1 в байте 195 (выполнить в соответствии с процедурами 3 и 4, результат показан на рисунке).
Пара байтов 3, бит обновления состояния, индекс 4 в байте 123.
Если обновляемый бит уже был перелистнут другой темой, он останется как есть. Будет перевернуто на 1, если нет.
С помощью описанного выше рабочего процесса можно определить, что тема сигнатуры события перевернет следующие биты в фильтре Блума:
Битовый индекс 3 в байте 75
индекс бита 1 в байте 195
битовый индекс 4 в байте 123
Глядя на logBlooms в квитанции транзакции, преобразованную в двоичный код, можно убедиться, что эти битовые индексы установлены.
Между тем, те читатели, которым интересно узнать больше о реализации поиска по журналам и фильтру Блума, могут обратиться к статье BloomBits Trie.
На этом наше подробное обсуждение серии статей по EVM подошло к концу, и в будущем мы предоставим вам больше качественных технических статей.
Посмотреть Оригинал
Содержание носит исключительно справочный характер и не является предложением или офертой. Консультации по инвестициям, налогообложению или юридическим вопросам не предоставляются. Более подробную информацию о рисках см. в разделе «Дисклеймер».
Погрузитесь в структуры данных EVM, квитанции о транзакциях и журналы событий
**Автор:**NOXX
Скомпилировать: сбросить
Навигация по цепочке данных — важный навык для любого, кто хочет понять пространство Web3. Понимание структур данных, составляющих блокчейн, помогает нам думать о творческих способах анализа этих данных. В то же время эти данные в цепочке составляют большую часть доступных данных. Этот пост будет посвящен ключевой структуре данных в EVM, квитанции транзакции и связанному с ней журналу событий.
Зачем вести журнал
Прежде чем мы начнем, давайте кратко поговорим о том, почему нам нужно использовать журналы событий как разработчику солидности:
Узлам EVM не нужно хранить журналы вечно, и они могут сэкономить место, удалив старые журналы. Контракты не имеют доступа к хранилищу журналов, поэтому узлам они не нужны для выполнения контрактов. С другой стороны, хранение контракта требуется для выполнения и поэтому не может быть удалено.
Эфириум блокирует Merkle Root
В части 4 мы углубились в структуру Ethereum, особенно в корневую часть состояния Merkle. State Root — это один из трех корней Merkle, включенных в заголовок блока. Два других — это Transaction Root и Receipt Root.
Для ввода данных для создания этой структуры мы будем ссылаться на блок 15001871 в Ethereum, который содержит 5 транзакций с соответствующими квитанциями и отправленными журналами событий.
заголовок блока
Мы начнем с 3 частей в заголовке блока: Корень транзакции, Корень квитанции и Цвет журналов (краткое введение в заголовок блока можно просмотреть в части 4).
Источник:
В клиенте Ethereum в разделе Transaction Root и Receipt Root Merkle Patricia Tries содержит все данные транзакций и данные квитанций в блоке. В этой статье мы сосредоточимся только на всех транзакциях и квитанциях, к которым может получить доступ узел.
Информация заголовка блока 15001871, найденная через узел Ethereum, выглядит следующим образом:
logsBloom в заголовке блока — это ключевая структура данных, о которой будет сказано далее в этой статье. Сначала давайте начнем с данных, расположенных в корне транзакции, Transaction Trie.
Транзакция дерева транзакций
Transaction Trie – это набор данных, который генерирует transactionRoot и записывает векторы запросов транзакций. Векторы запросов транзакций – это фрагменты информации, необходимые для выполнения транзакции. Поля данных, содержащиеся в транзакции, следующие:
Разобравшись с приведенными выше полями данных, давайте взглянем на первую транзакцию блока 15001871.
С помощью запроса Geth ethclient вы можете видеть, что и ChainId, и AccessList имеют «omitempty», что означает, что если поле пусто, оно будет опущено в ответе, чтобы уменьшить или сократить размер сериализованных данных.
источник кода:
Эта транзакция представляет собой перевод токенов USDT на адрес 0xec23e787ea25230f74a3da0f515825c1d820f47a. Адрес To — это адрес контракта ERC20 USDT 0xdac17f958d2ee523a2206206994597c13d831ec7. Через ВХОДНЫЕ ДАННЫЕ мы видим, что сигнатура функции 0xa9059cbb соответствует функции Transfer (Address, UINT256), а 42,251 USDT (точность 6) на 0x2b279b8 (45251000) передается на 0xEC23E787EA25230F на 0xEC23E787EA25230F. Адрес DA0F515825C1D820F47A.
Вы, возможно, заметили, что эта структура данных транзакции ничего не говорит нам о результате транзакции, так что транзакция прошла успешно? Сколько газа потребляет? Какие записи событий активируются? На этом этапе мы представим получение Trie.
Попробовать получение
Подобно тому, как в чеке о покупке записывается результат транзакции, объект в дереве чеков делает то же самое для транзакции Ethereum, но также записывает некоторые дополнительные сведения. Возвращаясь к вопросу о квитанциях о транзакциях выше, мы сосредоточимся на журналах, которые вызвали следующие события.
Снова запросите данные по цепочке 0x311b и получите квитанцию о транзакции.В это время будут получены следующие поля:
источник кода:
Теперь, когда мы знаем состав квитанции транзакции, давайте подробнее рассмотрим журналы logsBloom и log array в квитанции транзакции.
Журналы событий
В коде контракта USDT в сети Ethereum мы видим, что событие Transfer объявлено в строке 86 контракта, а два входных параметра имеют ключевое слово «indexed».
(источник кода:
Когда вход события «индексирован», это позволяет нам быстро находить журналы по этому входу. Например, при использовании вышеуказанного индекса from можно получить все журналы событий типа Transfer с адресом from 0x5041ed759dd4afc3a72b8192c143f72f4724081a между блоками X и Y. Мы также можем видеть, что при вызове функции передачи в строке 138 запускается журнал событий. Стоит отметить, что текущий контракт использует более раннюю версию Solidity, поэтому ключевое слово emit отсутствует.
Вернитесь к полученным ончейн-данным:
источник кода:
Давайте углубимся в поля адресов, тем и данных.
Тема Темы
Темы — это значение индекса. Из приведенного выше рисунка видно, что в данных запроса в цепочке есть 3 индексных параметра топиков, в то время как у события Transfer всего 2 индексных параметра (от и до). Это связано с тем, что первой темой всегда является хэш сигнатуры функции события. Сигнатура функции события в текущем примере — Transfer(address, address, uint256). Хэшируя его с помощью keccak256, мы получаем результат ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.
(Онлайн-инструмент:
Когда мы запрашиваем поле from, как указано выше, но в то же время хотим ограничить тип журнала событий запроса только журналами событий типа Transfer, нам нужно фильтровать по типу события, индексируя сигнатуры событий.
У нас может быть до 4 тем, каждая тема имеет размер 32 байта (если тип параметра индекса больше 32 байт (т.е. строка и байты), фактические данные не сохраняются, а дайджест данных keccak256 хранится). Мы можем объявить 3 параметра индекса, потому что первый параметр принимается сигнатурой события. Но бывает ситуация, когда первая тема не является сигнатурой хеш-события. Это тот случай, когда объявляются анонимные события. Это открывает возможность использовать 4 параметра индекса вместо предыдущих 3, но теряет возможность индексировать имена событий. Еще одно преимущество анонимных событий заключается в том, что их развертывание менее затратно, поскольку они не навязывают дополнительную тему. Остальные темы — это значения индексов «откуда» и «куда» из события Transfer.
ДанныеДанные
Раздел данных содержит остальные (неиндексированные) параметры из журнала событий. В приведенном выше примере есть значение 0x0000000000000000000000000000000000000000000000000000000002b279b8, что составляет 45251000 в десятичном виде, что является вышеупомянутой суммой в 45,251 долларов США. Если таких параметров больше, они будут добавлены к элементу данных. В приведенном ниже примере показан случай более чем 1 неиндексированного параметра.
В текущем примере к событию Transfer добавляется дополнительное поле «tax». Допустим установлен налог 20%, тогда значение налога должно быть 45251000*20%=9050200, его шестнадцатеричное значение 0x8a1858, так как тип этого числа uint256, а тип данных 32 байта, нужно шестнадцатеричное значение заполняется 32 байтами, а результат элемента данных равен 0x000000000000000000000000000000000000000000000000000000000002b279b80000000000000000000000000000000000000 0000 00000000000000000000008a1858.
Адрес
Поле адреса — это адрес контракта, вызвавшего событие. Важное замечание об этом поле — оно будет проиндексировано, даже если оно не включено в раздел темы. Причина в том, что событие Transfer является частью стандарта ERC20, а это значит, что при необходимости отфильтровать логи событий передачи ERC20, события передачи будут получены из всех контрактов ERC20. И индексируя адрес контракта, поиск можно сузить до конкретного контракта/токена, например, USDT.
Коды операций
Наконец, есть код операции LOG. Они варьируются от LOG0, когда тем нет, до LOG4, когда тем 4. LOG3 — это то, что мы используем в нашем примере. Содержит следующее:
(Источник:
смещение и длина определяют, где данные расположены в разделе данных в памяти.
Разобравшись со структурой журнала и тем, как индексируется тема, давайте разберемся, как осуществляется поиск элементов индекса.
Фильтры Блума Фильтры Блума
Секрет более быстрого индексирования искомых элементов — фильтр Блума.
В статье Llimllib есть хорошее определение и объяснение этой структуры данных.
«Фильтр Блума — это структура данных, которую можно использовать для определения того, находится ли элемент в коллекции. Он обладает характеристиками быстрой работы и небольшого объема памяти. Стоимость эффективной вставки и запроса заключается в том, что фильтр Блума — это данные, основанные на вероятности. структура: он может только сказать нам, что элемент определенно не находится в наборе или, возможно, в наборе. Базовая структура данных фильтра Блума — это битовый вектор».
Ниже приведен пример битового вектора. Белые ячейки представляют биты со значением 0, а зеленые ячейки представляют биты со значением 1.
Эти биты устанавливаются в 1 путем некоторого ввода и хеширования, полученное значение хеш-функции используется в качестве битового индекса, бит которого следует обновить. Приведенный выше битовый вектор является результатом применения двух разных хэшей к значению «ethereum» для получения 2-битного индекса. Хэш представляет собой шестнадцатеричное число, и чтобы получить индекс, вы берете число и конвертируете его в значение от 0 до 14. Есть много способов сделать это, например мод 14.
Обзор
С фильтром Блума для транзакций, то есть битовым вектором, его можно хешировать в Ethereum, чтобы определить, какие биты в битовом векторе нужно обновить.Входом является поле адреса и тема журнала событий. Давайте рассмотрим logsBloom в квитанции о транзакции, которая представляет собой фильтр Блума для конкретной транзакции. Транзакция может иметь несколько журналов, в которых содержится адрес/тема всех журналов.
Если вы вернетесь к заголовку блока, вы найдете еще один файл logsBloom. Это фильтр Блума для всех транзакций внутри блока. Который содержит все адреса/темы в каждом журнале для каждой транзакции.
Эти фильтры Блума выражаются в шестнадцатеричном, а не в двоичном формате. Они имеют длину 256 байт и представляют собой 2048-битный вектор. Если мы обратимся к приведенному выше примеру Llimllib, длина нашего битового вектора равна 15, а битовые индексы 2 и 13 инвертируются как 1. Давайте посмотрим, что мы получим, когда преобразуем это в шестнадцатеричный формат.
Хотя шестнадцатеричное представление не похоже на битовый вектор, в logsBloom оно похоже.
Запросы запросов
Ранее упоминался запрос: «Найти все журналы событий типа Transfer, адрес отправителя которых равен 0x5041ed759dd4afc3a72b8192c143f72f4724081a между блоками X и Y». Мы можем получить тему сигнатуры события, которая представляет тему типа Transfer и значение from (0x5041…), и определить, какие битовые индексы в фильтре Блума должны быть установлены в 1.
Если вы используете logsBloom в заголовке блока, вы можете проверить, не установлен ли какой-либо из этих битов в 1. Если нет, можно определить, что в блоке нет журналов, соответствующих условию. И если эти биты установлены, мы знаем, что соответствующий журнал, вероятно, находится в блоке. Но не совсем уверен, т.к. заголовок блока logsBloom состоит из нескольких адресов и топиков. В других журналах событий могут быть установлены биты соответствия. Вот почему фильтр Блума представляет собой вероятностную структуру данных. Чем больше битовый вектор, тем меньше вероятность возникновения коллизий битового индекса с другими журналами. Если у вас есть соответствующий фильтр Блума, вы можете использовать тот же метод для запроса logsBloom для отдельных квитанций. Когда совпадение получено, фактическая запись журнала может быть просмотрена для извлечения объекта.
Выполните вышеуказанные операции над блоками от X до Y, чтобы быстро найти и получить все журналы, соответствующие критериям. Вот как концептуально работает фильтр Блума.
Теперь давайте посмотрим на реализацию, используемую в Ethereum.
Реализация Geth — фильтры Блума
Теперь, когда мы знаем, как работает фильтр Блума, давайте узнаем, как фильтр Блума выполняет пошаговую проверку от адреса/темы до logsBloom в реальном блоке.
Прежде всего, из определения Ethereum Yellow Paper:
Источник:
«Мы определяем функцию фильтра Блума M, которая сокращает записи журнала до одного хэша размером 256 байт:
в
это специализированный фильтр Блума, который устанавливает три бита в 2048 заданной произвольной последовательности байтов. Это делается путем взятия младших 11 битов каждой из первых трех пар байтов в хеше Keccak-256 последовательности байтов. "
Пример и ссылка на реализацию клиента Geth приведены ниже, чтобы упростить понимание приведенных выше определений.
Вот журнал транзакций, который мы просмотрели на Etherscan.
Первая тема — это подпись события 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, которая преобразует это значение в битовый индекс, который следует обновить.
Ниже представлена функция bloomValues из кодовой базы Geth.
Эта функция получает топик сигнатуры события, например: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef и другие данные, и возвращает битовый индекс, который необходимо обновить в фильтре Блума.
источник кода:
Обратитесь к фрагменту желтой бумаги «Первые три пары байтов в хэше Keccak-256 последовательности байтов». Эти три пары байтов составляют 6 байтов, что соответствует длине hashbuf.
Пример данных: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.
3 Рассчитать v1.
хешбуф [1] = 0xa3 = 10100011 для побитового И с 0x7. 0x7 = 00000111.
Байт состоит из битов 8. Если вы хотите получить битовый индекс, вам нужно убедиться, что полученное значение находится между 0 и 7 массива нулевого индекса. Используйте побитовое И для hashbuf [1] Ограничено значениями от 0 до 7. Рассчитано в примере: 10100011 & 00000111 = 00000011 = 3.
Это значение индекса бита используется с оператором сдвига битов, т. е. сдвигается на 3 бита влево, в результате чего получается 8-битовый индекс байта 00001000, для создания перевернутого бита.
v1 — это целый байт, а не фактический битовый индекс, потому что это значение будет позже подвергнуто побитовому ИЛИ в фильтре Блума. Операция ИЛИ гарантирует, что все соответствующие биты в фильтре Блума также перевернуты.
Поместите hashbuf в порядок байтов uint16 с обратным порядком байтов, что ограничивает первые 2 байта битового массива, что в примере равно 0xada3 = 1010110110100011.
Побитовое И это значение с 0x7ff = 0000011111111111. Есть 11 бит, где 0x7ff установлен в 1. Как упоминалось в желтой бумаге, «это делается путем взятия младших 11 битов каждой из первых трех пар». Это приведет к значению 0000010110100011, которое равно 1010110110100011 и 0000011111111111.
Затем сдвиньте значение вправо на 3 бита. Это преобразует 11-значное число в 8-значное число. Нам нужен байтовый индекс, а длина фильтра Блума составляет 256 байтов, поэтому значение байтового индекса должно быть в этом диапазоне. И 8-битное число может быть любым значением от 0 до 255. В нашем примере это значение 0000010110100011, сдвинутое вправо на 3 бита 10110100 = 180.
Рассчитаем наш байтовый индекс по BloomByteLength, зная, что это 256 минус вычисленные 180, минус 1. Вычтите 1, чтобы сохранить результат от 0 до 255. Это дает нам индекс байта для обновления, который в данном случае оказывается байтом 75, именно так мы вычислили i1.
Мы рассмотрели только первую пару байтов 0xada3, что было сделано снова для пар байтов 2 и 3. Каждый адрес/тема будет обновлять 3 бита в 2048-битном векторе. Как упоминалось в «Желтой книге», «фильтр Блума устанавливает три бита в 2048, учитывая произвольную последовательность байтов».
Статус пары байтов 2 обновляет индекс бита 1 в байте 195 (выполнить в соответствии с процедурами 3 и 4, результат показан на рисунке).
Пара байтов 3, бит обновления состояния, индекс 4 в байте 123.
Если обновляемый бит уже был перелистнут другой темой, он останется как есть. Будет перевернуто на 1, если нет.
С помощью описанного выше рабочего процесса можно определить, что тема сигнатуры события перевернет следующие биты в фильтре Блума:
Глядя на logBlooms в квитанции транзакции, преобразованную в двоичный код, можно убедиться, что эти битовые индексы установлены.
Между тем, те читатели, которым интересно узнать больше о реализации поиска по журналам и фильтру Блума, могут обратиться к статье BloomBits Trie.
На этом наше подробное обсуждение серии статей по EVM подошло к концу, и в будущем мы предоставим вам больше качественных технических статей.