Plongez dans les structures de données EVM, les reçus de transaction et les journaux d'événements

Comprendre les structures de données qui composent une blockchain nous aide à réfléchir à des moyens créatifs d'analyser ces données.

**Écrit par :**NOXX

Compiler : Flush

La navigation dans les données en chaîne est une compétence essentielle pour quiconque souhaite comprendre l'espace Web3. Comprendre les structures de données qui composent une blockchain nous aide à réfléchir à des moyens créatifs d'analyser ces données. Dans le même temps, ces données en chaîne constituent une grande partie des données disponibles. Cet article se plongera dans une structure de données clé dans l'EVM, le reçu de transaction et son journal d'événements associé.

Pourquoi se connecter

Avant de commencer, parlons brièvement de la raison pour laquelle nous devons utiliser les journaux d'événements en tant que développeur Solidity :

  • Le journal des événements est une option moins chère pour le stockage des données qui n'a pas besoin d'être accessible par le contrat, et peut également reconstruire l'état stocké en testant des variables spécifiques dans le contrat intelligent, en indexant les variables.
  • La journalisation des événements est un moyen de déclencher une application Web3 écoutant un journal d'événements spécifique.

Les nœuds EVM n'ont pas besoin de conserver les journaux indéfiniment et peuvent économiser de l'espace en supprimant les anciens journaux. Les contrats n'ont pas accès au stockage des journaux, les nœuds n'en ont donc pas besoin pour exécuter les contrats. Le stockage de contrat, en revanche, est nécessaire à l'exécution et ne peut donc pas être supprimé.

Bloc Ethereum Racine Merkle

Dans la partie 4, nous avons approfondi le cadre Ethereum, en particulier la partie racine de l'état Merkle. La racine d'état est l'une des trois racines Merkle incluses dans l'en-tête de bloc. Les deux autres sont Transaction Root et Receipt Root.

Pour contribuer à la construction de ce cadre, nous nous référerons au bloc 15001871 sur Ethereum, qui contient 5 transactions avec leurs reçus associés et les journaux d'événements envoyés.

en-tête de bloc

Nous commencerons par 3 parties dans l'en-tête de bloc, Transaction Root, Receipt Root et Logs Bloom (une brève introduction à l'en-tête de bloc peut être examinée dans la partie 4).

Source:

Dans le client Ethereum sous Transaction Root et Receipt Root, Merkle Patricia Tries contient toutes les données de transaction et les données de reçu dans le bloc. Cet article se concentrera uniquement sur toutes les transactions et tous les reçus auxquels un nœud peut accéder.

Les informations d'en-tête de bloc du bloc 15001871 trouvées via le nœud Ethereum sont les suivantes :

Le logsBloom dans l'en-tête du bloc est une structure de données clé, qui sera mentionnée plus loin dans cet article. Commençons d'abord par les données situées sous la Transaction Root, la Transaction Trie.

Test de transaction de l'arborescence des transactions

Transaction Trie est un ensemble de données qui génère transactionsRoot et enregistre les vecteurs de demande de transaction. Les vecteurs de demande de transaction sont des éléments d'information requis pour exécuter une transaction. Les champs de données contenus dans une transaction sont les suivants :

  • Type - type de transaction (transaction traditionnelle LegacyTxType, introduction AccessListTxType EIP-2930, introduction DynamicFeeTxType EIP-1559)
  • ChainId - ID de chaîne EIP155 de la transaction
  • Données - données d'entrée de la transaction
  • AccessList - liste d'accès pour les transactions
  • Gaz - la limite de gaz de la transaction
  • GasPrice - le prix du gaz de la transaction
  • GasTipCap - la prime incitative pour les mineurs dont le gaz unitaire de transaction dépasse les frais de base pour emballer en premier, maxPriorityFeePerGas à Geth est défini par EIP1559
  • GasFeeCap - la limite supérieure des frais de gaz par unité de transaction, maxFeePerGas à Geth (GasFeeCap ≥ baseFee + GasTipCap)
  • Valeur - la quantité d'Ethereum échangée
  • Nonce - le nonce de l'initiateur du compte de trading
  • À - L'adresse du destinataire de la transaction. Pour les transactions de création de contrat, To renvoie une valeur nulle
  • RawSignaturues - Valeurs de signature V, R, S des données de transaction

