Leggere i dati grezzi della blockchain · Lezione 4 di 6
Transazioni Ethereum grezze, ricevute e log
9 min di lettura
Su Ethereum e sulle altre chain EVM, una transazione registra ciò che il mittente ha richiesto, mentre la ricevuta, i log e le tracce registrano ciò che è effettivamente accaduto. I movimenti di token, le chiamate non riuscite e il valore trasferito dai contratti non compaiono mai nella transazione stessa. Un analista che legge solo le transazioni si perde la maggior parte dell’attività sulla chain; chi conosce i quattro oggetti può dire con precisione che cosa si è spostato, chi lo ha attivato e se l’operazione è riuscita.
Campi delle transazioni e i cinque tipi
Da EIP-2718 (2021), una transazione Ethereum è un involucro tipizzato: un byte di tipo seguito da un payload i cui campi dipendono dal tipo. Le transazioni legacy, precedenti all’involucro, sono considerate di tipo 0.
| Tipo | Introdotto | Campi delle commissioni | Campi aggiuntivi |
|---|---|---|---|
0x0 legacy | Originale; ID della chain aggiunto da EIP-155 (2016) | gasPrice | ID della chain incorporato in v |
0x1 access list | EIP-2930, Berlin (aprile 2021) | gasPrice | accessList |
0x2 commissione dinamica | EIP-1559, London (agosto 2021) | maxFeePerGas, maxPriorityFeePerGas | accessList |
0x3 blob | EIP-4844, Dencun (marzo 2024) | Come il tipo 2, più maxFeePerBlobGas | blobVersionedHashes |
0x4 impostazione del codice | EIP-7702, Pectra (maggio 2025) | Come il tipo 2 | authorizationList |
Tutti i tipi condividono nonce (il contatore delle transazioni del mittente), gas (il limite di gas), to, value (in wei), input (i dati della chiamata) e una firma (v o yParity, r, s). Nota che from non è un campo: i nodi ricavano il mittente dalla firma e lo aggiungono alle risposte API. In una transazione legacy l’ID della chain è codificato in v come chainId × 2 + 35 o 36; le transazioni tipizzate contengono un chainId esplicito.
Due tipi più recenti sono importanti per gli analisti:
- Le transazioni blob (tipo 3) trasportano grandi blob di dati per i rollup. I blob non fanno parte del blocco di esecuzione: viaggiano come sidecar del livello di consenso che i nodi devono conservare solo per 4.096 epoche, circa 18 giorni. La transazione conserva solo l’hash versionato di ciascun blob (primo byte
0x01). Una transazione di tipo 3 deve avere un indirizzoto, quindi non può creare un contratto. - Le transazioni di impostazione del codice (tipo 4) permettono a un account con controllo esterno (EOA) di delegare a codice di contratto. Ogni voce dell’elenco di autorizzazioni è firmata dal proprietario dell’account e contiene
chainId,address,nonce,yParity,res. Dopo l’elaborazione, il codice dell’account è il designatore di delega di 23 byte0xef0100seguito dall’indirizzo del delegato; delegare all’indirizzo zero lo rimuove. Da quel momento, le chiamate a quell’EOA eseguono il codice del delegato, e l’account che ha firmato l’autorizzazione non deve necessariamente essere quello che ha pagato il gas. Le deleghe malevole sono state usate per svuotare wallet, quindi vale la pena notare quandoeth_getCoderestituisce0xef0100…per un indirizzo apparentemente ordinario. La lezione Il modello degli account per gli analisti illustra le conseguenze.
Ecco una transazione di tipo 2 nel formato restituito da eth_getTransactionByHash, con i valori lunghi troncati:
{
"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…"
}
Tutte le quantità sono stringhe esadecimali: il limite di gas 0x186a0 è 100.000, la commissione massima 0x9502f9000 è 40.000.000.000 wei (40 gwei) per gas e la mancia 0x3b9aca00 è 1 gwei. Il value è zero e to è un contratto: si tratta di una chiamata, non di un pagamento in ETH.
Ricevute e stato
Una ricevuta viene prodotta quando la transazione viene eseguita. eth_getTransactionReceipt restituisce:
| Campo | Significato |
|---|---|
status | 0x1 successo o 0x0 insuccesso (EIP-658, Byzantium, ottobre 2017; le ricevute più vecchie contengono invece root) |
gasUsed, cumulativeGasUsed | Gas utilizzato da questa transazione e dal blocco fino a essa inclusa |
effectiveGasPrice | Prezzo effettivamente pagato per unità di gas |
logs, logsBloom | Eventi emessi e il relativo filtro Bloom |
contractAddress | Indirizzo creato, per le distribuzioni; altrimenti null |
blobGasUsed, blobGasPrice | Presenti per le transazioni blob |
type, from, to, blockHash, blockNumber, transactionIndex | Identificatori |
Per una transazione di tipo 2, il prezzo effettivo è il minore tra maxFeePerGas e la somma della commissione base e di maxPriorityFeePerGas. Se il nostro esempio fosse stato eseguito con una commissione base di 12 gwei e avesse utilizzato 52.000 unità di gas, il prezzo sarebbe stato min(40, 12 + 1) = 13 gwei e la commissione 52.000 × 13 = 676.000 gwei, ovvero 0,000676 ETH. Una transazione blob paga anche blobGasUsed × blobGasPrice.
Quando status è 0x0, il valore non è stato trasferito, tutti i log sono stati scartati e le modifiche allo stato sono state annullate, ma il nonce è stato consumato e la commissione è stata pagata. status descrive solo la transazione nel suo complesso: una chiamata interna può non riuscire anche se la transazione ha successo, e questo è visibile solo nelle tracce.
Log e topic
Un log (evento) è un record che un contratto scrive durante l’esecuzione. Ogni log contiene:
address: il contratto che lo ha emesso;topics: da zero a quattro valori di 32 byte;data: i byte rimanenti codificati secondo l’ABI;- campi di posizione:
logIndex(posizione nel blocco),transactionHash,transactionIndex,blockNumber,blockHasheremoved, che è true quando una riorganizzazione ha eliminato il log.
Per un evento Solidity standard (non anonimo), topics[0] è l’hash Keccak-256 della firma dell’evento e i topic successivi contengono fino a tre parametri indexed. L’evento ERC-20 Transfer si decodifica così:
{
"address": "0x5d3a…7c10",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000a1b2…009f",
"0x000000000000000000000000c3d4…22e1"
],
"data": "0x000000000000000000000000000000000000000000000000000000009502f900",
"logIndex": "0x1c", "removed": false
}
topics[0] è il keccak256 di Transfer(address,address,uint256). I topic 1 e 2 sono gli indirizzi from e to, completati a sinistra fino a 32 byte: rimuovi i primi 12 byte per recuperarli. data contiene il value non indicizzato: 0x9502f900 corrisponde a 2.500.000.000 unità grezze, ovvero 2.500 token se il contratto dichiara 6 decimali. Controlla sempre address. Chiunque può distribuire un contratto che emette un evento Transfer perfettamente formato, ed è proprio così che si comportano i token falsi che imitano quelli noti; l’evento ha significato solo in base al contratto che lo ha scritto.
Dati di input e nozioni di base sulla decodifica ABI
Il campo input indica al contratto quale funzione eseguire. I suoi primi 4 byte sono il selettore della funzione, l’inizio dell’hash Keccak-256 della firma della funzione: transfer(address,uint256) produce 0xa9059cbb. Gli argomenti seguono in parole di 32 byte secondo le regole di codifica ABI: in questo caso, il destinatario completato a sinistra c3d4…22e1, poi l’importo 0x9502f900.
La decodifica richiede l’ABI del contratto (la descrizione della sua interfaccia), pubblicata insieme al codice sorgente verificato o ricostruita a partire da database pubblici di selettori. I selettori sono lunghi solo 4 byte, quindi firme diverse possono collidere, e una corrispondenza nel database è un’ipotesi, non una prova. Considera l’input come la richiesta e i log, le tracce e lo stato come l’esito: una chiamata a transfer che è stata annullata non ha trasferito nulla. La lezione Decodificare le interazioni con gli smart contract tratta proxy, multicall e codice sorgente verificato.
Tracce per le chiamate interne
Quando un contratto chiama un altro contratto o invia ETH, quella chiamata interna non è una transazione e non produce log, a meno che il codice non ne emetta uno. È visibile solo rieseguendo la transazione con il tracciamento. Esistono due formati di output:
debug_traceTransactionin stile Geth, concallTracer, restituisce un albero annidato di frame di chiamata, ciascuno contype(CALL, STATICCALL, DELEGATECALL, CREATE, CREATE2, SELFDESTRUCT),from,to,value,gas,gasUsed,input,output,errore le chiamate figliecalls.trace_transactionetrace_blockin stile Parity, offerti da client come Erigon, Nethermind e Reth, restituiscono un elenco lineare in cuitraceAddressindica la posizione di ogni chiamata nell’albero eaction.callTypene indica il tipo.
Le tracce rispondono a domande a cui nient’altro può rispondere: l’ETH pagato da un contratto, quale chiamata secondaria non è riuscita e in quale contesto è stato eseguito il codice (una DELEGATECALL esegue il codice di un altro contratto utilizzando lo storage e il saldo del chiamante). Per tracciare vecchie transazioni serve un nodo in grado di ricostruire lo stato storico, in genere un nodo archivio. Escludere i frame che contengono un error, e i relativi figli, evita di conteggiare valore che non è mai stato trasferito; la lezione Interrogare log e tracce EVM mostra come farlo in SQL.
Mettere insieme i quattro oggetti
Leggere l’esempio dall’inizio alla fine mostra come i quattro oggetti si completino a vicenda. La transazione dice che 0xa1b2…009f ha chiesto al contratto 0x5d3a…7c10 di eseguire transfer per il destinatario c3d4…22e1 e 2.500.000.000 unità grezze, senza pagare ETH. La ricevuta dice che la richiesta è riuscita (status 0x1) e che è costata 0,000676 ETH. Il log, emesso dallo stesso contratto, conferma che i saldi del token sono cambiati: 2.500 token sono passati da a1b2…009f a c3d4…22e1. Poiché la chiamata era un trasferimento diretto di token, una traccia mostrerebbe un solo frame e non aggiungerebbe altro.
Basta cambiare un dettaglio perché la lettura cambi. Se invece l’utente avesse chiamato un router DEX, la transazione mostrerebbe solo l’indirizzo del router e una richiesta di swap; i log mostrerebbero diversi eventi Transfer emessi da contratti di token differenti; e qualsiasi ETH pagato apparirebbe solo nelle tracce. La regola pratica: parti dalla transazione, controlla lo stato della ricevuta, ricava i movimenti dei token dai log e i movimenti di ETH da value e dalle tracce.
Focus sull'esame: i cinque tipi di transazione (da 0 a 4), i relativi EIP, aggiornamenti e campi distintivi; il fatto chefromvenga ricavato dalla firma; le transazioni blob, i sidecar e la conservazione dei blob per circa 18 giorni; le autorizzazioni EIP-7702 e il designatore di delega0xef0100; i campi della ricevuta,status, il prezzo effettivo del gas e ciò che costa comunque una transazione non riuscita; la struttura dei log,topics[0]come hash della firma dell’evento e la decodifica dell’evento Transfer; i selettori delle funzioni e perché le corrispondenze dei selettori sono ipotesi; le tracce in stile Geth e Parity per le chiamate interne.