Vai al contenuto

Ethereum Virtual Machine (EVM)

Guida sensibile al fork a transazioni EVM, message call, bytecode, stack, memoria, storage, gas, REVERT, DELEGATECALL, precompile, ricevute e riconciliazione dello stato.

Aggiornato

Solo a scopo educativo; non costituisce consulenza né raccomandazione d’investimento. Gli investimenti possono comportare perdite.

Risposta diretta

L’Ethereum Virtual Machine è la macchina a stati dell’execution layer che interpreta il bytecode secondo le regole di uno specifico fork. A parità di pre-stato valido, transazione o messaggio di primo livello, ambiente del blocco e specifica del fork, i client conformi devono calcolare lo stesso post-stato o esito di errore. L’EVM non decide l’ordine delle transazioni, non fornisce finalità del consenso, non autentica un frontend e non rende ogni chain EVM-compatible sicura quanto Ethereum.

Una transazione firmata esternamente è un oggetto di protocollo di primo livello. L’attività tra contratti consiste in message call e call frame annidati, non in transazioni indipendenti con nonce, ricevuta o hash propri. Ogni frame contiene codice, program counter, stack a 256 bit, memoria, calldata, returndata, gas e contesto di esecuzione; lo storage persistente appartiene a un account, mentre quello transitorio dura per la transazione secondo le regole del fork.

Come funziona

  1. Fissare lo snapshot di esecuzione: chain e chainId, rete, numero e hash del blocco, stato canonico o finalizzato, fork, client e revisione della specifica, pre-stato o state root, tipo di transazione, byte firmati, hash e ricevuta. Il determinismo dipende da questo contesto esatto.
  2. Decodificare l’envelope e separare validità preliminare ed esecuzione. Verificare firma e mittente, nonce, destinazione o creazione, valore, calldata, gas limit, campi tariffari, access list e campi specifici del tipo. Il rifiuto prima dell’inclusione non equivale a una transazione inclusa che ha eseguito un revert.
  3. Costruire il messaggio di primo livello e l’intero call tree. Registrare i frame CALL, STATICCALL, DELEGATECALL, creazione e precompile, nonché chiamante, indirizzo di contesto, indirizzo del codice, msg.sender, msg.value, trasferimento di valore, calldata, returndata, gas inoltrato e flag di successo. La voce “transazione interna” di un explorer è una vista della trace, non una transazione firmata.
  4. Tracciare program counter, stack, memoria, calldata, returndata, log, storage persistente e journal dello storage transitorio per ogni frame. CALL usa indirizzo e contesto di storage del chiamato; DELEGATECALL esegue il codice di destinazione nell’indirizzo e nello storage del chiamante preservando mittente e valore a monte; STATICCALL vieta modifiche dello stato.
  5. Applicare le regole gas del fork: gas intrinseco, costi dinamici degli opcode, espansione della memoria, accessi cold e warm, inoltro nelle call, stipend, precompile, rimborsi e relativi limiti. Calcolare poi le commissioni separatamente dal valore usando gas consumato e prezzo effettivo. Una stima del gas è condizionale, non una garanzia.
  6. Risolvere gli esiti per ambito. RETURN conferma il frame solo se gli antenati vengono poi confermati. REVERT annulla frame e discendenti, restituisce dati e non deve consumare tutto il gas residuo del frame; gli arresti eccezionali sono diversi. Un parent può intercettare il fallimento di una low-level call e continuare, quindi lo stato della ricevuta può essere 1 anche con un child fallito. Un fallimento incluso di primo livello consuma comunque nonce e gas pagato.
  7. Riconciliare stato della ricevuta, gas usato, log, indirizzo creato e dati restituiti con saldi, nonce, codice, storage persistente e transitorio, registri token, trace e state root prima e dopo. Se necessario rieseguire con un client indipendente e verificare separatamente implementazioni proxy, layout di storage, precompile, target EVM del compilatore e aggiornamenti del fork rispetto alla finalità del consenso.