Après avoir compris les champs de données ci-dessus, examinons la première transaction du bloc 15001871

Grâce à la requête ethclient de Geth, vous pouvez voir que ChainId et AccessList ont "omitempty", ce qui signifie que si le champ est vide, il sera omis dans la réponse pour réduire ou raccourcir la taille des données sérialisées.

code source :

Cette transaction représente le transfert de jetons USDT vers l'adresse 0xec23e787ea25230f74a3da0f515825c1d820f47a. L'adresse À est l'adresse du contrat ERC20 USDT 0xdac17f958d2ee523a2206206994597c13d831ec7. Grâce à INPUT DATA, nous pouvons voir que la signature de fonction 0xa9059cbb correspond à la fonction Transfer (Address, UINT256), et 42.251 USDT (précision de 6) à 0x2b279b8 (45251000) est transféré à 0xEC23E787EA25230F à 0xEC23E787EA25230F. Adresse 0F515825C1D820F47A.

Vous avez peut-être remarqué que cette structure de données de transaction ne nous dit rien sur le résultat de la transaction, alors la transaction a-t-elle réussi ? Combien de gaz consomme-t-il ? Quels enregistrements d'événements sont déclenchés ? À ce stade, nous allons introduire le test de réception.

Essai de reçu

Tout comme un reçu d'achat enregistre le résultat d'une transaction, un objet dans le Receipt Trie fait de même pour une transaction Ethereum mais enregistre également quelques détails supplémentaires. Pour en revenir à la question sur les reçus de transaction ci-dessus, nous nous concentrerons sur les journaux qui ont déclenché les événements suivants.

Interrogez à nouveau les données en chaîne de 0x311b et obtenez son reçu de transaction. À ce stade, les champs suivants seront obtenus :

code source :

  • Type - type de transaction (LegacyTxType, AccessListTxType, DynamicFeeTxType)
  • PostState(root) - StateRoot, le nœud racine de l'arbre d'état généré après l'exécution de la transaction, la valeur correspondante trouvée dans la figure est 0x, probablement à cause d'EIP98
  • CumulativeGasUsed - Gaz total cumulé consommé par cette transaction et toutes les transactions précédentes dans le même bloc
  • Bloom (logsBloom) - Filtre Bloom pour les journaux d'événements, utilisé pour rechercher et accéder efficacement aux journaux d'événements de contrat sur la blockchain, permettant aux nœuds de récupérer rapidement si un certain événement s'est produit dans un bloc sans analyser complètement le bloc Tous les reçus de transaction dans le bloc
  • Journaux - un tableau d'objets de journal contenant des entrées de journal générées par des événements de contrat déclenchés lors de l'exécution de la transaction
  • TxHash - le hachage de transaction associé au reçu
  • ContractAddress - Si la transaction consiste à créer un contrat, l'adresse où le contrat a été déployé. Si la transaction n'est pas une création de contrat, mais un transfert ou une interaction avec un contrat intelligent déployé, alors le champ ContractAddress sera vide
  • GasUsed - le gaz consommé par cette transaction
  • BlockNumber - le numéro de bloc du bloc où cette transaction s'est produite
  • TransactionIndex - L'index de transaction dans le bloc, l'index détermine quelle transaction est exécutée en premier. Cette transaction est en haut du bloc, donc index 0

Maintenant que nous connaissons la composition du reçu de transaction, examinons de plus près les journaux logsBloom et le tableau de journaux dans le reçu de transaction.

Journaux d'événements

Grâce au code de contrat USDT sur le réseau principal Ethereum, nous pouvons voir que l'événement Transfer est déclaré à la ligne 86 du contrat, et les deux paramètres d'entrée ont le mot-clé "indexed".

