Зануртеся в структури даних EVM, квитанції про транзакції та журнали подій

Розуміння структур даних, які складають блокчейн, допомагає нам думати про творчі способи аналізу цих даних.

**Автор:**NOXX

Компіляція: Flush

Навігація в мережевих даних є важливою навичкою для кожного, хто хоче зрозуміти простір Web3. Розуміння структур даних, з яких складається блокчейн, допомагає нам думати про творчі способи аналізу цих даних. У той же час ці дані в ланцюжку складають значну частину доступних даних. У цьому дописі буде розглянуто ключову структуру даних у EVM, квитанцію про транзакцію та пов’язаний з нею журнал подій.

Навіщо реєструвати

Перш ніж ми почнемо, давайте коротко поговоримо про те, чому нам потрібно використовувати журнали подій як розробник solidity:

  • Журнал подій — це дешевший варіант зберігання даних, до якого не потрібен доступ за контрактом, а також може реконструювати збережений стан шляхом тестування певних змінних у смарт-контракті, індексуючи змінні.
  • Журнал подій — це спосіб запустити програму Web3, яка прослуховує певний журнал подій.

Вузлам EVM не потрібно зберігати журнали вічно, і вони можуть заощадити місце, видаливши старі журнали. Контракти не мають доступу до сховища журналів, тому вузли не потребують їх для виконання контрактів. З іншого боку, зберігання договору потрібне для виконання, тому його не можна видалити.

Ethereum Block Merkle Root

У частині 4 ми заглибились у фреймворк Ethereum, особливо кореневу частину стану Merkle. Корінь стану є одним із трьох коренів Merkle, включених до заголовка блоку. Інші два - це корінь транзакції та корінь отримання.

Для створення цього фреймворку ми звернемося до блоку 15001871 на Ethereum, який містить 5 транзакцій із відповідними квитанціями та надісланими журналами подій.

заголовок блоку

Ми почнемо з 3 частин у заголовку блоку: корінь транзакції, корінь квитанції та журнали Bloom (короткий вступ до заголовка блоку можна переглянути в частині 4).

Джерело:

У клієнті Ethereum у розділі Transaction Root і Receipt Root Merkle Patricia Tries містить усі дані транзакцій і дані квитанцій у блоці. У цій статті мова піде лише про всі транзакції та квитанції, до яких вузол може отримати доступ.

Інформація заголовка блоку 15001871, знайдена через вузол Ethereum, така:

LogsBloom у заголовку блоку є ключовою структурою даних, яка буде згадана далі в цій статті. Спочатку давайте почнемо з даних, розташованих у корені транзакції, Transaction Trie.

Дерево транзакцій Transaction Trie

Transaction Trie — це набір даних, який генерує транзакціїRoot і записує вектори запитів транзакцій. Вектори запитів транзакцій — це частини інформації, необхідні для виконання транзакції. Поля даних, які містяться в транзакції, такі:

  • Type - тип транзакції (традиційна транзакція LegacyTxType, представлення AccessListTxType EIP-2930, представлення DynamicFeeTxType EIP-1559)
  • ChainId - ідентифікатор ланцюга EIP155 транзакції
  • Дані - вхідні дані транзакції
  • AccessList - список доступу для транзакцій
  • Газ - газовий ліміт транзакції
  • GasPrice - ціна газу транзакції
  • GasTipCap — заохочувальна надбавка для майнерів, чия одиниця транзакції газу перевищує базову плату за першу упаковку, maxPriorityFeePerGas у Geth визначається EIP1559
  • GasFeeCap – верхня межа комісії за газ за одиницю транзакції, maxFeePerGas у Geth (GasFeeCap ≥ baseFee + GasTipCap)
  • Значення - кількість ефіріуму, що торгується
  • Nonce - nonce ініціатора торгового рахунку
  • Кому – адреса одержувача транзакції. Для транзакцій створення контракту To повертає нульове значення
  • RawSignaturues - Значення підпису V, R, S даних транзакції

Розібравшись із наведеними вище полями даних, давайте подивимося на першу транзакцію блоку 15001871

За допомогою запиту ethclient Geth ви можете побачити, що ChainId і AccessList мають «omitempty», що означає, що якщо поле порожнє, воно буде опущено у відповіді, щоб зменшити або скоротити розмір серіалізованих даних.

джерело коду:

Ця транзакція означає передачу токенів USDT на адресу 0xec23e787ea25230f74a3da0f515825c1d820f47a. Адреса «Кому» — це адреса контракту ERC20 USDT 0xdac17f958d2ee523a2206206994597c13d831ec7. За допомогою ВХІДНИХ ДАНИХ ми можемо побачити, що сигнатура функції 0xa9059cbb відповідає функції Transfer (Address, UINT256), а 42,251 USDT (точність 6) до 0x2b279b8 (45251000) передається в 0xEC23E787EA25230F до 0xEC23E787EA25 230F. 74A3DA0F515825C1D820F47A адреса.

Можливо, ви помітили, що ця структура даних транзакції нічого не повідомляє нам про результат транзакції, тож чи була транзакція успішною? Скільки газу споживає? Які записи подій запускаються? На цьому етапі ми представимо Receipt Trie.

Квитанція Trie

Подібно до того, як квитанція про покупку записує результат транзакції, об’єкт у Receipt Trie робить те саме для транзакції Ethereum, але також записує деякі додаткові деталі. Повертаючись до запитання про квитанції про транзакції вище, ми зосередимося на журналах, які викликали наступні події.

Знову запитайте дані ланцюжка 0x311b і отримайте квитанцію про транзакцію. У цей час будуть отримані такі поля:

джерело коду:

  • Type - тип транзакції (LegacyTxType, AccessListTxType, DynamicFeeTxType)
  • PostState(root) - StateRoot, кореневий вузол дерева стану, створеного після виконання транзакції, відповідне значення на малюнку дорівнює 0x, ймовірно, через EIP98
  • CumulativeGasUsed – сукупний загальний газ, спожитий цією транзакцією та всіма попередніми транзакціями в тому самому блоці
  • Bloom(logsBloom) – фільтр Bloom для журналів подій, який використовується для ефективного пошуку та доступу до журналів подій контрактів у блокчейні, дозволяючи вузлам швидко отримувати інформацію про те, чи сталася певна подія в блоці, без повного аналізу блоку. Усі квитанції про транзакції в блоці.
  • Журнали – масив об’єктів журналу, що містить записи журналу, згенеровані подіями контракту, ініційованими під час виконання транзакції
  • TxHash - хеш транзакції, пов'язаний з квитанцією
  • ContractAddress – якщо транзакція передбачає створення контракту, адреса, де було розгорнуто контракт. Якщо транзакція є не створенням контракту, а передачею або взаємодією з розгорнутим смарт-контрактом, тоді поле ContractAddress буде порожнім
  • GasUsed - газ, спожитий цією транзакцією
  • BlockNumber - номер блоку, де відбулася ця транзакція
  • TransactionIndex – індекс транзакції в блоці, індекс визначає, яка транзакція виконується першою. Ця транзакція знаходиться у верхній частині блоку, тому індекс 0

Тепер, коли ми знаємо склад квитанції про транзакцію, давайте ближче розглянемо logsBloom і журнали масиву журналів у квитанції про транзакцію.

Журнали подій

За допомогою коду контракту USDT в основній мережі Ethereum ми бачимо, що подія Transfer оголошена в рядку 86 контракту, а два вхідні параметри мають ключове слово «indexed».

