This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
EVM データ構造、トランザクション レシート、イベント ログについて詳しく説明します。
**作者:**NOXX
コンパイル: フラッシュ
オンチェーン データをナビゲートすることは、Web3 スペースを理解したい人にとって不可欠なスキルです。ブロックチェーンを構成するデータ構造を理解することは、このデータを解析する創造的な方法を考えるのに役立ちます。同時に、このオンチェーンデータは利用可能なデータの大部分を占めます。この投稿では、EVM の主要なデータ構造、トランザクション受信、および関連するイベント ログについて詳しく説明します。
ログを記録する理由
始める前に、Solidity 開発者としてイベント ログを使用する必要がある理由について簡単に説明しましょう。
EVM ノードはログを永久に保持する必要はなく、古いログを削除することでスペースを節約できます。コントラクトはログ ストレージにアクセスできないため、ノードはコントラクトを実行するためにログ ストレージを必要としません。一方、コントラクト ストレージは実行に必要なため、削除できません。
イーサリアム ブロック マークル ルート
パート 4 では、イーサリアム フレームワーク、特にステート マークル ルート部分を詳しく掘り下げました。ステート ルートは、ブロック ヘッダーに含まれる 3 つのマークル ルートのうちの 1 つです。他の 2 つは、トランザクション ルートと受信ルートです。
このフレームワークを構築するための入力として、イーサリアムのブロック 15001871 を参照します。このブロックには、5 つのトランザクションと、関連する領収書と送信されたイベント ログが含まれています。
ブロックヘッダー
ブロック ヘッダーの 3 つの部分、トランザクション ルート、受信ルート、ログ ブルームから始めます (ブロック ヘッダーの簡単な紹介はパート 4 で確認できます)。
ソース:
Ethereum クライアントのトランザクション ルートと領収書ルートでは、マークル パトリシア トライズがブロック内のすべてのトランザクション データと領収書データを含みます。この記事では、ノードがアクセスできるすべてのトランザクションと領収書のみに焦点を当てます。
イーサリアムノードを通じて発見されたブロック15001871のブロックヘッダー情報は次のとおりです。
ブロック ヘッダーの logsBloom は重要なデータ構造であり、これについてはこの記事で後ほど説明します。まず、トランザクション ルート、つまりトランザクション トライの下にあるデータから始めましょう。
トランザクション ツリー トランザクション トライ
トランザクショントライは、transactionRootを生成し、トランザクションリクエストベクトルを記録するデータセットです。トランザクションリクエストベクトルとは、トランザクションを実行するために必要な情報です。トランザクションに含まれるデータフィールドは次のとおりです。
上記のデータフィールドを理解した後、ブロック 15001871 の最初のトランザクションを見てみましょう。
Geth の ethclient クエリを通じて、ChainId と AccessList の両方に「omitempty」があることがわかります。これは、フィールドが空の場合、シリアル化されたデータのサイズを削減または短縮するために応答でそのフィールドが省略されることを意味します。
コードソース:
このトランザクションは、0xec23e787ea25230f74a3da0f515825c1d820f47a アドレスへの USDT トークンの転送を表します。 To アドレスは、ERC20 USDT 契約アドレス 0xdac17f958d2ee523a2206206994597c13d831ec7 です。 INPUT DATA から、関数シグネチャ 0xa9059cbb が関数 Transfer (Address, UINT256) に対応し、0x2b279b8 (45251000) への 42.251 USDT (精度 6) が 0xEC23E787EA25230F ~ 0xEC23E787EA25230F に転送されることがわかります。 3DA0F515825C1D820F47A アドレス。
このトランザクション データ構造ではトランザクションの結果について何も示されていないことに気づいたかもしれませんが、トランザクションは成功したのでしょうか?どれくらいのガスを消費しますか?どのイベント レコードがトリガーされますか?ここでレシートトライを紹介します。
受信トライ
買い物のレシートがトランザクションの結果を記録するのと同じように、レシート トライのオブジェクトはイーサリアム トランザクションに対して同じことを行いますが、いくつかの追加の詳細も記録します。上記のトランザクション受信に関する質問に戻り、次のイベントをトリガーしたログに焦点を当てます。
0x311b のオンチェーン データを再度クエリしてトランザクション レシートを取得すると、次のフィールドが取得されます。
コードソース:
トランザクション レシートの構成がわかったので、トランザクション レシート内の logsBloom とログ配列ログを詳しく見てみましょう。
イベント ログ
イーサリアム メインネットの USDT コントラクト コードを見ると、Transfer イベントがコントラクトの 86 行目で宣言されており、2 つの入力パラメータにキーワード「indexed」が含まれていることがわかります。
(コードソース:
イベント入力に「インデックスが付けられる」と、その入力を通じてログをすばやく見つけることができます。たとえば、上記のインデックス「from」を使用すると、ブロック X と Y の間の「from」アドレス 0x5041ed759dd4afc3a72b8192c143f72f4724081a を持つ転送タイプのすべてのイベント ログを取得できます。また、138 行目で転送関数が呼び出されると、イベント ログが発生することもわかります。現在のコントラクトでは以前のバージョンの Solidity が使用されているため、emit キーワードが欠落していることに注意してください。
取得したオンチェーン データに戻ります。
コードソース:
アドレス、トピック、データ フィールドをもう少し詳しく見てみましょう。
テーマトピック
トピックスはインデックス値です。上の図から、チェーン上のクエリ データにはトピックの 3 つのインデックス パラメーターがあるのに対し、Transfer イベントには 2 つのインデックス パラメーター (from と to) しかないことがわかります。これは、最初のトピックが常にイベントの関数署名ハッシュであるためです。現在の例のイベント関数のシグネチャは Transfer(address, address, uint256) です。 keccak256 でハッシュすると、結果 ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef が得られます。
(オンラインツール:
前述のように from フィールドをクエリすると同時に、クエリのイベント ログ タイプを転送タイプのイベント ログのみに制限したい場合は、イベント シグネチャのインデックスを作成してイベント タイプでフィルタリングする必要があります。
最大 4 つのトピックを持つことができ、各トピックのサイズは 32 バイトです (インデックス パラメーターの型が 32 バイト (つまり、文字列とバイト) を超える場合、実際のデータは保存されませんが、データの keccak256 ダイジェストが保存されます)が格納されます)。最初のパラメータはイベント シグネチャによって取得されるため、3 つのインデックス パラメータを宣言できます。ただし、最初のトピックがハッシュ イベントの署名ではない場合があります。これは、匿名イベントを宣言する場合に当てはまります。これにより、以前の 3 つの代わりに 4 つのインデックス パラメーターを使用できる可能性が広がりますが、イベント名のインデックスを作成する機能は失われます。匿名イベントのもう 1 つの利点は、追加のトピックを強制しないため、展開コストが低くなるということです。他のトピックは、Transfer イベントのインデックス「from」と「to」の値です。
データデータ
データ セクションには、イベント ログの残りの (インデックス付けされていない) パラメーターが含まれます。上の例では、値 0x0000000000000000000000000000000000000000000000000000002b279b8 があります。これは 10 進数で 45251000、前述の $45.251 の金額です。このようなパラメータがさらにある場合は、データ項目に追加されます。以下の例は、インデックスのないパラメータが複数ある場合を示しています。
現在の例では、追加の「税金」フィールドを Transfer イベントに追加します。設定された税金が 20% であると仮定すると、税金の値は 45251000 * 20% = 9050200 になります。この数値の型は uint256 で、データの型は 32 バイトであるため、その 16 進値は 0x8a1858 になります。次のことを行う必要があります。 16 進値は 32 バイトで埋められ、データ項目の結果は 0x000000000000000000000000000000000000000000000000000000002b279b80000000000000000000000000000000000 0000 00000000000000000000008a1858。
## 住所
アドレス フィールドは、イベントを発行したコントラクトのアドレスです。このフィールドに関する重要な点は、トピック セクションに含まれていない場合でもインデックスが作成されることです。その理由は、転送イベントが ERC20 標準の一部であるためです。つまり、ERC20 転送イベントのログをフィルタリングする必要がある場合、すべての ERC20 契約から転送イベントが取得されることになります。また、コントラクト アドレスにインデックスを付けることで、検索を特定のコントラクト/トークン (例では USDT など) に絞り込むことができます。
オペコード オペコード
最後に LOG オペコードがあります。トピックがない場合の LOG0 からトピックが 4 つある場合の LOG4 までの範囲になります。この例では LOG3 を使用します。以下が含まれます:
(ソース:
オフセットと長さは、メモリ内のデータ セクション内のデータの配置場所を定義します。
ログの構造とトピックのインデックス付け方法を理解した後、インデックス項目がどのように検索されるかを理解しましょう。
ブルーム フィルター ブルーム フィルター
検索されるアイテムのインデックスをより高速に作成する秘密は、ブルーム フィルターです。
Llimllib の記事には、このデータ構造の適切な定義と説明が記載されています。
「ブルーム フィルターは、要素がコレクション内にあるかどうかを判断するために使用できるデータ構造です。ブルーム フィルターは、高速な操作と小さいメモリ使用量という特徴があります。効率的な挿入とクエリのコストは、ブルーム フィルターが確率ベースのデータであることです。 「構造: 要素がセット内にない、またはセット内にある可能性があるということしかわかりません。ブルーム フィルターの基礎となるデータ構造はビット ベクトルです。」
以下はビットベクトルの例です。白いセルは値 0 のビットを表し、緑色のセルは値 1 のビットを表します。
これらのビットは、何らかの入力を取得してハッシュすることによって 1 に設定され、結果として得られるハッシュ値は、どのビットを更新する必要があるかのビット インデックスとして使用されます。上記のビット ベクトルは、2 つの異なるハッシュを値「ethereum」に適用して 2 ビットのインデックスを取得した結果です。ハッシュは 16 進数を表し、インデックスを取得するには、その数値を取得して 0 ~ 14 の値に変換します。 mod 14 など、これを行う方法はたくさんあります。
## レビュー
トランザクションのブルーム フィルター、つまりビット ベクトルを使用すると、イーサリアムでハッシュしてビット ベクトル内のどのビットを更新するかを決定できます入力はアドレス フィールドとイベント ログのトピックです。トランザクション受領書の logsBloom を確認してみましょう。これはトランザクション固有のブルーム フィルターです。トランザクションには複数のログを含めることができ、すべてのログのアドレス/トピックが含まれます。
ブロックヘッダーまで遡って見ると、別の logsBloom が見つかります。これは、ブロック内のすべてのトランザクションに対するブルーム フィルターです。これには、各トランザクションの各ログ内のすべてのアドレス/トピックが含まれます。
これらのブルーム フィルターは 2 進数ではなく 16 進数で表現されます。これらは 256 バイトの長さで、2048 ビットのベクトルを表します。上記の Llimllib の例を参照すると、ビット ベクトルの長さは 15 で、ビット インデックス 2 と 13 は 1 として反転されます。これを 16 進数に変換すると何が得られるかを見てみましょう。
16 進表現はビット ベクトルのようには見えませんが、logsBloom ではビット ベクトルのように見えます。
クエリクエリ
前述のクエリは、「ブロック X と Y の間の「送信元」アドレスが 0x5041ed759dd4afc3a72b8192c143f72f4724081a である転送タイプのすべてのイベント ログを検索する」というものです。転送タイプのトピックを表すイベント署名トピックを (0x5041…) 値から取得し、ブルーム フィルターのどのビット インデックスを 1 に設定するかを決定できます。
ブロック ヘッダーで logsBloom を使用する場合は、これらのビットのいずれかが 1 に設定されていないかどうかを確認できます。そうでない場合は、ブロック内に条件に一致するログが存在しないと判断できる。これらのビットが設定されていることが判明した場合は、一致するログがブロック内に存在する可能性があることがわかります。ただし、ブロック ヘッダー logsBloom は複数のアドレスとトピックで構成されているため、完全にはわかりません。他のイベント ログには一致ビットが設定されている場合があります。これが、ブルーム フィルターが確率的データ構造である理由です。ビット ベクトルが大きいほど、他のログとビット インデックスの衝突が発生する可能性が低くなります。一致するブルーム フィルターを取得したら、同じ方法を使用して、個々のレシートについて logBloom をクエリできます。一致が得られると、実際のログ エントリを表示してオブジェクトを取得できます。
ブロック X から Y に対して上記の操作を実行して、条件を満たすすべてのログをすばやく検索して取得します。これは、ブルーム フィルターが概念的にどのように機能するかです。
次に、イーサリアムで使用される実装を見てみましょう。
Geth 実装 - ブルーム フィルター
ブルーム フィルターがどのように機能するかがわかったので、実際のブロックでブルーム フィルターがアドレス/トピックから logsBloom までのスクリーニングを段階的に完了する方法を学びましょう。
まず、イーサリアムイエローペーパーの定義から:
ソース:
「ログエントリを単一の 256 バイトのハッシュに減らすブルーム フィルター関数 M を定義します。
の
は、任意のバイト シーケンスを指定して 2048 に 3 ビットを設定する特殊なブルーム フィルターです。これは、バイト シーケンスの Keccak-256 ハッシュ内の最初の 3 つのバイト ペアのそれぞれの下位 11 ビットを取得することによって行われます。 」
上記の定義を理解しやすくするために、Geth クライアント実装の例と参照を以下に示します。
こちらはEtherscanで調べたトランザクションログです。
最初のトピックはイベント シグネチャ 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef で、この値を更新する必要があるビット インデックスに変換します。
以下は、Geth コードベースの BloomValues 関数です。
この関数は、0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef などのイベント署名トピックとその他のデータを受け取り、ブルーム フィルターで更新する必要があるビット インデックスを返します。
コードソース:
1.bloomValues 関数は、トピック (この例ではイベント シグネチャ) と hashbuf (長さ 6 の空のバイト配列) を入力として受け取ります。
イエロー ペーパーのスニペット「バイト シーケンスの Keccak-256 ハッシュの最初の 3 つのバイト ペア」を参照してください。これら 3 つのバイトのペアは 6 バイトであり、これが hashbuf の長さです。
サンプルデータ: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef。
3 v1 を計算します。
1)ハッシュバフ [1] = 0xa3 = 10100011 (0x7 とのビット単位の AND)。 0x7 = 00000111。
バイトは 8 ビットで構成されており、ビット インデックスを取得したい場合は、取得した値がゼロ インデックス配列の 0 ~ 7 の間にあることを確認する必要があります。ビットごとの AND を使用して hashbuf を実行する [1] 0 から 7 までの値に制限されます。この例では、10100011 & 00000111 = 00000011 = 3 と計算されます。
このビット インデックス値はビット シフト演算子で使用されます。つまり、3 ビット左にシフトされ、8 ビットのバイト インデックス 00001000 が得られ、反転ビットが作成されます。
v1 は実際のビット インデックスではなくバイト全体です。これは、この値が後でブルーム フィルターでビット単位の OR 演算されるためです。 OR 演算により、ブルーム フィルター内のすべての対応するビットも反転されます。
ハッシュバッファをビッグエンディアンの uint16 バイト順に配置します。これにより、ビット配列の最初の 2 バイトが制限されます (例では 0xada3 = 1010110110100011)。
この値と 0x7ff = 0000011111111111 をビット単位で AND 演算します。 0x7ff が 1 に設定されているビットは 11 個あります。黄色の論文で述べられているように、「最初の 3 つのペアのそれぞれの下位 11 ビットを取得することによってこれが行われます」。これにより、値 0000010110100011 (1010110110100011 & 0000011111111111) が得られます。
次に、値を 3 ビット右にシフトします。これにより、11 桁の数値が 8 桁の数値に変換されます。バイト インデックスが必要ですが、ブルーム フィルターのバイト長は 256 であるため、バイト インデックス値はこの範囲内である必要があります。また、8 ビット数値は 0 ~ 255 の任意の値になります。この例では、この値は 0000010110100011 を 3 ビット右シフトした 10110100 = 180 です。
BloomByteLength によってバイト インデックスを計算します。これは、256 から計算された 180 を引いて 1 を引いたものであることがわかります。結果を 0 ~ 255 の範囲に保つには、1 を減算します。これにより、更新するバイト インデックスが得られます。この場合、バイト 75 であることがわかり、これが i1 の計算方法です。
最初のバイト ペア 0xada3 のみを説明しましたが、バイト ペア 2 と 3 についても同様に行いました。各アドレス/トピックは、2048 ビット ベクトル内の 3 ビットを更新します。イエロー ペーパーで述べられているように、「ブルーム フィルターは、任意のバイト シーケンスが与えられると、2048 年に 3 ビットを設定します」。
バイト ペア 2 のステータスは、バイト 195 のビット インデックス 1 を更新します (手順 3 および 4 に従って実行、結果を図に示します)。
バイト 123 のバイト ペア 3 ステータス更新ビット インデックス 4。
更新対象のビットがすでに別のトピックによって反転されている場合は、そのまま残ります。そうでない場合は 1 に切り替わります。
上記の操作プロセスを通じて、イベント シグネチャ トピックがブルーム フィルター内の次のビットを反転することがわかります。
バイナリに変換されたトランザクション レシート内の logBloom を確認すると、これらのビット インデックスが設定されていることを確認できます。
一方、ログ検索とブルーム フィルターの実装について詳しく知りたい読者は、BloomBits Trie の記事を参照してください。
この時点で、EVM シリーズの記事に関する詳細な説明は終了しました。今後はさらに質の高い技術記事をお届けする予定です。