Données on-chain et SQL · Leçon 4 sur 9
Transactions Ethereum brutes, reçus et journaux
10 min de lecture
Sur Ethereum et les autres chaînes EVM, une transaction consigne ce qu’un expéditeur a demandé, tandis que le reçu, les journaux et les traces consignent ce qui s’est réellement passé. Les mouvements de jetons, les appels ayant échoué et la valeur transférée par les contrats n’apparaissent jamais dans la transaction elle-même. Un analyste qui ne lit que les transactions passe à côté de la majeure partie de l’activité de la chaîne ; celui qui connaît les quatre objets peut dire précisément ce qui a été transféré, qui l’a déclenché et si l’opération a réussi.
Les champs des transactions et les cinq types
Depuis EIP-2718 (2021), une transaction Ethereum est une enveloppe typée : un octet de type suivi d’une charge utile dont les champs dépendent du type. Les transactions historiques, antérieures à cette enveloppe, sont considérées comme étant de type 0.
| Type | Introduction | Champs de frais | Champs supplémentaires |
|---|---|---|---|
0x0 historique | À l’origine ; identifiant de chaîne ajouté par EIP-155 (2016) | gasPrice | Identifiant de chaîne intégré à v |
0x1 liste d’accès | EIP-2930, Berlin (avril 2021) | gasPrice | accessList |
0x2 frais dynamiques | EIP-1559, London (août 2021) | maxFeePerGas, maxPriorityFeePerGas | accessList |
0x3 blob | EIP-4844, Dencun (mars 2024) | Comme pour le type 2, plus maxFeePerBlobGas | blobVersionedHashes |
0x4 définition de code | EIP-7702, Pectra (mai 2025) | Comme pour le type 2 | authorizationList |
Tous les types partagent les champs nonce (le compteur de transactions de l’expéditeur), gas (la limite de gas), to, value (en wei), input (les données d’appel) et une signature (v ou yParity, r, s). Notez que from n’est pas un champ : les nœuds retrouvent l’expéditeur à partir de la signature et l’ajoutent aux réponses de l’API. Dans une transaction historique, l’identifiant de chaîne est encodé dans v sous la forme chainId × 2 + 35 ou 36 ; les transactions typées contiennent un champ chainId explicite.
Deux types plus récents sont importants pour les analystes :
- Les transactions à blobs (type 3) transportent de volumineux blobs de données pour les rollups. Les blobs ne font pas partie du bloc d’exécution : ils circulent sous forme de données annexes (sidecars) de la couche de consensus, que les nœuds ne doivent conserver que pendant 4 096 époques, soit environ 18 jours. La transaction ne conserve que le hachage versionné de chaque blob (premier octet
0x01). Une transaction de type 3 doit avoir une adresseto; elle ne peut donc pas créer de contrat. - Les transactions de définition de code (type 4) permettent à un compte détenu par un utilisateur externe (EOA) de déléguer à du code de contrat. Chaque entrée de la liste d’autorisations est signée par le propriétaire du compte et contient
chainId,address,nonce,yParity,rets. Après traitement, le code du compte est le désignateur de délégation de 23 octets0xef0100suivi de l’adresse du délégataire ; une délégation à l’adresse zéro l’efface. Dès lors, les appels à cet EOA exécutent le code du délégataire, et le compte qui a signé l’autorisation n’est pas nécessairement celui qui a payé le gas. Des délégations malveillantes ont été utilisées pour vider des portefeuilles ; une réponse0xef0100…deeth_getCodepour une adresse apparemment ordinaire mérite donc d’être relevée. La leçon Le modèle de comptes et la reconstitution des soldes en examine les conséquences.
Voici une transaction de type 2 telle que la renvoie eth_getTransactionByHash, avec les valeurs longues tronquées :
{
"type": "0x2", "chainId": "0x1", "nonce": "0x2a",
"hash": "0x7e4b…a3c0", "blockNumber": "0x1406f52", "transactionIndex": "0x5",
"from": "0xa1b2…009f", "to": "0x5d3a…7c10", "value": "0x0",
"gas": "0x186a0", "maxFeePerGas": "0x9502f9000", "maxPriorityFeePerGas": "0x3b9aca00",
"input": "0xa9059cbb000000000000000000000000c3d4…22e1000…009502f900",
"accessList": [], "yParity": "0x1", "r": "0x8f1e…", "s": "0x2c07…"
}
Toutes les quantités sont des chaînes hexadécimales : la limite de gas 0x186a0 vaut 100 000, les frais maximaux 0x9502f9000 valent 40 000 000 000 wei (40 gwei) par unité de gas et le pourboire 0x3b9aca00 vaut 1 gwei. Le champ value est nul et to est un contrat : il s’agit d’un appel, pas d’un paiement en ETH.
Reçus et statut
Un reçu est produit lors de l’exécution de la transaction. eth_getTransactionReceipt renvoie :
| Champ | Signification |
|---|---|
status | 0x1 pour une réussite ou 0x0 pour un échec (EIP-658, Byzantium, octobre 2017 ; les reçus plus anciens contiennent root à la place) |
gasUsed, cumulativeGasUsed | Gas utilisé par cette transaction, et par le bloc jusqu’à cette transaction incluse |
effectiveGasPrice | Prix réellement payé par unité de gas |
logs, logsBloom | Événements émis et leur filtre de Bloom |
contractAddress | Adresse créée, pour les déploiements ; sinon, valeur nulle |
blobGasUsed, blobGasPrice | Présents pour les transactions à blobs |
type, from, to, blockHash, blockNumber, transactionIndex | Identifiants |
Pour une transaction de type 2, le prix effectif est le minimum entre maxFeePerGas et les frais de base augmentés de maxPriorityFeePerGas. Si notre exemple s’est exécuté avec des frais de base de 12 gwei et a utilisé 52 000 unités de gas, le prix était min(40, 12 + 1) = 13 gwei et les frais étaient de 52 000 × 13 = 676 000 gwei, soit 0,000676 ETH. Une transaction à blobs paie également blobGasUsed × blobGasPrice.
Lorsque status vaut 0x0, la valeur n’a pas été transférée, tous les journaux ont été supprimés et les modifications de l’état ont été annulées, mais le nonce a été consommé et les frais payés. status ne décrit que la transaction dans son ensemble : un appel interne peut échouer alors que la transaction réussit, ce que seules les traces révèlent.
Journaux et topics
Un journal (événement) est un enregistrement qu’un contrat écrit pendant son exécution. Chacun comporte :
address: le contrat qui l’a émis ;topics: de zéro à quatre valeurs de 32 octets ;data: les octets restants, encodés selon l’ABI ;- des champs de position :
logIndex(position dans le bloc),transactionHash,transactionIndex,blockNumber,blockHash, etremoved, qui vaut true lorsqu’une réorganisation a supprimé le journal.
Pour un événement Solidity standard (non anonyme), topics[0] est le hachage Keccak-256 de la signature de l’événement et les topics suivants contiennent jusqu’à trois paramètres indexed. L’événement Transfer ERC-20 se décode comme suit :
{
"address": "0x5d3a…7c10",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000a1b2…009f",
"0x000000000000000000000000c3d4…22e1"
],
"data": "0x000000000000000000000000000000000000000000000000000000009502f900",
"logIndex": "0x1c", "removed": false
}
topics[0] est le hachage keccak256 de Transfer(address,address,uint256). Les topics 1 et 2 sont les adresses from et to, complétées à gauche par des zéros jusqu’à 32 octets : supprimez les 12 premiers octets pour les retrouver. data contient le paramètre value non indexé : 0x9502f900 représente 2 500 000 000 unités brutes, soit 2 500 jetons si le contrat indique 6 décimales. Vérifiez toujours address. N’importe qui peut déployer un contrat qui émet un événement Transfer parfaitement formé, et c’est précisément ce que font les faux jetons imitant des jetons connus ; la valeur informative de l’événement dépend entièrement du contrat qui l’a écrit.
Données d’entrée et bases du décodage ABI
Le champ input indique à un contrat quelle fonction exécuter. Ses 4 premiers octets constituent le sélecteur de fonction, le début du hachage Keccak-256 de la signature de la fonction : transfer(address,uint256) donne 0xa9059cbb. Les arguments suivent sous forme de mots de 32 octets, selon les règles d’encodage ABI : ici, le destinataire c3d4…22e1 complété par des zéros, puis le montant 0x9502f900.
Le décodage nécessite l’ABI du contrat (la description de son interface), publiée avec le code source vérifié ou reconstituée à partir de bases de données publiques de sélecteurs. Les sélecteurs ne font que 4 octets ; des signatures différentes peuvent donc entrer en collision, et une correspondance dans une base de données constitue une hypothèse plutôt qu’une preuve. Considérez les données d’entrée comme la demande, et les journaux, les traces et l’état comme le résultat : un appel à transfer dont l’exécution a été annulée n’a rien transféré. La leçon Décodage des interactions avec les contrats intelligents traite des proxys, des appels multiples et du code source vérifié.
Traces des appels internes
Lorsqu’un contrat appelle un autre contrat ou envoie des ETH, cet appel interne n’est pas une transaction et ne produit aucun journal, sauf si le code en émet un. Il n’est visible qu’en réexécutant la transaction avec le traçage activé. Il existe deux styles de sortie :
- Le style Geth, avec
debug_traceTransactionetcallTracer, renvoie une arborescence imbriquée de cadres d’appel, chacun contenanttype(CALL, STATICCALL, DELEGATECALL, CREATE, CREATE2, SELFDESTRUCT),from,to,value,gas,gasUsed,input,output,erroret des appels enfantscalls. - Le style Parity, avec
trace_transactionettrace_block, proposé par des clients tels qu’Erigon, Nethermind et Reth, renvoie une liste à plat dans laquelletraceAddressindique la position de chaque appel dans l’arborescence etaction.callTypeson type.
Les traces répondent à des questions auxquelles rien d’autre ne peut répondre : les ETH versés par un contrat, le sous-appel qui a échoué et le contexte dans lequel le code s’est exécuté (un DELEGATECALL exécute le code d’un autre contrat en utilisant le stockage et le solde de l’appelant). Le traçage de transactions anciennes nécessite un nœud capable de reconstituer l’état historique, généralement un nœud d’archive. Exclure les cadres qui contiennent un champ error, ainsi que leurs enfants, évite de comptabiliser une valeur qui n’a jamais été transférée ; la leçon Interrogation des données UTXO et des journaux et traces EVM montre comment procéder en SQL.
Combiner les quatre objets
La lecture de bout en bout de l’exemple suivi montre comment les objets se complètent. La transaction indique que 0xa1b2…009f a demandé au contrat 0x5d3a…7c10 d’exécuter transfer pour le destinataire c3d4…22e1 et 2 500 000 000 unités brutes, sans verser d’ETH. Le reçu indique que la demande a réussi (status à 0x1) et a coûté 0,000676 ETH. Le journal, émis par le même contrat, confirme que les soldes du jeton ont changé : 2 500 jetons de a1b2…009f vers c3d4…22e1. Comme l’appel était un transfert direct de jetons, une trace ne montrerait qu’un seul cadre et n’ajouterait rien.
Modifiez un détail et l’interprétation change. Si l’utilisateur avait appelé un routeur DEX à la place, la transaction ne montrerait que l’adresse du routeur et une demande d’échange ; les journaux montreraient plusieurs événements Transfer provenant de différents contrats de jetons ; et tout versement d’ETH n’apparaîtrait que dans les traces. La règle pratique : partez de la transaction, vérifiez le statut du reçu, déterminez les mouvements de jetons à partir des journaux et les mouvements d’ETH à partir de value et des traces.
Points clés pour l'examen : les cinq types de transactions (0 à 4), leurs EIP, leurs mises à niveau et leurs champs distinctifs ; le fait quefromest retrouvé à partir de la signature ; les transactions à blobs, les données annexes et la conservation des blobs pendant environ 18 jours ; les autorisations EIP-7702 et le désignateur de délégation0xef0100; les champs du reçu,status, le prix effectif du gas et ce qu’une transaction échouée coûte malgré tout ; la structure des journaux,topics[0]comme hachage de la signature de l’événement et le décodage de l’événement Transfer ; les sélecteurs de fonction et les raisons pour lesquelles les correspondances de sélecteurs sont des hypothèses ; les traces de style Geth et de style Parity pour les appels internes.
Questions d’entraînement
Vérifiez votre compréhension. Ces questions ne comptent pas pour l’examen.
Connectez-vous pour enregistrer votre progression et reprendre là où vous vous êtes arrêté. C’est gratuit. Se connecter