Esempi svolti

  • Revert di primo livello e registro delle commissioni. Una transazione di tipo 2 ha gas limit 80,000, gas usato 52,000, base fee 20 gwei, max priority fee 3 gwei e max fee 40 gwei. Il prezzo effettivo è min(40, 20 + 3) = 23 gwei; la commissione effettiva è 52,000 * 23 gwei = 0.001196 ETH, composta da 52,000 * 20 gwei = 0.001040 ETH bruciati e 52,000 * 3 gwei = 0.000156 ETH di priority fee. I 28,000 gas inutilizzati non vengono addebitati. Se l’esecuzione di primo livello fa revert, storage, trasferimento di valore e log vengono annullati, ma nonce e commissione effettiva restano.
  • Fallimento child intercettato. Il contratto A parte da A.x = 5. Chiama B, che scrive B.y = 9, emette un log ed esegue REVERT. Scrittura e log di B vengono annullati. A osserva success = false, scrive A.x = 7 e termina normalmente. Lo stato finale della ricevuta è 1, vale A.x = 7 e B conserva il valore precedente. Il successo di primo livello non dimostra quindi che ogni child call sia riuscita.
  • Contesto di storage di DELEGATECALL. Un proxy ha slot0 = 5; l’account di implementazione ha slot0 = 99. Il codice legge lo slot 0, aggiunge 7 e salva il risultato. Eseguito mediante DELEGATECALL, modifica il proxy in slot0 = 12, mentre l’implementazione resta slot0 = 99; si applicano indirizzo e storage del proxy e si preservano mittente e valore a monte. Un layout incompatibile può corrompere lo stato del proxy.
  • SELFDESTRUCT dipendente dal fork. Secondo EIP-6780, un contratto esistente con 2 ETH esegue SELFDESTRUCT a favore di B. B riceve 2 ETH e il saldo del contratto diventa zero, ma account, codice e storage esistenti non vengono eliminati. L’eliminazione resta possibile solo se il contratto è stato creato e autodistrutto nella stessa transazione. È una regola specifica del fork, non retroattiva né necessariamente valida per ogni chain EVM.

Rischi

  • Rieseguire sulla chain, sul blocco, sul fork, sul client o sul pre-stato sbagliati.
  • Trattare come finale uno stato non canonico o soggetto a riorganizzazione.
  • Confondere il rifiuto preliminare con un revert incluso.
  • Presumere che una simulazione pending coincida con lo stato al momento dell’inclusione.
  • Ignorare revert di primo livello, arresto eccezionale o out-of-gas.
  • Non rilevare un fallimento child intercettato dal parent.
  • Leggere male contesto, indirizzo del codice, chiamante, mittente o valore.
  • Corrompere lo stato proxy per incompatibilità del layout con DELEGATECALL.
  • Consentire reentrancy o trasferimenti esterni non sicuri di valore e controllo.
  • Fidarsi di returndata, flag di successo o errori personalizzati non verificati.
  • Trattare log o trace come stato finale autorevole.
  • Calcolare male accessi cold e warm, memoria o gas inoltrato.
  • Applicare male rimborsi, limiti, stipend o regola 63/64.
  • Usare indirizzo, input, costo o semantica di fork errati per una precompile.
  • Confondere durata di storage persistente, memoria e storage transitorio.
  • Aspettarsi una modifica dello stato dentro STATICCALL.
  • Applicare a SELFDESTRUCT ipotesi di eliminazione anteriori a EIP-6780.
  • Non rilevare variazioni di implementazione proxy, admin o target del compilatore.
  • Eseguire regole divergenti o non aggiornate del client.
  • Dedurre sicurezza di consenso, bridge, token, governance o finalità dalla sola compatibilità EVM.

Idee errate comuni

  • Il codice sorgente Solidity è l’oggetto eseguito direttamente on-chain.
  • Una transazione inclusa che fa revert non costa nulla e non cambia il nonce.
  • Lo stato 1 della ricevuta dimostra che ogni call interna è riuscita come previsto.
  • Un evento o una trace rappresenta lo stato autorevole di asset e storage.
  • La compatibilità EVM garantisce opcode, gas, precompile, consenso e sicurezza identici.

Argomenti correlati

Fonti

Navigazione

Cerca nella wiki...