Why am I sharing my travel stories?
Founder & CEO of TruStory. I have a passion for understanding things at a fundamental level and sharing it as clearly as possible.
Special thanks to Hacktar(@inzhoop) and Matteopey(@matteopey) for translating this post into Italian
Probabilmente avrete sentito parlare della blockchain di Ethereum, anche se magari non sapete di cosa si tratta. Recentemente è stato pubblicato un gran numero di notizie a riguardo, e alcune importanti riviste hanno anche dedicato la copertina, ma leggere questi articoli può creare confusione se non avete le basi per capire cosa sia esattamente Ethereum. Quindi, di cosa si tratta? In sostanza, è un database pubblico che conserva una registrazione permanente delle transazioni digitali. L'aspetto importante è che questo database non richiede alcuna autorità centrale per la gestione e la protezione. Al contrario, funziona come un sistema transazionale senza terze parti fiduciarie, ossia un framework in cui gli individui possono effettuare transazioni peer-to-peer senza la necessità di fidarsi di terze parti o degli altri.
Non è ancora chiaro? È qui che torna utile questo post. Il mio obiettivo è di spiegare come funziona Ethereum a livello tecnico, senza concetti matematici complessi o formule che possono incutere timore. Spero che anche chi non si intende di programmazione riesca alla fine a comprendere un po' meglio questa tecnologia. Se alcune parti sono troppo tecniche e difficili da comprendere, non spaventatevi. Non c'è bisogno di capire ogni minimo dettaglio. Il mio suggerimento è di concentrarsi sulla comprensione a livello generale.
Molti degli argomenti trattati in questo post riprendono i concetti discussi nello Yellow Paper. Ho aggiunto le mie spiegazioni e alcuni grafici per facilitare la comprensione di Ethereum. Chi se la sente di affrontare la sfida tecnica può leggere anche lo Yellow Paper di Ethereum.
Iniziamo!
Una blockchain è una " macchina singleton transazionale crittograficamente sicura con stato condiviso". [1] Suona bene, vero? Analizziamo i vari concetti.
Ethereum implementa questo paradigma blockchain.
La blockchain di Ethereum è essenzialmente una macchina a stati basata su transazioni. In informatica, una macchina a stati si riferisce a qualcosa che leggerà una serie di input e, sulla base di tali input, passerà a un nuovo stato.
La macchina a stati di Ethereum inizia con uno stato simile alla genesi, cioè analogo a una lista vuota, prima che qualsiasi transazione avvenga sulla rete. Quando vengono eseguite transazioni, questo stato di genesi passa a un determinato stato finale. In qualsiasi momento, questo stato finale rappresenta lo stato attuale di Ethereum.
Lo stato di Ethereum ha milioni di transazioni. Queste transazioni sono raggruppate in "blocchi". Un blocco contiene una serie di transazioni, e ogni blocco viene concatenato al blocco precedente.
Per passare da uno stato all'altro, la transazione deve essere valida. Affinché una transazione sia considerata valida, deve passare attraverso un processo di convalida detto mining. Mining è quando un gruppo di nodi (es. computer) utilizza le proprie risorse di calcolo per creare un blocco di transazioni valide.
Qualsiasi nodo della rete che si dichiara miner può provare a creare e convalidare un blocco. Molti miner di tutto il mondo cercano di creare e convalidare blocchi contemporaneamente. Ogni miner fornisce una "prova" matematica quando invia un blocco alla blockchain e questa prova funge da garanzia: se la prova esiste, il blocco deve essere valido.
Per aggiungere un blocco alla blockchain principale, il miner deve fornire la prova più velocemente di qualsiasi altro miner concorrente. Il processo di convalida di ciascun blocco con un miner che fornisce una prova matematica è detto "proof of work".
Un miner che convalida un nuovo blocco viene premiato con un determinato valore per il suo lavoro. Qual è questo valore? La blockchain di Ethereum utilizza un token digitale intrinseco chiamato "Ether". Ogni volta che un miner fornisce la prova per un blocco, nuovi token Ether vengono generati e assegnati.
Ci si potrebbe chiedere: quali garanzie ci sono che tutti si attengano a una sola catena di blocchi? Come possiamo essere certi che non esista un sottogruppo di miner che decidono di creare una propria catena di blocchi?
In precedenza, abbiamo definito una blockchain come macchina singleton transazionale con stato condiviso. In base a questa definizione, possiamo capire che lo stato corrente corretto è un'unica verità globale, che tutti devono accettare. Avere più stati (o catene) danneggerebbe l'intero sistema, perché sarebbe impossibile essere d'accordo su quale stato sia quello corretto. Se le catene dovessero divergere, si potrebbero possedere 10 monete su una catena, 20 su un'altra, 40 su un'altra ancora. In questo scenario, non ci sarebbe modo di determinare quale catena sia la più "valida".
Ogni volta che vengono generati più percorsi, si verifica una biforcazione. In genere vogliamo evitare biforcazioni, perché destabilizzano il sistema e costringono le persone a scegliere in quale catena “credere”.
Per determinare qual'è il percorso più valido e impedire la nascita di più catene, Ethereum utilizza un meccanismo chiamato "protocollo GHOST".
"GHOST" = "Greedy Heaviest Observed Subtree"
In termini semplici, il protocollo GHOST afferma che dobbiamo scegliere il percorso su cui si è verificato il maggior numero di calcoli. Un modo per determinare di quale percorso si tratta è utilizzare il numero di blocchi del blocco più recente (il "blocco foglia"), che rappresenta il numero totale di blocchi nel percorso corrente (senza contare il blocco genesi). Più alto è il numero di blocchi, più lungo è il percorso e maggiore è il lavoro di mining effettuato per arrivare alla foglia. Utilizzando questo ragionamento, possiamo accordarci sulla versione canonica dello stato attuale.
Ora che abbiamo fornito una panoramica molto generica di cosa sia una blockchain, approfondiamo i componenti principali del sistema Ethereum:
Una nota prima di iniziare: ogni volta che parlo di "hash" di X, mi riferisco all'hash KECCAK-256 utilizzato da Ethereum.
Lo "stato condiviso" globale di Ethereum è composto da molti piccoli oggetti ("account") che sono in grado di interagire tra loro attraverso un framework che passa messaggi. A ogni account è associato uno stato e un indirizzo a 20 byte. Un indirizzo in Ethereum è un identificatore a 160 bit che viene utilizzato per identificare qualsiasi account.
Esistono due tipi di account:
È importante comprendere la differenza fondamentale tra account esterni e account a contratto. Un account esterno può inviare messaggi ad altri account esterni OPPURE ad altri account a contratto creando e firmando una transazione mediante la propria chiave privata. Un messaggio tra due account esterni è semplicemente un trasferimento di valore. Ma un messaggio da un account esterno a un account a contratto attiva il codice dell'account a contratto, consentendogli di eseguire diverse azioni (ad esempio trasferire token, scrivere nello storage interno, coniare nuovi token, eseguire calcoli, creare nuovi contratti, ecc.).
A differenza degli account esterni, gli account a contratto non possono avviare da soli nuove transazioni. Possono generare transazioni solo in risposta ad altre transazioni ricevute (da account esterni o da un altro account a contratto). Scopriremo di più sulle chiamate da contratto a contratto nella sezione "Transazioni e messaggi".
Pertanto, qualsiasi azione che si verifichi nella catena di Ethereum è sempre avviata da transazioni originate da account esterni.
Lo stato dell'account è costituito da quattro componenti, che sono presenti indipendentemente dal tipo di account:
A questo punto sappiamo che lo stato globale di Ethereum consiste in una mappatura tra gli indirizzi degli account e gli stati degli account. Questa mappatura è memorizzata in una struttura di dati detta albero di Merkle Patricia.
Un albero di Merkle (detto anche "Merkle trie") è un tipo di albero binario composto da un insieme di nodi con:
I dati nella parte inferiore dell'albero vengono generati suddividendo i dati che vogliamo archiviare in porzioni, quindi suddividendo le porzioni in bucket e quindi prendendo l'hash di ciascun bucket e ripetendo lo stesso processo fino a quando il numero totale di hash rimanenti diventa uno solo: l'hash radice.
Questo albero deve avere una chiave per ogni valore memorizzato al suo interno. A partire dal nodo radice dell'albero, la chiave deve indicare quale nodo figlio seguire per ottenere il valore corrispondente, che è memorizzato nei nodi foglia. Nel caso di Ethereum, la mappatura chiave/valore per l'albero degli stati è tra gli indirizzi e i loro account associati, inclusi saldo, nonce, codeHash e storageRoot per ciascun account (dove storageRoot è esso stesso un albero).
Fonte: white paper Ethereum La stessa struttura dell'albero è utilizzata anche per memorizzare transazioni e ricevute. Più nello specifico, ogni blocco ha un'"intestazione" che memorizza l'hash del nodo radice di tre diverse strutture di Merkle trie, tra cui:
La capacità di archiviare tutte queste informazioni in modo efficiente in diversi Merkle trie è incredibilmente utile in Ethereum per quelli che chiamiamo "client leggeri" o "nodi leggeri". Ricordate che una blockchain è gestita da un gruppo di nodi. In linea di massima, esistono due tipi di nodi: nodi completi e nodi leggeri.
Un nodo archivio completo sincronizza la blockchain scaricando l'intera catena, dal blocco genesi al blocco intestazione corrente, ed eseguendo tutte le transazioni contenute all'interno. In genere, i miner archiviano il nodo archivio completo, perché è necessario ai fini del processo di mining. È anche possibile scaricare un nodo completo senza eseguire ogni transazione. Indipendentemente da ciò, qualsiasi nodo completo contiene l'intera catena.
Ma a meno che un nodo non debba eseguire tutte le transazioni o eseguire facilmente query sui dati storici, non è davvero necessario archiviare l'intera catena. È qui che entra in gioco il concetto di nodo leggero. Invece di scaricare e archiviare l'intera catena ed eseguire tutte le transazioni, i nodi leggeri scaricano solo la catena di intestazioni, dal blocco genesi all'intestazione corrente, senza eseguire alcuna transazione o recuperare alcuno stato associato. Poiché i nodi leggeri hanno accesso alle intestazioni dei blocchi, che contengono hash di tre alberi, possono comunque generare e ricevere facilmente risposte verificabili su transazioni, eventi, saldi, ecc.
Tutto ciò funziona perché gli hash nell'albero di Merkle si propagano verso l'alto: se un hacker tenta di scambiare una transazione non autentica che si trova in fondo a un albero di Merkle, questo cambiamento causerà una modifica nell'hash del nodo che si trova sopra, che cambierà l'hash del nodo ancora sopra, e così via, fino a cambiare la radice dell'albero.
Qualsiasi nodo che desideri verificare un dato può utilizzare la cosiddetta "prova di Merkle".
Una prova di Merkle è composta da:
Chiunque legga la prova può verificare che l'hash di quel ramo è coerente per tutto l'albero, e quindi che una data porzione si trova effettivamente in quella posizione nell'albero.
In sintesi, usando un albero di Merkle Patricia si ha il vantaggio che il nodo radice di questa struttura è crittograficamente dipendente dai dati memorizzati nell'albero, e quindi l'hash del nodo radice può essere usato come identità sicura per questi dati. Poiché l'intestazione dei blocchi include l'hash radice degli alberi dello stato, delle transazioni e delle ricevute, qualsiasi nodo può convalidare una piccola parte dello stato di Ethereum senza per forza contenere l'intero stato, che può essere potenzialmente illimitato nelle dimensioni.
Un concetto molto importante in Ethereum è quello delle commissioni. Ogni calcolo che si verifica a seguito di una transazione sulla rete Ethereum è soggetto a una commissione. Niente è gratis! La commissione corrisposta viene denominata "carburante". Il carburante è l'unità di misura utilizzata per le commissioni richieste per un determinato calcolo.
Il prezzo del carburante è la quantità di Ether che siete disposti a spendere per ogni unità di carburante, ed è misurato in "gwei". "Wei" è la più piccola unità di Ether, dove 1⁰¹⁸ Wei rappresenta 1 Ether. Un gwei è composto da 1.000.000 Wei.
Con ogni transazione, un mittente imposta un limite di carburante e il prezzo del carburante. Il prodotto di prezzo del carburante per limite di carburante rappresenta l'importo massimo di Wei che il mittente è disposto a pagare per l'esecuzione di una transazione.
Ad esempio, supponiamo che il mittente fissi il limite del carburante a 50.000 e un prezzo per il carburante di 20 gwei. Questo significa che il mittente è disposto a spendere al massimo 50.000 x 20 gwei = 1.000.000.000.000.000.000 Wei = 0,001 Ether per eseguire la transazione.
Ricorda che il limite di carburante rappresenta il carburante massimo per il quale il mittente è disposto a spendere denaro. Se ha Ether sufficienti nel saldo del proprio conto per coprire questo costo massimo, può partire. Il mittente viene rimborsato per l'eventuale carburante non utilizzato al termine della transazione, scambiato alla tariffa originale.
Nel caso in cui il mittente non metta a disposizione carburante sufficiente per eseguire la transazione, questa rimane "a secco" e viene considerata non valida. In questo caso, l'elaborazione della transazione si interrompe e qualsiasi cambiamento di stato che si è verificato viene annullato, in modo tale da tornare allo stato di Ethereum precedente alla transazione. Inoltre, viene registrato un record del fallimento della transazione che mostra quale transazione è stata tentata e dove ha fallito. E poiché la macchina ha già lavorato per eseguire i calcoli prima di esaurire il carburante, logicamente il carburante utilizzato non viene rimborsato al mittente.
Dove va a finire esattamente questo denaro speso per il carburante? Tutti i soldi spesi per il carburante dal mittente vengono inviati all'indirizzo "beneficiario", che in genere è l'indirizzo del miner. Poiché i miner lavorano per eseguire calcoli e convalidare le transazioni, ricevono il costo del carburante come ricompensa.
In genere, più alto è il prezzo del carburante che il mittente è disposto a pagare, maggiore sarà il valore che il miner trarrà dalla transazione, e quindi più alta sarà la probabilità che i miner la scelgano. I miner sono quindi liberi di scegliere quali transazioni convalidare o ignorare. Al fine di aiutare i mittenti a fissare il prezzo del carburante, i miner hanno la possibilità di pubblicizzare il prezzo minimo del carburante per il quale effettueranno le transazioni.
Il carburante viene utilizzato non solo per pagare le fasi di calcolo, ma anche per l'utilizzo dello storage. Il costo totale dello storage è proporzionale al multiplo più piccolo di 32 byte utilizzato.
Le commissioni per lo storage hanno alcuni aspetti più complessi. Ad esempio, poiché l'aumento dello storage comporta un aumento delle dimensioni del database degli stati di Ethereum su tutti i nodi, viene incentivato un utilizzo minimo dello storage per i dati archiviati. Per questo motivo, se una transazione prevede un passaggio che cancella una voce nello storage, la commissione richiesta per eseguire l'operazione viene annullata e allo stesso tempo viene concesso un rimborso per aver liberato spazio nello storage.
Un aspetto importante del funzionamento di Ethereum è che ogni operazione eseguita dalla rete viene effettuata simultaneamente da ogni nodo completo. Però i passaggi di calcolo sulla macchina virtuale Ethereum sono molto costosi. Gli smart contract di Ethereum sono utilizzati al meglio per attività semplici, come l'esecuzione di logica business semplice o la verifica di firme e altri oggetti crittografici, piuttosto che usi più complessi, come archiviazione di file, e-mail o machine learning, che possono mettere a dura prova la rete. L'imposizione di commissioni impedisce agli utenti di sovraccaricare la rete.
Ethereum è un linguaggio Turing completo. In breve, una macchina Turing è una macchina che può simulare qualsiasi algoritmo informatico (chi non ha familiarità con le macchine Turing può dare un'occhiata qui e qui). Questo permette di utilizzare i cicli e rende Ethereum vulnerabile al problema di arresto, cioè non è possibile determinare se un programma verrà eseguito all'infinito. Se non ci fossero commissioni, un utente malintenzionato potrebbe facilmente tentare di interrompere il funzionamento della rete eseguendo un ciclo infinito all'interno di una transazione, senza alcuna ripercussione negativa. Le commissioni quindi proteggono la rete da attacchi deliberati.
Si potrebbe pensare: "perché bisogna anche pagare lo storage?" Esattamente come il calcolo, anche lo storage sulla rete Ethereum è un costo che l'intera rete deve sostenere.
Abbiamo fatto notare in precedenza che Ethereum è una macchina a stati basata sulle transazioni. In altre parole, le transazioni che si verificano tra diversi account sono ciò che fa muovere globalmente Ethereum da uno stato a quello successivo.
In parole semplici, una transazione è un'istruzione firmata crittograficamente che viene generata da un account esterno, serializzata e quindi inviata alla blockchain.
Esistono due tipi di transazioni: chiamate di messaggi e creazioni di contratti (cioè transazioni che creano nuovi contratti Ethereum).
Indipendentemente dal loro tipo, tutte le transazioni contengono i seguenti componenti:
Nella sezione "Account" abbiamo capito che le transazioni (sia le chiamate di messaggi che le transazioni che creano contratti) sono sempre avviate da account esterni e inviate alla blockchain. Si può anche pensare alle transazioni come al legame tra il mondo esterno e lo stato interno di Ethereum.
Ma questo non significa che i contratti non possano parlare con altri contratti. I contratti esistenti nell'ambito globale dello stato di Ethereum possono parlare con altri contratti nello stesso ambito. Lo fanno attraverso “messaggi” (o “transazioni interne”) ad altri contratti. Possiamo considerare i messaggi o le transazioni interne simili alle transazioni, con l'importante differenza che NON sono generati da account esterni. Sono generati da contratti. Sono oggetti virtuali che, a differenza delle transazioni, non sono serializzati ed esistono solo nell'ambiente di esecuzione di Ethereum.
Quando un contratto invia una transazione interna a un altro contratto, viene eseguito il codice associato presente nell'account a contratto destinatario.
Un aspetto importante da tenere a mente è che le transazioni interne o i messaggi non contengono un limite di carburante perché questo è determinato dal creatore esterno della transazione originale (cioè da un account esterno). Il limite di carburante che l'account esterno imposta deve essere sufficientemente elevato per consentire di eseguire la transazione, comprese eventuali esecuzioni secondarie attivate dalla transazione, come ad esempio i messaggi da contratto a contratto. Se, nella catena di transazioni e messaggi, una particolare esecuzione di messaggi rimane "a secco" di carburante, l'esecuzione del messaggio viene ripristinata, insieme a tutti i messaggi successivi attivati dall'esecuzione. Non è però necessario ripristinare l'esecuzione padre.
Tutte le transazioni sono raggruppate in "blocchi". Una blockchain contiene una serie di blocchi concatenati.
In Ethereum, un blocco è composto da:
Cosa è un "ommer"? È un blocco il cui padre corrisponde al padre del padre del blocco corrente. Vediamo rapidamente per cosa vengono utilizzati gli ommer e perché un blocco contiene le intestazioni dei blocchi degli ommer.
A causa del modo in cui è strutturato Ethereum, i tempi dei blocchi sono molto più bassi (~15 secondi) di quelli di altre blockchain, ad esempio Bitcoin (~10 minuti). Questo consente di elaborare le transazioni più velocemente. Tuttavia, uno degli aspetti negativi associati al fatto di avere tempi più brevi per i blocchi è che i miner trovano soluzioni più concorrenti per i blocchi. Questi blocchi concorrenti sono definiti anche "blocchi orfani" (cioè i blocchi sottoposti a mining che non finiscono nella catena principale).
Lo scopo degli ommer è contribuire a premiare i miner per l'inclusione di questi blocchi orfani. Gli ommer che i miner includono devono essere "validi", ovvero trovarsi non oltre la sesta generazione del blocco attuale. Dopo sei figli, i blocchi orfani non aggiornati non possono più essere referenziati (perché includere transazioni più vecchie complicherebbe le cose).
I blocchi ommer sono associati a una ricompensa inferiore rispetto a un blocco intero. Questo rimane comunque un incentivo per i miner, che ottengono una ricompensa se includono blocchi orfani.
Torniamo per un attimo ai blocchi. In precedenza abbiamo detto che ogni blocco ha un'"intestazione". Ma di cosa si tratta esattamente?
L'intestazione di un blocco è una parte del blocco composta da:
Si noti che ogni intestazione di blocco contiene tre strutture ad albero per:
Queste strutture ad albero non sono altro che gli alberi di Merkle Patricia di cui abbiamo parlato prima.
Ci sono anche alcuni termini contenuti nella descrizione precedente che meritano di essere chiariti. Diamo un'occhiata.
Ethereum permette di creare log per poter tracciare transazioni e messaggi. Un contratto può esplicitamente generare un log definendo gli "eventi" che desidera registrare.
Una voce di log contiene:
I log sono memorizzati in un filtro bloom, che contiene i dati di log infiniti in modo efficiente.
I log memorizzati nell'intestazione provengono dalle informazioni di log contenute nella ricevuta della transazione. Esattamente come si ottiene una ricevuta quando si acquista qualcosa in un negozio, Ethereum genera una ricevuta per ogni transazione. Come vi aspettereste, ogni ricevuta contiene alcune informazioni sulla transazione. Questa ricevuta include voci come:
La "difficoltà" di un blocco viene utilizzata per imporre la coerenza nel tempo necessario per convalidare i blocchi. Il blocco genesi ha una difficoltà di 131,072 e viene utilizzata una formula speciale per calcolare la difficoltà di ogni blocco successivo. Se un determinato blocco viene convalidato più velocemente rispetto a quello precedente, il protocollo Ethereum ne aumenta la difficoltà.
La difficoltà del blocco influenza il nonce, che è un hash che deve essere calcolato quando si esegue il mining di un blocco, utilizzando l'algoritmo proof of work.
La relazione tra la difficoltà del blocco e il suo nonce è espressa matematicamente come:
dove Hd è la difficoltà.
L'unico modo per trovare un nonce che soddisfi la soglia di difficoltà è utilizzare l'algoritmo proof of work per enumerare tutte le possibilità. Il tempo previsto per trovare una soluzione è proporzionale alla difficoltà: più aumenta la difficoltà, più difficile è trovare il nonce e quindi convalidare il blocco, che a sua volta aumenta il tempo necessario per convalidare un nuovo blocco. Quindi, modificando la difficoltà di un blocco, il protocollo può modificare il tempo necessario per convalidare un blocco.
Se invece i tempi di convalida si fanno più lenti, il protocollo riduce la difficoltà. In questo modo, il tempo di convalida si autoregola per mantenere un tasso costante, in media un blocco ogni 15 secondi.
Siamo arrivati a una delle parti più complesse del protocollo Ethereum: l'esecuzione di una transazione. Immaginiamo di inviare una transazione alla rete Ethereum per l'elaborazione. Cosa avviene per modificare lo stato di Ethereum e includere questa transazione?
In primo luogo, tutte le transazioni devono soddisfare una serie iniziale di requisiti per poter essere eseguite. Tra questi vi sono:
Se la transazione soddisfa tutti i requisiti di validità appena elencati, si passa alla fase successiva.
Innanzitutto, sottraiamo il costo anticipato per l'esecuzione dal saldo del mittente e aumentiamo il nonce dell'account del mittente di 1 per tenere conto della transazione corrente. A questo punto, possiamo calcolare il carburante rimanente come limite di carburante totale per la transazione meno il carburante intrinseco utilizzato.
Quindi inizia l'esecuzione della transazione. Durante l'esecuzione di una transazione, Ethereum tiene traccia dello "stato secondario". È un modo per registrare le informazioni accumulate durante la transazione che saranno necessarie immediatamente dopo il completamento della transazione. Nello specifico, contiene:
Successivamente, vengono elaborati i vari calcoli richiesti dalla transazione.
Una volta che tutti i passaggi richiesti dalla transazione sono stati elaborati e supponendo che non vi sia uno stato non valido, lo stato viene finalizzato determinando la quantità di carburante inutilizzato da rimborsare al mittente. Oltre al carburante inutilizzato, al mittente vengono rimborsate anche alcune quote dal "saldo di rimborso" che abbiamo descritto sopra.
Una volta rimborsato il mittente:
Infine, ci restano da vedere il nuovo stato e una serie di log creata dalla transazione.
Ora che abbiamo trattato i concetti di base dell'esecuzione delle transazioni, diamo un'occhiata ad alcune differenze tra transazioni che creano contratti e chiamate di messaggi.
Non dimentichiamo che in Ethereum esistono due tipi di account: account a contratto e account esterni. Quando diciamo che una transazione "crea un contratto", intendiamo che lo scopo della transazione è creare un nuovo account a contratto.
Per creare un nuovo account a contratto, dichiariamo innanzitutto l'indirizzo del nuovo account utilizzando una formula speciale. Quindi inizializziamo il nuovo account:
Una volta inizializzato l'account, possiamo effettivamente creare l'account, utilizzando il codice di inizializzazione inviato con la transazione (vedere la sezione "Transazione e messaggi" per un ripasso del codice di inizializzazione). Ciò che accade durante l'esecuzione di questo codice di inizializzazione non sempre è uguale. A seconda del costruttore del contratto, potrebbe avvenire l'aggiornamento dello storage dell'account, la creazione di altri account a contratto, la chiamata di altri messaggi, ecc.
L'esecuzione del codice per inizializzare un contratto utilizza carburante. La transazione non può utilizzare più carburante di quanto ne rimane. Se il carburante non basta, l'esecuzione genera un'eccezione out-of-gas (OOG) e viene interrotta. Se la transazione viene interrotta a causa di un'eccezione di carburante esaurito, lo stato viene ripristinato al punto immediatamente precedente la transazione. Al mittente non viene rimborsato il carburante speso prima dell'esaurimento. Se però il mittente invia Ether con la transazione, il valore in Ether viene rimborsato anche se la creazione del contratto non riesce.
Meno male.
Se il codice di inizializzazione viene eseguito correttamente, viene versato un costo finale per la creazione del contratto.
Si tratta di un costo per lo storage ed è proporzionale alla dimensione del codice del contratto creato (di nuovo, niente è gratis!). Se non rimane abbastanza carburante per pagare questo costo finale, la transazione dichiara un'eccezione di carburante esaurito e si interrompe.
Se tutto va bene e si procede senza eccezioni, il carburante inutilizzato viene rimborsato al mittente originale della transazione e lo stato alterato può rimanere tale.
Evviva.
L'esecuzione di una chiamata di messaggio è simile a quella per la creazione di un contratto, con alcune piccole differenze.
L'esecuzione di una chiamata di messaggio non include alcun codice di inizializzazione, poiché non vengono creati nuovi account. Può però contenere dati di input, se questi dati sono stati forniti dal mittente della transazione. Una volta eseguite, le chiamate ai messaggi hanno anche un componente aggiuntivo contenente i dati di output, che viene utilizzato se un'esecuzione successiva richiede questi dati.
Come avviene per la creazione del contratto, se l'esecuzione di una chiamata di messaggio viene interrotta perché si esaurisce il carburante o perché la transazione non è valida (ad esempio overflow dello stack, destinazione di salto non valida o istruzione non valida), il carburante utilizzato non viene rimborsato al chiamante originale. Viene cioè utilizzato tutto il carburante inutilizzato rimanente e lo stato viene riportato al punto immediatamente precedente al trasferimento del saldo.
Fino all'aggiornamento più recente di Ethereum, non c'era modo di interrompere o ripristinare l'esecuzione di una transazione senza che il sistema consumasse tutto il carburante fornito. Ad esempio, supponiamo che abbiate creato un contratto che genera un errore quando un chiamante non è stato autorizzato a eseguire alcune transazioni. Nelle versioni precedenti di Ethereum, il carburante rimanente verrebbe comunque consumato e niente verrebbe rimborsato al mittente. L'aggiornamento a Bisanzio invece include un nuovo codice di "ripristino" che consente a un contratto di interrompere l'esecuzione e ripristinare i cambiamenti di stato, senza utilizzare il carburante rimanente e con la possibilità di restituire un motivo per la transazione non riuscita. Se una transazione viene interrotta a causa di un ripristino, il carburante non utilizzato viene restituito al mittente.
Finora abbiamo visto i passaggi necessari affinché una transazione venga eseguita dall'inizio alla fine. Ora vedremo come viene effettivamente eseguita la transazione all'interno della VM.
La parte del protocollo che gestisce effettivamente l'elaborazione delle transazioni è la macchina virtuale di Ethereum, detta Ethereum Virtual Machine (EVM). L'EVM è una macchina virtuale completa in Turing, come definito in precedenza.
L'unica limitazione che l'EVM ha, al contrario di una tipica macchina completa in Turing, è quella di essere legata intrinsecamente al carburante. Pertanto, la quantità totale di calcolo che può essere eseguita è intrinsecamente limitata dalla quantità di carburante fornita.
Fonte: CMU Inoltre, l'EVM ha un'architettura basata su stack. Una macchina a stack è un computer che utilizza uno stack last-in, first-out (LIFO) per mantenere valori temporanei.
La dimensione di ciascun elemento dello stack nell'EVM è di 256 bit e lo stack ha una dimensione massima di 1024.
L'EVM ha una memoria che archivia gli elementi come array di byte indirizzati a parole. La memoria è volatile, cioè non è permanente.
L'EVM dispone anche di spazio di storage. A differenza della memoria, lo storage non è volatile e viene mantenuto come parte dello stato del sistema. L'EVM archivia il codice del programma separatamente, in una ROM virtuale a cui è possibile accedere solo tramite istruzioni speciali. In questo senso, c'è una differenza rispetto alla tipica architettura von Neumann, in cui il codice del programma è contenuto nella memoria o nello storage.
L'EVM ha anche un suo linguaggio: "bytecode EVM". Quando un programmatore come me o voi scrive smart contract che funzionano in Ethereum, in genere scrive codice in un linguaggio di livello superiore come Solidity. Può quindi compilarlo in bytecode EVM che EVM può comprendere.
Ok, ora passiamo all'esecuzione.
Prima di eseguire un determinato calcolo, il processore si accerta che le seguenti informazioni siano disponibili e valide:
All'inizio dell'esecuzione, la memoria e lo stack sono vuoti e il contatore del programma è zero.
PC: 0 STACK: [] MEM: [], STORAGE: {}
L'EVM esegue la transazione in modo ricorsivo, calcolando lo stato del sistema e lo stato della macchina per ogni ciclo. Lo stato del sistema è semplicemente lo stato globale di Ethereum. Lo stato della macchina comprende:
Gli elementi dello stack sono aggiunti o rimossi dalla porzione più a sinistra della serie.
A ogni ciclo, la quantità appropriata di carburante viene sottratta dal carburante restante e il contatore del programma viene incrementato.
Alla fine di ogni ciclo, ci sono tre possibilità:
Supponendo che l'esecuzione non raggiunga uno stato di eccezione ma un arresto "controllato" o normale, la macchina genera lo stato risultante, il carburante rimanente dopo l'esecuzione, lo stato secondario ottenuto e l'output risultante.
Bene. Abbiamo trattato una delle parti più complesse di Ethereum. Anche se non avete capito proprio tutto, non importa. Non è necessario comprendere tutti i dettagli dell'esecuzione, a meno che non stiate lavorando a un livello molto complesso.
Infine, vediamo come viene finalizzato un blocco di molte transazioni.
Il termine "finalizzato", può significare due cose diverse, a seconda che il blocco sia nuovo o esistente. Se si tratta di un blocco nuovo, ci riferiamo al processo necessario per il mining del blocco. Se si tratta di un blocco esistente, allora stiamo parlando del processo di convalida del blocco. In entrambi i casi, i requisiti per "finalizzare" un blocco sono quattro:
1) Convalida (o, in caso di mining, determinazione) degli ommer
Ogni blocco ommer all'interno dell'intestazione di un blocco deve essere un'intestazione valida e trovarsi entro la sesta generazione del blocco corrente.
2) Convalida (o, in caso di mining, determinazione) delle transazioni
Il valore di gasUsed sul blocco deve essere uguale al carburante totale utilizzato dalle transazioni elencate nel blocco. Tenete a mente che quando eseguiamo una transazione, teniamo traccia del contatore di carburante del blocco, che a sua volta tiene traccia del carburante totale utilizzato da tutte le transazioni del blocco.
3) Applicazione della ricompensa (solo in caso di mining)
L'indirizzo del beneficiario riceve 5 Ether per il mining del blocco. (Secondo la proposta Ethereum EIP-649, questa ricompensa di 5 ETH sarà presto ridotta a 3 ETH). Inoltre, per ogni ommer, il beneficiario corrente del blocco riceve un ulteriore 1/32 della ricompensa per il blocco corrente. Infine, anche al beneficiario del blocco ommer viene assegnato un certo importo (c'è una formula speciale per il calcolo).
4) Verifica di stato e nonce (o, se mining, calcolo di uno valido)
Tutte le transazioni e le modifiche di stato risultanti devono essere state applicate. Il nuovo blocco viene quindi definito come stato dopo che la ricompensa per il blocco è stata applicata allo stato risultante della transazione finale. La verifica avviene controllando questo stato finale rispetto all'albero dello stato contenuto nell'intestazione.
Nella sezione “Blocchi” è stato brevemente affrontato il concetto di difficoltà dei blocchi. L'algoritmo che dà senso alla difficoltà del blocco è chiamato Proof of Work (PoW).
L'algoritmo proof-of-work di Ethereum si chiama “Ethash” (precedentemente Dagger-Hashimoto).
L'algoritmo è definito formalmente come:
dove m è mixHash , n è nonce, Hn è l'intestazione del nuovo blocco (esclusi i componenti nonce e mixHash che devono essere calcolati), Hn è il nonce dell'intestazione del blocco e d è il DAG , che è un set di dati di grandi dimensioni. Nella sezione " Blocchi *", abbiamo parlato dei vari elementi presenti nell'intestazione di un blocco.
Due di questi componenti erano stati chiamati **mixHash e nonce.
Come ricorderete:
La funzione PoW è utilizzata per valutare questi due elementi.
Come vengono calcolati esattamente mixHash e nonce usando la funzione PoW è un po' complesso, e andrebbe approfondito in un post separato. Ma a un livello alto funziona così:
per ogni blocco viene calcolato un "seed". Questo seed è diverso per ogni “epoca”, dove ogni epoca dura 30.000 blocchi. Per la prima epoca, il seed è l'hash di una serie di 32 byte di zeri. Per ogni epoca successiva, è l'hash dell'hash del seme precedente. Usando questo seed, un nodo può calcolare una cache pseudo-casuale.
Questa cache è incredibilmente utile perché rende possibile il concetto di "nodi leggeri", di cui abbiamo discusso in precedenza in questo post. Lo scopo dei nodi leggeri è quello di permettere ad alcuni nodi di verificare in modo efficiente una transazione senza dover archiviare l'intero set di dati della blockchain. Un nodo leggero può verificare la validità di una transazione basandosi esclusivamente su questa cache, perché la cache può rigenerare il blocco specifico necessario per la verifica.
Utilizzando la cache, un nodo può generare il "set di dati" del DAG, dove ogni elemento nel set di dati dipende da un piccolo numero di elementi selezionati pseudo-casualmente dalla cache. Per essere un miner, è necessario generare questo set di dati completo; tutti i client e i miner completi archiviano questo set di dati, che cresce in modo lineare con il tempo.
I miner possono quindi prendere porzioni casuali del set di dati e attraverso una funzione matematica riunirli in un “mixHash". Un miner genererà ripetutamente un mixHash finché l'output non sarà inferiore al nonce che si desidera raggiungere. Quando l'output soddisferà questo requisito, il nonce sarà considerato valido e il blocco potrà essere aggiunto alla catena.
Nel complesso, lo scopo del PoW è di dimostrare, in modo crittograficamente sicuro, che una particolare quantità di calcolo è stata impiegata per generare un output (cioè il nonce). Questo perché non esiste un modo migliore per trovare un nonce inferiore alla soglia richiesta se non quello di elencare tutte le possibilità. Gli output dell'applicazione ripetuta della funzione hash hanno una distribuzione uniforme, e quindi possiamo essere certi che, in media, il tempo necessario per trovare tale nonce dipende dalla soglia di difficoltà. Maggiore è la difficoltà, maggiore è il tempo necessario per risolvere il nonce. In questo modo, l'algoritmo PoW dà un senso al concetto di difficoltà, che viene utilizzato per garantire la sicurezza della blockchain.
Cosa intendiamo per sicurezza della blockchain? È semplice: vogliamo creare una blockchain che TUTTI possano ritenere attendibile. Come abbiamo già spiegato in questo post, se esistesse più di una catena, gli utenti perderebbero fiducia, perché non sarebbero in grado di determinare in modo ragionevole quale catena sia quella “valida”. Affinché un gruppo di utenti accetti lo stato sottostante che è memorizzato su una blockchain, abbiamo bisogno di una singola blockchain canonica in cui un gruppo di persone creda.
Questo è esattamente ciò che fa l'algoritmo PoW: garantisce che una particolare blockchain rimanga canonica in futuro, rende incredibilmente difficile per un hacker creare nuovi blocchi che sovrascrivano una determinata parte della cronologia (ad esempio cancellando le transazioni o creando transazioni false) o gestire una biforcazione. Per ottenere la prima convalida del blocco, un hacker dovrebbe sempre risolvere il nonce in modo più veloce di chiunque altro nella rete, in modo tale che la rete ritenga che la sua catena sia la più pesante (in base ai principi del protocollo GHOST di cui abbiamo parlato prima). Questo sarebbe impossibile a meno che l’hacker non abbia più della metà della potenza di mining della rete, uno scenario noto come attacco del 51%.
Oltre a fornire una blockchain sicura, PoW è anche un modo per distribuire ricchezza a coloro che utilizzano le proprie capacità di calcolo per fornire questa sicurezza.
Ricordate che un miner riceve una ricompensa per il mining di un blocco, tra cui:
Per garantire che l'utilizzo del meccanismo di consenso PoW per la distribuzione della sicurezza e della ricchezza sia sostenibile a lungo termine, Ethereum fa il possibile per implementare queste due proprietà:
Nella rete blockchain Bitcoin, un problema che si presenta in relazione alle due proprietà appena descritte è che l'algoritmo PoW è una funzione hash SHA256. La debolezza di questo tipo di funzione è che può essere risolta in modo molto più efficiente utilizzando hardware specializzato, detto anche ASIC.
Per mitigare questo problema, Ethereum ha scelto di richiedere sequenzialmente un uso molto intensivo della memoria per il suo algoritmo PoW (Ethhash). Significa che l'algoritmo è progettato in modo che il calcolo del nonce richieda molta memoria e larghezza di banda. Gli elevati requisiti di memoria rendono difficile l'utilizzo della memoria in parallelo da parte di un computer per scoprire diversi nonce contemporaneamente, e gli elevati requisiti di larghezza di banda rendono difficile scoprire simultaneamente più nonce anche per un computer super veloce. Questo riduce il rischio di centralizzazione e crea condizioni più eque per i nodi che effettuano la verifica.
Una cosa da notare è che Ethereum sta passando da un meccanismo di consenso PoW al cosiddetto "proof-of-stake". Si tratta di un argomento molto impegnativo che speriamo di poter eviscerare in un post futuro.
☺️
Eccoci qui. Siete arrivati fino alla fine. O almeno lo spero.
C'è molto da assimilare in questo post, lo so. Se vi servono più letture per comprendere appieno cosa sta succedendo, è normale. Anche io ho letto lo yellow paper, il white papter di Ethereum e varie parti della base di codice molte volte prima di capire bene quello che stava accadendo.
Spero comunque che abbiate trovato utile questa panoramica. Se avete notato errori, vi invito a scrivermi privatamente o a pubblicare un commento direttamente qui sotto. Leggero tutto, prometto ;)
E ricordate, sono umano anche io (ebbene sì) e commetto errori. Ho scritto questo post a beneficio della comunità, gratuitamente. Quindi vi pregherei di essere costruttivi nel vostro feedback, ed evitare attacchi inutili.
☺️
[1] https://github.com/ethereum/yellowpaper
Founder & CEO of TruStory. I have a passion for understanding things at a fundamental level and sharing it as clearly as possible.