(code source :

Lorsqu'une entrée d'événement est "indexée", cela nous permet de trouver rapidement des journaux via cette entrée. Par exemple, lors de l'utilisation de l'index "de" ci-dessus, il est possible d'obtenir tous les journaux d'événements de type Transfert avec l'adresse "de" 0x5041ed759dd4afc3a72b8192c143f72f4724081a entre les blocs X et Y. Nous pouvons également voir que lorsque la fonction de transfert est appelée à la ligne 138, le journal des événements est déclenché. Il convient de noter que le contrat actuel utilise une version antérieure de solidity, de sorte que le mot-clé d'émission est manquant.

Revenez aux données en chaîne obtenues :

code source :

Approfondissons un peu les champs d'adresse, de sujets et de données.

Sujets thématiques

Les sujets sont une valeur d'index. D'après la figure ci-dessus, nous pouvons voir qu'il y a 3 paramètres d'index de sujets dans les données de requête sur la chaîne, tandis que l'événement Transfer n'a que 2 paramètres d'index (de et à). En effet, le premier sujet est toujours le hachage de la signature de la fonction de l'événement. La signature de la fonction d'événement dans l'exemple actuel est Transfer(address, address, uint256). En le hachant avec keccak256, on obtient le résultat ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

(Outil en ligne :

Lorsque nous interrogeons le champ de comme mentionné ci-dessus, mais que nous voulons en même temps limiter le type de journal d'événements de requête aux seuls journaux d'événements de type transfert, nous devons filtrer par type d'événement en indexant les signatures d'événements.

Nous pouvons avoir jusqu'à 4 sujets, chaque sujet a une taille de 32 octets (si le type du paramètre d'index est supérieur à 32 octets (c'est-à-dire chaîne et octets), les données réelles ne sont pas stockées, mais un résumé keccak256 des données est stocké). On peut déclarer 3 paramètres d'index car le premier paramètre est pris par la signature de l'événement. Mais il existe une situation où le premier sujet n'est pas une signature d'événement de hachage. C'est le cas lors de la déclaration d'événements anonymes. Cela ouvre la possibilité d'utiliser 4 paramètres d'index au lieu des 3 précédents, mais perd la possibilité d'indexer les noms d'événements. Un autre avantage des événements anonymes est qu'ils sont moins coûteux à déployer car ils n'imposent pas de sujet supplémentaire. Les autres sujets sont les valeurs des index "from" et "to" de l'événement Transfer.

DonnéesDonnées

La section des données contient les paramètres restants (non indexés) du journal des événements. Dans l'exemple ci-dessus, il y a une valeur 0x0000000000000000000000000000000000000000000000000002b279b8, qui est 45251000 en décimal, soit le montant susmentionné de 45,251 $. S'il existe plusieurs paramètres de ce type, ils seront ajoutés à l'élément de données. L'exemple ci-dessous montrera le cas de plus d'un paramètre non indexé.

L'exemple actuel ajoute un champ "taxe" supplémentaire à l'événement de transfert. Supposons que la taxe définie soit de 20 %, alors la valeur de la taxe doit être de 45251000 * 20 % = 9050200, sa valeur hexadécimale est 0x8a1858, puisque le type de ce nombre est uint256 et que le type de données est de 32 octets, vous devez la valeur hexadécimale est remplie avec 32 octets et le résultat de l'élément de données est 0x000000000000000000000000000000000000000000000000000002b279b800000000000000000000000000000000 0000 0000000000000000000008a1858.

Adresse

Le champ d'adresse est l'adresse du contrat qui a émis l'événement, une remarque importante à propos de ce champ est qu'il sera indexé même s'il n'est pas inclus dans la section du sujet. La raison en est que l'événement de transfert fait partie de la norme ERC20, ce qui signifie que lorsqu'il est nécessaire de filtrer les journaux des événements de transfert ERC20, les événements de transfert seront obtenus à partir de tous les contrats ERC20. Et en indexant l'adresse du contrat, la recherche peut être réduite à un contrat/token spécifique, tel que USDT dans l'exemple.

Opcodes Opcodes

Enfin, il y a l'opcode LOG. Ils vont de LOG0 lorsqu'il n'y a pas de sujets à LOG4 lorsqu'il y a 4 sujets. LOG3 est ce que nous utilisons dans notre exemple. Contient les éléments suivants :

  • décalage - décalage de la mémoire, indiquant la position de départ de l'entrée du champ de données
  • longueur - longueur des données à lire depuis la mémoire
  • sujet x(0 - 4) - la valeur du sujet x

(Source:

offset et length définissent l'emplacement des données dans la section de données en mémoire.

Après avoir compris la structure du journal et comment un sujet est indexé, comprenons comment les éléments d'index sont recherchés.

Filtres BloomFiltres Bloom

Le secret pour indexer les éléments recherchés plus rapidement est le filtre Bloom.

L'article de Llimllib a une bonne définition et explication de cette structure de données.

"Le filtre Bloom est une structure de données qui peut être utilisée pour déterminer si un élément se trouve dans une collection. Il présente les caractéristiques d'un fonctionnement rapide et d'une faible empreinte mémoire. Le coût d'une insertion et d'une requête efficaces est que Bloom Filter est une donnée basée sur la probabilité. structure : Il peut seulement nous dire qu'un élément n'est certainement pas dans l'ensemble ou peut-être dans l'ensemble. La structure de données sous-jacente d'un filtre Bloom est un vecteur de bits.

Voici un exemple de vecteur de bits. Les cellules blanches représentent des bits avec une valeur de 0 et les cellules vertes représentent des bits avec une valeur de 1.

Ces bits sont mis à 1 en prenant une entrée et un hachage, la valeur de hachage résultante est utilisée comme un index de bit sur lequel le bit doit être mis à jour. Le vecteur de bits ci-dessus est le résultat de l'application de 2 hachages différents à la valeur "ethereum" pour obtenir un index de 2 bits. Le hachage représente un nombre hexadécimal, et pour obtenir l'index, vous prenez le nombre et le convertissez en une valeur comprise entre 0 et 14. Il existe de nombreuses façons de le faire, comme le mod 14.

Examen

Avec un filtre Bloom pour les transactions, c'est-à-dire un vecteur de bits, il peut être haché dans Ethereum pour déterminer quels bits du vecteur de bits mettre à jour.L'entrée est le champ d'adresse et le sujet du journal des événements. Passons en revue les logsBloom dans le reçu de transaction, qui est un filtre Bloom spécifique à la transaction. Une transaction peut avoir plusieurs journaux, qui contiennent l'adresse/le sujet de tous les journaux.

Si vous regardez en arrière jusqu'à l'en-tête du bloc, vous trouverez un autre logsBloom. Il s'agit d'un filtre Bloom pour toutes les transactions du bloc. Qui contient toutes les adresses/sujets de chaque journal pour chaque transaction.

Ces filtres Bloom sont exprimés en hexadécimal et non en binaire. Ils ont une longueur de 256 octets et représentent un vecteur de 2048 bits. Si nous nous référons à l'exemple Llimllib ci-dessus, la longueur de notre vecteur de bits est de 15 et les indices de bits 2 et 13 sont inversés en 1. Voyons ce que nous obtenons lorsque nous convertissons cela en hexadécimal.

Bien que la représentation hexadécimale ne ressemble pas à un vecteur de bits, c'est le cas dans logsBloom.

Requêtes de requête

Une requête a été mentionnée précédemment, "Rechercher tous les journaux d'événements de type Transfert dont l'adresse" de "est 0x5041ed759dd4afc3a72b8192c143f72f4724081a entre les blocs X et Y". Nous pouvons obtenir le sujet de signature d'événement, qui représente un sujet de type Transfert et de valeur (0x5041…), et déterminer quels indices de bit dans le filtre Bloom doivent être définis sur 1.

Si vous utilisez logsBloom dans l'en-tête du bloc, vous pouvez vérifier si l'un de ces bits n'est pas défini sur 1. Si ce n'est pas le cas, il peut être déterminé qu'aucun journal ne correspond à la condition dans le bloc. Et si ces bits sont définis, nous savons qu'un journal correspondant se trouve probablement dans le bloc. Mais pas tout à fait sûr, car l'en-tête de bloc logsBloom se compose de plusieurs adresses et sujets. D'autres journaux d'événements peuvent avoir le bit de correspondance défini. C'est pourquoi un filtre Bloom est une structure de données probabiliste. Plus le vecteur de bits est grand, moins il est probable que des collisions d'index de bits se produisent avec d'autres journaux. Une fois que vous avez un filtre Bloom correspondant, vous pouvez utiliser la même méthode pour interroger logsBloom pour les reçus individuels. Lorsqu'une correspondance est obtenue, l'entrée de journal réelle peut être visualisée pour récupérer l'objet.

Exécutez les opérations ci-dessus sur les blocs X à Y pour trouver et récupérer rapidement tous les journaux qui répondent aux critères. C'est ainsi que le filtre Bloom fonctionne conceptuellement.

Regardons maintenant l'implémentation utilisée dans Ethereum.

Implémentation Geth - Filtres Bloom

Maintenant que nous savons comment fonctionne le filtre Bloom, apprenons comment le filtre Bloom complète le filtrage étape par étape de l'adresse / du sujet aux logsBloom dans un bloc réel.

Tout d'abord, à partir de la définition du livre jaune Ethereum :

Source:

"Nous définissons une fonction de filtre Bloom M qui réduit les entrées de journal en un seul hachage de 256 octets :

dans est un filtre Bloom spécialisé qui définit trois bits en 2048 en fonction d'une séquence arbitraire d'octets. Cela se fait en prenant les 11 bits inférieurs de chacune des trois premières paires d'octets dans le hachage Keccak-256 de la séquence d'octets. "

Un exemple et une référence à une implémentation de client Geth sont fournis ci-dessous pour simplifier la compréhension des définitions ci-dessus.

Voici le journal des transactions que nous avons consulté sur Etherscan.

Le premier sujet est la signature d'événement 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef et convertit cette valeur en index de bits qui doit être mis à jour.

Vous trouverez ci-dessous la fonction bloomValues de la base de code Geth.

Cette fonction reçoit la rubrique de signature d'événement, telle que : 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef et d'autres données, et renvoie l'index de bits qui doit être mis à jour dans le filtre Bloom.

code source :

  1. La fonction bloomValues reçoit en entrée un sujet (signature d'événement dans l'exemple) et un hashbuf (un tableau d'octets vide de longueur 6).
  1. Reportez-vous à l'extrait de Yellow Paper, "Les trois premières paires d'octets dans un hachage Keccak-256 d'une séquence d'octets". Ces trois paires d'octets font 6 octets, ce qui correspond à la longueur de hashbuf.

  2. Exemple de données : 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef.

  1. La commande sha entre les lignes 140 à 144 hache les données d'entrée et charge la sortie dans hashbuf.
  1. Le résultat hexadécimal de la sortie sha utilisant keccak256 est (lorsque vous utilisez keccak 256 comme signature de fonction, l'entrée est de type texte, mais ici est de type hexadécimal): ada389e1fc24a8587c776340efb91b36e675792ab631816100d55df0b5cf3cbc.

  1. Le contenu actuel de hasbuf [ad, a3, 89, e1, fc, 24] (hexadécimal). Chaque caractère hexadécimal représente 4 bits.

3 Calculez v1.

1)hashbuf [1] = 0xa3 = 10100011 pour ET au niveau du bit avec 0x7. 0x7 = 00000111.

  1. Un octet est composé de 8 bits. Si vous souhaitez obtenir un index de bit, vous devez vous assurer que la valeur obtenue est comprise entre 0 et 7 du tableau d'index zéro. Utilisez ET au niveau du bit pour hashbuf [1] Limité aux valeurs comprises entre 0 et 7. Calculé dans l'exemple, 10100011 & 00000111 = 00000011 = 3.

  2. Cette valeur d'index de bit est utilisée avec un opérateur de décalage de bit, c'est-à-dire décalé de 3 bits vers la gauche, résultant en un index d'octet de 8 bits 00001000, pour créer un bit inversé.

  3. v1 est l'octet entier plutôt que l'index de bit réel, car cette valeur sera associée ultérieurement à un OR au niveau du bit sur le filtre Bloom. L'opération OU garantira que tous les bits correspondants dans le filtre Bloom sont également inversés.

  1. Nous avons maintenant des valeurs d'octets, mais nous avons toujours besoin d'index d'octets. Le filtre Bloom a une longueur de 256 octets (2048 bits), nous devons donc savoir sur quel octet exécuter le OU au niveau du bit. La valeur i1 représente cet indice d'octet.
  1. Mettez le hashbuf dans l'ordre des octets big-endian uint16, ce qui limite les 2 premiers octets du tableau de bits, qui est 0xada3 = 1010110110100011 dans l'exemple.

  2. Au niveau du bit ET cette valeur avec 0x7ff = 0000011111111111. Il y a 11 bits où 0x7ff est mis à 1. Comme mentionné dans le livre jaune, "il le fait en prenant les 11 bits inférieurs de chacune des trois premières paires". Cela se traduira par la valeur 0000010110100011 qui est 1010110110100011 & 0000011111111111.

  3. Décalez ensuite la valeur vers la droite de 3 bits. Ceci convertit un nombre à 11 chiffres en un nombre à 8 chiffres. Nous voulons un index d'octets, et la longueur en octets du filtre Bloom est de 256, donc la valeur de l'index d'octets doit être dans cette plage. Et un nombre de 8 bits peut être n'importe quelle valeur entre 0 et 255. Dans notre exemple, cette valeur est 0000010110100011 décalé vers la droite de 3 bits 10110100 = 180.

  4. Calculez notre indice d'octets par BloomByteLength, sachant qu'il est de 256 moins les 180 calculés, moins 1. Soustrayez 1 pour conserver le résultat entre 0 et 255. Cela nous donne l'index d'octet à mettre à jour, qui dans ce cas s'avère être l'octet 75, c'est ainsi que nous avons calculé i1.

  1. Mettez à jour l'index de bit 3 dans le 75e octet du filtre Bloom (0 est l'index, donc le 4e bit), ce qui peut être fait en effectuant une opération OU au niveau du bit de v1 sur le 75e octet du filtre Bloom.
  1. Nous n'avons couvert que la première paire d'octets 0xada3, qui a été refaite pour les paires d'octets 2 et 3. Chaque adresse/sujet mettra à jour 3 bits dans un vecteur de 2048 bits. Comme mentionné dans le livre jaune, "le filtre Bloom définit trois bits en 2048 étant donné une séquence arbitraire d'octets".

  2. L'état de la paire d'octets 2 met à jour l'index de bit 1 dans l'octet 195 (exécutez conformément aux procédures 3 et 4, le résultat est indiqué sur la figure).

  3. Paire d'octets 3 index de bit de mise à jour d'état 4 dans l'octet 123.

  4. Si le bit à mettre à jour a déjà été retourné par un autre sujet, il restera tel quel. Retournera à 1 sinon.

Grâce au processus d'opération ci-dessus, il peut être déterminé que le sujet de signature d'événement inversera les bits suivants dans le filtre Bloom :

  • Indice de bit 3 dans l'octet 75
  • indice de bit 1 dans l'octet 195
  • indice de bit 4 dans l'octet 123

L'examen des logBlooms dans le reçu de transaction, converti en binaire, vérifie que ces indices de bits sont définis.

En attendant, pour les lecteurs qui souhaitent en savoir plus sur la mise en œuvre de la recherche de journaux et du filtre Bloom, vous pouvez vous référer à l'article BloomBits Trie.

À ce stade, notre discussion approfondie de la série d'articles EVM est terminée et nous vous proposerons d'autres articles techniques de haute qualité à l'avenir.

Voir l'original
Le contenu est fourni à titre de référence uniquement, il ne s'agit pas d'une sollicitation ou d'une offre. Aucun conseil en investissement, fiscalité ou juridique n'est fourni. Consultez l'Avertissement pour plus de détails sur les risques.
  • Récompense
  • Commentaire
  • Partager
Commentaire
0/400
Aucun commentaire
  • Épingler
Trader les cryptos partout et à tout moment
qrCode
Scan pour télécharger Gate.io app
Communauté
Français (Afrique)
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • ไทย
  • Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)