(джерело коду:

Коли вхід подій «індексований», це дозволяє нам швидко знаходити журнали через цей вхід. Наприклад, при використанні індексу «від» вище можна отримати всі журнали подій типу «Передача» з адресою «від» 0x5041ed759dd4afc3a72b8192c143f72f4724081a між блоками X і Y. Ми також бачимо, що коли функція передачі викликається в рядку 138, запускається журнал подій. Варто зазначити, що поточний контракт використовує попередню версію solidity, тому ключове слово emit відсутнє.

Поверніться до отриманих даних у мережі:

джерело коду:

Давайте заглибимося в адреси, теми та поля даних.

Тематичні теми

Теми – це значення індексу. З наведеного вище малюнка ми бачимо, що в даних запиту в ланцюжку є 3 параметри індексу тем, тоді як подія Transfer має лише 2 параметри індексу (від і до). Це пов’язано з тим, що першою темою завжди є хеш сигнатури функції події. Сигнатурою функції події в поточному прикладі є Transfer(address, address, uint256). Хешуючи його за допомогою keccak256, ми отримуємо результат ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

(Інструмент онлайн:

Коли ми запитуємо поле from, як зазначено вище, але водночас хочемо обмежити тип журналу подій запиту лише журналами подій типу Transfer, нам потрібно фільтрувати за типом події, індексуючи підписи подій.

Ми можемо мати до 4 тем, кожна тема має розмір 32 байти (якщо тип параметра індексу перевищує 32 байти (тобто рядок і байти), фактичні дані не зберігаються, а keccak256 дайджест даних зберігається). Ми можемо оголосити 3 параметри індексу, оскільки перший параметр приймається сигнатурою події. Але є ситуація, коли перша тема не є підписом хеш-події. Це стосується декларування анонімних подій. Це відкриває можливість використовувати 4 параметри індексу замість попередніх 3, але втрачає можливість індексувати назви подій. Ще одна перевага анонімних подій полягає в тому, що вони дешевші в розгортанні, оскільки вони не запроваджують додаткову тему. Інші теми - це значення індексів "від" і "до" з події "Передача".

DataData

Розділ даних містить решту (неіндексованих) параметрів із журналу подій. У наведеному вище прикладі є значення 0x00000000000000000000000000000000000000000000000000002b279b8, яке дорівнює 45251000 у десятковій системі, що є вищезазначеною сумою 45,251 доларів США. Якщо таких параметрів більше, вони будуть додані до елемента даних. У наведеному нижче прикладі показано випадок більше ніж 1 неіндексованого параметра.

У поточному прикладі до події Transfer додається додаткове поле "tax". Припустимо, встановлений податок становить 20%, тоді значення податку має бути 45251000 * 20% = 9050200, його шістнадцяткове значення дорівнює 0x8a1858, оскільки тип цього числа uint256, а тип даних 32 байти, вам потрібно шістнадцяткове значення заповнюється 32 байтами, а результатом елемента даних є 0x00000000000000000000000000000000000000000000000000000002b279b800000000000000000000000000000000 0000000 00000000000000000000008a1858.

Адреса

Поле адреси – це адреса контракту, який випустив подію. Важливим зауваженням щодо цього поля є те, що воно буде проіндексовано, навіть якщо воно не включене в розділ теми. Причина полягає в тому, що подія Transfer є частиною стандарту ERC20, а це означає, що коли необхідно фільтрувати журнали подій трансферу ERC20, події трансферу будуть отримані з усіх контрактів ERC20. А шляхом індексації адреси контракту пошук можна звузити до конкретного контракту/токена, наприклад USDT у прикладі.

Opcodes Opcodes

Нарешті є код операції LOG. Вони коливаються від LOG0, коли теми немає, до LOG4, коли є 4 теми. LOG3 – це те, що ми використовуємо в нашому прикладі. Містить наступне:

  • offset - зміщення пам'яті, що вказує на початкову позицію введення поля даних
  • length - довжина даних для читання з пам'яті
  • тема х(0 - 4) - значення теми х

(Джерело:

зміщення та довжина визначають місце розташування даних у розділі даних у пам’яті.

Зрозумівши структуру журналу та спосіб індексування теми, давайте зрозуміємо, як здійснюється пошук елементів індексу.

Фільтри Bloom Filters Bloom

Секрет швидшого індексування елементів, що шукаються, полягає в фільтрі Блума.

У статті Llimllib є гарне визначення та пояснення цієї структури даних.

«Фільтр Блума — це структура даних, за допомогою якої можна визначити, чи є елемент у колекції. Він має характеристики швидкої роботи та малого обсягу пам’яті. Вартість ефективного вставлення та запиту полягає в тому, що фільтр Блума — це дані на основі ймовірностей. Структура: це може лише сказати нам, що елемент точно не в наборі або, можливо, в наборі. Основна структура даних фільтра Блума — це бітовий вектор».

Нижче наведено приклад бітового вектора. Білі клітинки представляють біти зі значенням 0, а зелені клітинки представляють біти зі значенням 1.

Ці біти встановлюються в 1 шляхом введення деяких даних і хешування, отримане хеш-значення використовується як індекс біта, біт якого слід оновити. Наведений вище бітовий вектор є результатом застосування 2 різних хешів до значення "ethereum", щоб отримати 2-бітовий індекс. Хеш представляє шістнадцяткове число, і щоб отримати індекс, ви берете число та перетворюєте його на значення від 0 до 14. Є багато способів зробити це, наприклад мод 14.

Огляд

За допомогою фільтра Блума для транзакцій, тобто бітового вектора, його можна хешувати в Ethereum, щоб визначити, які біти бітового вектора оновлювати. Вхідними даними є поле адреси та тема журналу подій. Давайте переглянемо logsBloom у квитанції транзакції, яка є фільтром Bloom для певної транзакції. Транзакція може мати кілька журналів, які містять адресу/тему всіх журналів.

Якщо ви знову подивитеся на заголовок блоку, ви знайдете інший logsBloom. Це фільтр Блума для всіх транзакцій у блоці. Містить усі адреси/теми в кожному журналі для кожної транзакції.

Ці фільтри Блума виражаються у шістнадцятковій, а не двійковій системі. Вони мають довжину 256 байт і представляють 2048-бітний вектор. Якщо ми звернемося до прикладу Llimllib вище, наша довжина бітового вектора дорівнює 15, а бітові індекси 2 і 13 перевернуті як 1. Давайте подивимося, що ми отримаємо, якщо перетворити його на hex.

Хоча шістнадцяткове представлення не виглядає як бітовий вектор, воно виглядає в logsBloom.

Запити

Запит згадувався раніше: «Знайти всі журнали подій типу «Передача», адреса «від» яких 0x5041ed759dd4afc3a72b8192c143f72f4724081a між блоками X і Y». Ми можемо отримати тему підпису події, яка представляє тему типу Transfer і значення from (0x5041…), і визначити, які бітові індекси у фільтрі Блума мають бути встановлені на 1.

Якщо ви використовуєте logsBloom у заголовку блоку, ви можете перевірити, чи один із цих бітів не встановлений на 1. Якщо ні, можна визначити, що в блоці немає журналів, які відповідають умові. І якщо ці біти встановлені, ми знаємо, що відповідний журнал, ймовірно, є в блоці. Але не зовсім впевнений, оскільки заголовок блоку logsBloom складається з кількох адрес і тем. Інші журнали подій можуть мати встановлені біти відповідності. Ось чому фільтр Блума є імовірнісною структурою даних. Чим більший бітовий вектор, тим менша ймовірність виникнення колізій індексів бітів з іншими журналами. Якщо у вас є відповідний фільтр Bloom, ви можете використовувати той самий метод для запиту logsBloom для окремих квитанцій. Коли знайдено збіг, фактичний запис журналу можна переглянути, щоб отримати об’єкт.

Виконайте наведені вище операції з блоками від X до Y, щоб швидко знайти та отримати всі журнали, які відповідають критеріям. Ось як концептуально працює фільтр Блума.

Тепер давайте розглянемо реалізацію, яка використовується в Ethereum.

Реалізація Geth - фільтри Bloom

Тепер, коли ми знаємо, як працює фільтр Bloom, давайте дізнаємося, як фільтр Bloom виконує крок за кроком скринінг від адреси/теми до logsBloom у фактичному блоці.

Перш за все, з визначення жовтої книги Ethereum:

Джерело:

«Ми визначаємо функцію фільтра Блума M, яка зводить записи журналу до одного 256-байтового хешу:

в — це спеціалізований фільтр Блума, який встановлює три біти в 2048 із заданою довільною послідовністю байтів. Це робиться шляхом взяття молодших 11 бітів кожної з перших трьох пар байтів у хеші Keccak-256 послідовності байтів. "

Нижче наведено приклад і посилання на реалізацію клієнта Geth, щоб спростити розуміння наведених вище визначень.

Ось журнал транзакцій, який ми переглянули на Etherscan.

Перша тема — це підпис події 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef і перетворює це значення на бітовий індекс, який слід оновити.

Нижче наведено функцію bloomValues з кодової бази Geth.

Ця функція отримує тему підпису події, наприклад: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef та інші дані, і повертає бітовий індекс, який потрібно оновити у фільтрі Блума.

джерело коду:

  1. Функція bloomValues отримує на вхід топік (сигнатуру події в прикладі) і хешбуф (порожній байтовий масив довжиною 6).
  1. Зверніться до фрагмента Yellow Paper, «Перші три пари байтів у хеші Keccak-256 послідовності байтів». Ці три пари байтів складають 6 байтів, що є довжиною hashbuf.

  2. Зразок даних: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

  1. Команда sha між рядками 140 - 144 хешує вхідні дані та завантажує вихідні дані в hashbuf.
  1. Шістнадцятковий результат виведення sha за допомогою keccak256 такий (при використанні keccak 256 як сигнатури функції введення є текстовим типом, але тут є шістнадцятковий тип): ada389e1fc24a8587c776340efb91b36e675792ab631816100d55df0b5cf3cb в.

  1. Поточний вміст hasbuf [ad, a3, 89, e1, fc, 24] (шістнадцятковий). Кожен шістнадцятковий символ представляє 4 біти.

3 Обчислити v1.

  1. хешбуф [1] = 0xa3 = 10100011 для побітового І з 0x7. 0x7 = 00000111.

  2. Байт складається з 8 біт.Якщо ви хочете отримати бітовий індекс, вам потрібно переконатися, що отримане значення знаходиться в межах від 0 до 7 масиву нульового індексу. Використовуйте порозрядне І для hashbuf [1] Обмежується значеннями від 0 до 7. Обчислено у прикладі: 10100011 & 00000111 = 00000011 = 3.

  3. Це значення індексу біта використовується з оператором зсуву біта, тобто зсувається на 3 біти вліво, що призводить до 8-бітового індексу байта 00001000, для створення перевернутого біта.

  4. v1 — це весь байт, а не фактичний бітовий індекс, тому що це значення буде об’єднане порозрядним АБО у фільтрі Блума пізніше. Операція АБО гарантує, що всі відповідні біти у фільтрі Блума також будуть перевернуті.

  1. Тепер у нас є значення байтів, але нам все ще потрібні індекси байтів. Фільтр Блума має довжину 256 байт (2048 біт), тому нам потрібно знати, на якому байті запускати порозрядне АБО. Значення i1 представляє індекс цього байта.
  1. Розмістіть хеш-буф через порядок байтів uint16 у порядку байтів, що обмежує перші 2 байти бітового масиву, який у прикладі становить 0xada3 = 1010110110100011.

  2. Побітове І це значення з 0x7ff = 0000011111111111. Є 11 бітів, де 0x7ff встановлено на 1. Як зазначено в жовтому документі, «він робить це, беручи молодші 11 бітів кожної з перших трьох пар». Це призведе до значення 0000010110100011, яке дорівнює 1010110110100011 і 0000011111111111.

  3. Потім зрушити значення праворуч на 3 біти. Це перетворює 11-значне число на 8-значне. Нам потрібен байтовий індекс, а довжина байтів фільтра Блума становить 256, тому значення байтового індексу має бути в цьому діапазоні. А 8-бітне число може мати будь-яке значення від 0 до 255. У нашому прикладі це значення дорівнює 0000010110100011 зі зміщенням праворуч на 3 біти 10110100 = 180.

  4. Обчисліть наш байтовий індекс за BloomByteLength, знаючи, що він дорівнює 256 мінус розраховане 180, мінус 1. Відніміть 1, щоб зберегти результат між 0 і 255. Це дає нам байтовий індекс для оновлення, який у цьому випадку виявляється байтом 75, як ми обчислили i1.

  1. Оновіть бітовий індекс 3 у 75-му байті фільтра Блума (0 є індексом, отже, 4-й біт), що можна зробити, виконавши порозрядну операцію АБО v1 на 75-му байті у фільтрі Блума.
  1. Ми розглянули лише першу пару байтів 0xada3, що було зроблено знову для пар байтів 2 і 3. Кожна адреса/тема оновлюватиме 3 біти у 2048-бітному векторі. Як згадувалося в жовтій книзі, «фільтр Блума встановлює три біти в 2048, враховуючи довільну послідовність байтів».

  2. Статус пари байтів 2 оновлює індекс біта 1 у байті 195 (виконайте відповідно до процедур 3 і 4, результат показано на малюнку).

  3. Статус пари байтів 3 оновлює бітовий індекс 4 у байті 123.

  4. Якщо біт, який потрібно оновити, вже був перевернутий іншою темою, він залишиться як є. Якщо ні, буде змінено на 1.

За допомогою описаного вище процесу операції можна визначити, що тема підпису події переверне такі біти у фільтрі Блума:

  • Індекс біта 3 у байті 75
  • бітовий індекс 1 у байті 195
  • бітовий індекс 4 у байті 123

Перегляд logBlooms у квитанції про транзакцію, перетворений у двійковий формат, підтверджує, що ці бітові індекси встановлено.

Тим часом для тих читачів, яким цікаво дізнатися більше про реалізацію пошуку журналів і фільтр Bloom, ви можете звернутися до статті BloomBits Trie.

На цьому наше поглиблене обговорення серії статей EVM підійшло до кінця, і в майбутньому ми надамо вам більше якісних технічних статей.

Переглянути оригінал
Контент має виключно довідковий характер і не є запрошенням до участі або пропозицією. Інвестиційні, податкові чи юридичні консультації не надаються. Перегляньте Відмову від відповідальності , щоб дізнатися більше про ризики.
  • Нагородити
  • Прокоментувати
  • Поділіться
Прокоментувати
0/400
Немає коментарів
  • Закріпити