Vai al contenuto

Ethereum: ma come funziona ? [GUIDA]


E’ molto probabile che abbiate sentito parlare di Ethereum, una criptovaluta nata più recentemente rispetto al Bitcoin che sta però avendo un enorme successo e sembra destinata a prenderne il posto. Sia che sappiate, sia che non sappiate bene di cosa si tratti la lettura di articoli su Ethereum (se ne è parlato molto ultimamente, anche sulle copertine di riviste importanti) può confondere o risultare complessa se non si dispone di una base di conoscenza su cosa è e come funziona tecnicamente.

Cos’è dunque Ethereum ? In estrema sostanza possiamo definirlo un database pubblico che registra in modo permanente le transazioni digitali. È importante aggiungere che questo database non necessita di alcuna autorità centrale per essere manutenuto o protetto, ma funziona come un sistema transazionale “implicitamente affidabile” in cui tutti gli individui possono effettuare operazioni peer-to-peer senza preoccuparsi di doversi fidare di terzi o l’uno dell’altro.

Siete ancora confusi ? Proviamo ad analizzare insieme come funzionano gli Ethereum in dettaglio ed a livello tecnico, ma senza addentrarci in formule matematiche complesse o dall’aspetto incomprensibile. Anche se non siete programmatori spero che al termine della lettura di questa breve guida introduttiva avrete acquisito una migliore padronanza della tecnologia, e se alcune parti fossero troppo tecniche… va benissimo lo stesso, non c’è bisogno di capire ogni dettaglio per farsi un’idea del suo funzionamento complessivo !

Definizione di Blockchain

Una blockchain è una “macchina transazionale singleton, crittograficamente sicura, a stato condiviso”. Complesso, vero ? Vediamo meglio che significa:

  • “Macchina transazionale singleton”: C’è, per paradigma di costruzione, un’unica istanza canonica della macchina responsabile di tutte le transazioni che vengono create nel sistema. In altre parole c’è un’unica verità globale a cui tutti credono.
  • “Crittograficamente sicura”: La creazione di moneta digitale è garantita da complessi algoritmi matematici estremamente difficili da violare che rendono quasi impossibile imbrogliare il sistema (per, ad esempio, creare transazioni false, cancellare transazioni, ecc.).
  • “A stato condiviso”: lo stato memorizzato in questa macchina è condiviso ed accessibile da tutti.

Vediamo come gli Ethereum implementano questo paradigma blockchain.

Il paradigma blockchain degli Ethereum 

La blockchain Ethereum è essenzialmente una macchina a stati transazionale. Si parla di macchina a stati  quando ci si riferisce ad una macchina che, sulla base di una serie di dati di input, modifica il suo stato interno.

La macchina a stati di Ethereum  inizia con uno “stato di genesi” analogo ad una lista vuota prima che siano state effettuate transazioni sulla rete. Quando le varie transazioni vengono eseguite, questo stato di genesi passa di volta in volta ad uno stato successivo. In qualsiasi momento lo stato finale rappresenta lo stato complessivo di Ethereum.

Lo stato complessivo di Ethereum contiene milioni di transazioni. Tali transazioni sono raggruppate in “blocchi”. Ogni blocco contiene una serie di transazioni ed è concatenato al blocco precedente.

Per causare una transizione da uno stato all’altro una transazione deve essere valida. Per essere considerata valida una transazione deve essere sottoposta ad un processo di convalida noto come “mining”, termine con cui si definisce il lavoro di un gruppo di nodi (cioè di computer) che spende le proprie risorse di calcolo per creare un blocco di transazioni valide.

Qualsiasi nodo della rete che si dichiari “miner” (minatore) può tentare di creare e convalidare un blocco. Molti miners in tutto il mondo cercano di creare e convalidare blocchi contemporaneamente e ciascuno di loro fornisce una “prova matematica” quando presenta un blocco da aggiungere alla blockchain. Questa “prova” agisce come una garanzia: se la prova esiste, il blocco deve essere valido.

Per aggiungere un blocco alla blockchain principale un miner deve fornire la “prova” più velocemente di qualsiasi altro miner concorrente. Il processo di convalida di ogni blocco da parte di un miner attraverso la produzione della relativa “prova matematica” è noto come una “proof of work” (prova di lavoro).

Un miner che convalida un nuovo blocco viene ricompensato con una certa quantità di valore per il suo lavoro. Ma che cos’è questo valore? La blockchain Ethereum utilizza un proprio gettone digitale chiamato “Ether”. Ogni volta che un miner fornisce la “prova” di un blocco viene premiato con la generazione e l’assegnazione di nuovi gettoni Ether.

Ci si potrebbe domandare: ma quali garanzie ci sono che tutti utilizzino la stessa blockchain ? Come possiamo essere sicuri che non ci sia un sottogruppo di miners che decida di creare la propria blockchain ?

In precedenza abbiamo definito una blockchain come una “macchina transazionale singleton a stato condiviso”. Applicando questa definizione ne deriva che il corretto “stato attuale” è un’unica verità globale che tutti devono accettare. Avere più stati (o catene) distruggerebbe l’intero sistema perché sarebbe impossibile accordarsi su quale stato sia quello corretto. Se le catene dovessero divergere si potrebbero possedere 10 monete su una catena, 20 su un’altra, e 40 su un’altra. In questo scenario non ci sarebbe modo di determinare quale catena sia “la più valida”.

Ogni volta che vengono generati percorsi multipli si verifica una “fork” (forchetta). Solitamente si vogliono evitare le fork perché disturbano il sistema e costringono a scegliere in quale catena “credere”.

Per determinare quale percorso è più valido e prevenire la creazione di più catene Ethereum utilizza un meccanismo chiamato “protocollo GHOST” (acronimo dall’inglese “Greedy Heaviest Observed Subtree”, cioè, in sintesi, “il sottoalbero osservato più pesante”).

In termini semplici il protocollo GHOST ci dice che dobbiamo scegliere il percorso che ha fatto più elaborazioni su se stesso. Un modo per determinarlo  è quello di utilizzare il numero d’ordine del blocco più recente (il “blocco foglia”), che rappresenta il numero totale di blocchi nel percorso corrente (senza contare il blocco di genesi). Più alto è il numero d’ordine del blocco, più lungo è il percorso e maggiore è lo “sforzo minerario” che deve essere stato fatto per arrivare alla foglia. L’utilizzo di questo percorso logico permette a tutti di concordare sulla versione canonica dello stato attuale.

Dopo questa panoramica di che cosa è una blockchain addentriamoci nei componenti principali di cui è composto il sistema Ethereum:

  • account
  • stati di un account
  • carburante e tasse (“gas and fees”)
  • transazioni
  • blocchi
  • esecuzione delle transazioni
  • mining
  • prova di lavoro (“proof of work”)

Una nota prima di proseguire: quando d’ora in poi parleremo “di hash di un valore X” ci riferiremo al hash KECCAK-256 utilizzato da Ethereum.

Account

Lo “stato condiviso” globale di Ethereum è composto da molti piccoli oggetti, gli “account”, che sono in grado di interagire l’uno con l’altro attraverso un framework per lo scambio di messaggi. Ogni account ha uno stato ad esso associato ed un indirizzo di 20 byte. Un indirizzo in Ethereum è un identificatore a 160 bit che viene utilizzato per identificare ogni account.

Esistono due tipi di account:

  • Account esterni (externally owned accounts), che sono controllati da chiavi private e non hanno alcun codice associato ad essi.
  • Account dei contratti (contract accounts), che sono controllati dal loro codice contratto ed hanno un codice associato.

Account esterni vs. account dei contratti:

È importante sottolineare una differenza fondamentale tra gli account esterni e gli account dei contratti. Un account esterno può inviare messaggi ad altri account esterni o ad altri account dei contratti creando e firmando una transazione utilizzando 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 di contratto attiva il codice dell’account di contratto permettendogli di eseguire varie azioni (ad esempio trasferire tokens, scrivere su un archivio interno, creare tokens nuovi, eseguire calcoli, creare nuovi contratti, ecc.).

A differenza degli account esterni gli account di contratto non possono avviare nuove transazioni autonomamente. Al contrario, gli account di contratto possono essere utilizzati solo per effettuare transazioni in risposta ad altre transazioni ricevute (da un account esterno o da un altro account di contratto). Approfondiremo le chiamate da account di contratto ad account di contratto più avanti nella sezione “Transazioni e messaggi”.

Pertanto qualsiasi transazione che si verifichi sulla blockchain Ethereum è sempre attivata da transazioni provenienti da account esterni.

Stati di un account

Lo stato di un account è sempre composto da quattro componenti, presenti indipendentemente dal tipo di account:

nonce: Se l’account è esterno questo numero rappresenta il numero di transazioni inviate dall’indirizzo dell’account. Se l’account è di contratto il valore nonce è pari al numero di contratti creati dall’account.

balance: Il numero di Wei di proprietà di questo indirizzo. Ci sono 1e+18 Wei per Ether.

storageRoot: Un hash del nodo radice di un albero di Merkle Patricia  (ne parleremo più avanti). Questo albero codifica l’hash del contenuto archiviato in questo account ed è vuoto per impostazione predefinita.

codeHash: L’hash del codice EVM (Ethereum Virtual Machine – anche di questo parleremo più avanti) di questo account. Per gli account di contratto questo è il codice che viene hashato e memorizzato come codeHash. Per i conti di proprietà esterna, il campo codeHash è l’hash della stringa vuota.

Stato globale

Quindi ora sappiamo che lo stato globale di Ethereum consiste in una mappatura tra gli indirizzi degli account ed i loro stati. Questa mappatura è memorizzata in una struttura dati nota come albero di Merkle Patricia.

Un albero di Merkle (anche chiamato “Merkle trie”) è un tipo di albero binario composto da un insieme di nodi con:

  • un gran numero di nodi foglia in fondo all’albero che contengono i dati
  • un insieme di nodi intermedi, in cui ogni nodo è l’hash dei suoi due nodi figli
  • un singolo nodo radice, anch’esso formato dall’hash dei suoi due nodi figli, che rappresenta la sommità dell’albero

I dati in fondo all’albero vengono generati dividendo i dati che vogliamo memorizzare in chunks, quindi dividendo i chunks in buckets, e quindi prendendo l’hash di ogni bucket e ripetendo lo stesso processo fino a quando il numero totale di hash rimanenti diventa solo uno: l’hash radice.

Questo albero deve avere una chiave per ogni valore memorizzato al suo interno. Partendo dal nodo radice dell’albero la chiave dovrebbe indicare quale nodo figlio seguire per raggiungere il valore corrispondente che viene memorizzato nei nodi foglia. Nel caso di Ethereum la mappatura chiave/valore per l’albero di stato è tra gli indirizzi ed i conti associati, incluso il saldo, la nonce, il codeHash e lo storageRoot per ogni conto (dove lo storageRoot è esso stesso un albero).

La stessa struttura viene utilizzata anche per memorizzare transazioni e ricevute. Più specificamente ogni blocco ha un “header” che memorizza l’hash del nodo radice di tre diverse strutture Merkle:

  1. Albero di stato
  2. Albero delle transazioni
  3. Albero delle ricevute

La capacità di memorizzare tutte queste informazioni in modo efficiente negli alberi di Merkle è incredibilmente utile in Ethereum per quelli che chiamiamo “client light” o “nodi light” (leggeri). Ricordate che una blockchain è manutenuta da un gruppo di nodi. In generale esistono due tipi di nodi: i nodi completi e i nodi leggeri.

Un nodo completo sincronizza la blockchain scaricando l’intera catena, dal blocco genesi al blocco testata corrente, ed eseguendo tutte le operazioni contenute al suo interno. In genere i miners immagazzinano nodi completi perché sono tenuti a farlo per il processo di mining. E’ inoltre possibile scaricare un nodo completo senza eseguire ogni operazione. Indipendentemente da ciò ogni nodo completo contiene l’intera blockchain.

Ma a meno che un nodo non debba eseguire ogni transazione o interrogare dati storici non ha davvero bisogno di memorizzare l’intera blockchain. È qui che entra in gioco il concetto di nodo light. Invece di scaricare e memorizzare l’intera catena ed eseguire tutte le transazioni i nodi light scaricano solo la catena di intestazioni, dal blocco di genesi alla foglia corrente, senza eseguire alcuna transazione o recuperare alcuno stato associato. Poiché i nodi light hanno accesso alle intestazioni dei blocchi, che contengono gli hash di tre tentativi, possono ancora facilmente generare e ricevere risposte verificabili su transazioni, eventi, saldi, ecc.

Il motivo per cui questo meccanismo funziona è che le hash in un albero di Merkle si propagano verso l’alto – se un utente malintenzionato tentasse di scambiare una transazione falsa nella sua parte inferiore questo cambiamento causerebbe un cambiamento nell’hash del nodo superiore, che cambierà l’hash del nodo al di sopra di esso, e così via, fino a quando alla fine non cambia la radice dell’albero.

Qualsiasi nodo che desideri verificare un dato può utilizzare una cosiddetta “prova Merkle”. La prova Merkle è costituita da:

  1. Un dato da verificare e il suo hash
  2. L’hash della radice dell’albero
  3. Il “branch” (il “ramo”, tutta la catena di hash risalendo lungo il percorso, dal dato alla radice)

Chiunque legga la prova può verificare che l’hashing per quel ramo sia costante fino alla radice dell’albero, e quindi che il ramo dato sia effettivamente in quella posizione dell’albero.

In sintesi il vantaggio di utilizzare un albero di Merkle Patricia è che il nodo radice di questa struttura dipende crittograficamente dai dati memorizzati nell’albero e quindi l’hash del nodo radice può essere utilizzato come identità sicura per questi dati. Poiché l’intestazione del blocco include l’hash radice degli alberi di stato, delle transazioni e delle ricevute, qualsiasi nodo può validare una piccola parte dello stato di Ethereum senza bisogno di memorizzarene l’intero stato, che può essere potenzialmente di dimensioni illimitate.

Carburante e tasse (“gas and fees”)

Un concetto molto importante in Ethereum è quello di “tassa”. Ogni calcolo che avviene come risultato di una transazione sulla rete Ethereum è a pagamento, nulla è gratuito! Questa tassa è pagata in una moneta chiamata “gas” (carburante).

Il “gas” è l’unità di misura utilizzata per le tariffe richieste per l’esecuzione di un particolare calcolo. Il prezzo del gas equivale alla quantità di Ether che si è disposti a spendere per ogni unità di gas, ed è misurato in “gwei”. “Wei” è l’unità più piccola di Ether dove 1⁰¹⁸ Wei rappresentano 1 Ether. Un gwei equivale ad 1.000.000.000 di Wei.

Per ogni transazione il mittente stabilisce un limite di gas ed il prezzo per i gas. Il prodotto del prezzo del gas e del limite dei gas rappresenta la quantità massima di Wei che il mittente è disposto a pagare per l’esecuzione della transazione.

Ad esempio assumiamo che il mittente fissi il limite dei gas a 50.000 e il prezzo di gas a 20 gwei. Ciò implica che il mittente è disposto a spendere al massimo 50.000 x 20 gwei = 1.000.000.000.000.000 Wei = 0,001 Ether per eseguire la specifica transazione.

Ricordate che il limite del gas rappresenta il massimo per il quale il mittente è disposto a spendere denaro. Se ha abbastanza Ether nel proprio saldo per coprire questo importo la transazione può essere eseguita. Il mittente è poi rimborsato per l’importo di gas non utilizzati al termine della transazione, scambiati al tasso di cambio originale.

Se il mittente non fornissse il gas necessario per eseguire una transazione quest’ultima si esaurirebbe e non sarebbe considerata valida. In questo caso l’elaborazione della transazione si interromperebbe e qualsiasi cambiamento di stato che si fosse verificato verrebbe ripristinato in modo da tornare allo stato di Ethereum preceddente alla transazione. Inoltre verrebbe registrata una transazione non riuscita per mostrare quale operazione è stata tentata e dove è fallita. E poiché la macchina ha già speso risorse per eseguire i calcoli prima di esaurire il gas nessun importo viene rimborsato al mittente.

E dove vanno esattamente a finire questi soldi per il gas ? Tutto il denaro speso dal mittente per il gas viene inviato all’indirizzo “beneficiario”, che di solito è l’indirizzo del miner. Poiché i miner stanno spendendo le loro risorse per eseguire i calcoli e convalidare le transazioni, ricevono il fee come ricompensa.

In genere più alto è il prezzo del gas che un mittente è disposto a pagare maggiore sarà il valore che il miner ricaverà dalla transazione. In questo modo i miner sono liberi di scegliere quali transazioni convalidare o quali ignorare. Per orientare i mittenti verso un prezzo del gas da loro ritenuto accettabile i vari miner hanno la possibilità di pubblicizzare il prezzo minimo del gas per il quale son disposti ad effettuare transazioni.

Costi per lo storage

Il gas viene utilizzato non solo per remunerare le fasi di calcolo, ma anche per l’utilizzo dello storage. La tariffa totale per la memorizzazione è proporzionale al più piccolo multiplo utilizzato di 32 byte.

I costi dello storage hanno alcune peculiarità. Ad esempio, poiché l’aumento dello storage aumenta le dimensioni del database complessivo dello stato di Ethereum su tutti i nodi, c’è un incentivo a mantenere la quantità di dati memorizzati di piccole dimensioni. Per questo motivo se una transazione contiene un’operazione che cancella un dato nello storage la tassa per l’esecuzione di tale operazione è annullata e viene anche dato un rimborso per aver liberato spazio di storage.

Qual è lo scopo delle tariffe (“fees”) ?

Un aspetto importante del funzionamento di Ethereum è che ogni singola operazione eseguita dalla rete viene eseguita simultaneamente da ogni nodo completo. Ma i passaggi di calcolo sulla macchina virtuale Ethereum sono molto costosi. Pertanto gli “smart contracts” di Ethereum vengono utilizzati al meglio per attività elementari, come l’esecuzione di una semplice business logic, o la verifica di firme digitali ed altri oggetti crittografici, piuttosto che per usi più complessi come l’archiviazione di file, la posta elettronica o l’apprendimento automatico, operazioni che possono mettere a dura prova la rete. L’imposizione di tariffe (“fees”) impedisce agli utenti di sovraccaricarla eccessivamente.

Ethereum è un linguaggio Turing completo (in estrema sintesi una macchina Turing è una macchina in grado di simulare qualsiasi algoritmo). Ciò consente anche di creare dei loop e quindi rende Ethereum sensibile al problema di evitare programmi che vengano eseguiti all’infinito. In assenza di tariffe un operatore malintenzionato potrebbe facilmente cercare di perturbare la rete eseguendo un ciclo infinito all’interno di un’operazione senza alcuna ripercussione per se. Le tariffe proteggono quindi la rete anche da attacchi deliberati.

Si potrebbe pensare: “Perché dobbiamo pagare anche per lo storage ?” Ma proprio come il calcolo, lo storage su Ethereum è un costo di cui l’intera rete deve farsi carico.

Transazioni e messaggi

Abbiamo detto in precedenza che Ethereum è una macchina a stati basata su transazioni. In altre parole le transazioni che avvengono tra diversi conti sono gli eventi che fanno transitare lo stato globale di Ethereum da uno stato all’altro.

In parole semplici una transazione è un insieme di istruzioni firmate crittograficamente che vengono generate da un conto di proprietà esterna, vengono serializzate e quindi inviate alla blockchain.

Esistono due tipi di transazioni: le message call e le contract creation (cioè le transazioni che creano nuovi contratti Ethereum). Tutte le transazioni contengono le seguenti componenti, indipendentemente dalla loro tipologia:

  • nonce: il conteggio del numero di transazioni inviate dal mittente
  • gasPrice: il numero di Wei che il mittente è disposto a pagare per unità di gas necessaria per eseguire la transazione
  • gasLimit: il quantitativo massimo di gas che il mittente è disposto a pagare per l’esecuzione dell’operazione, tale importo è fissato e versato anticipatamente prima che venga effettuato qualsiasi calcolo
  • to: l’indirizzo del destinatario (in un’operazione di creazione di un contratto l’indirizzo del conto del contratto non esiste ancora e quindi viene utilizzato un valore vuoto)
  • value: la quantità di Wei da trasferire dal mittente al destinatario (in un’operazione di creazione di un contratto questo valore è il saldo iniziale all’interno del nuovo contratto creato)
  • v, r, s: utilizzati per generare la firma che identifica il mittente della transazione
  • init (esiste solo per transazioni di contract creation): è un frammento di codice EVM che viene utilizzato per inizializzare il nuovo account e viene eseguito una sola volta e poi scartato. Quando init viene eseguito per la prima volta restituisce il corpo del codice del conto che è il pezzo di codice che resterà permanentemente associato al conto del contratto
  • data (campo opzionale presente solo nelle message call): sono i dati di ingresso (parametri) della message call. Ad esempio se uno smart contract fornisse il servizio di registrazione di un dominio una chiamata a tale contratto potrebbe richiedere campi di input quali il dominio ed il relativo indirizzo IP

Parlando di “account” abbiamo compreso che le transazioni – sia le message call che le contract creation – sono sempre avviate da conti di proprietà esterna ed inviate alla blockchain, possiamo dire cioè che le transazioni sono il ponte tra il mondo esterno e lo stato interno di Ethereum.

Ma questo non significa che i contratti non possono interagire con altri contratti. I contratti che esistono nell’ambito dello scope complessivo di Ethereum possono interagire con altri contratti nello stesso ambito. Il modo in cui lo fanno è tramite “messages” o “internal transactions” verso altri contratti. Possiamo pensare che i “messages” o “internal transactions” siano simili alle transazioni, con la differenza che NON sono generati da conti di proprietà esterna. Sono invece oggetti virtuali generati da contratti che, a differenza delle transazioni, non sono serializzati ed esistono solo nell’ambiente di esecuzione di Ethereum.

Quando un contratto invia una transazione interna ad un altro contratto, viene eseguito il codice associato al conto del contratto destinatario.

Una cosa importante da notare è che i “messages” e le “internal transactions” non contengono un gasLimit. Questo perché il gasLimit è determinato dal creatore esterno della transazione originale (cioè da un conto di proprietà esterna). Il gasLimit fissato dal conto di proprietà esterna deve essere quindi sufficientemente elevato da consentire l’esecuzione dell’operazione complessiva, comprese eventuali sotto-esecuzioni che si verificano a seguito dell’operazione stessa, come “messages” e “internal transactions” tra contratti. Se nella catena complessiva di transazioni e messaggi una particolare esecuzione di un message si esaurisce, allora l’esecuzione di quel message verrà ripristinata allo stato iniziale, insieme a tutti i message successivi attivati dalla sua esecuzione.

Blocchi

Tutte le operazioni sono raggruppate in “blocchi”. Una catena di blocchi contiene una serie di tali blocchi concatenati tra loro. Un blocco Ethereum è composto da:

  • l’intestazione (header) del blocco
  • informazioni sull’insieme delle operazioni incluse in tale blocco
  • una serie di altri header per gli “ommer” del blocco corrente

E cosa sono gli “ommer” ? Un ommer è un blocco il cui genitore è uguale al genitore del blocco corrente. Ma vediamo come gli ommer vengono utilizzati e perché un blocco contiene le intestazioni degli ommer.

A causa del modo in cui Ethereum è costruito i tempi di elaborazione di un blocco sono molto più bassi (~ 15 secondi) rispetto a quelli di altre blockchain come i Bitcoin (~ 10 minuti). Ciò consente si un’elaborazione più rapida delle transazioni, ma uno degli aspetti negativi di tempi più brevi di calcolo è che più miners possono risolvere contemporaneamente i blocchi. Questi blocchi concorrenti sono denominati anche “blocchi orfani” (“orphaned blocks”, ossia blocchi che non entrano nella catena principale).

Lo scopo degli ommers è quello di contribuire a ricompensare i miner per l’inclusione di questi blocchi orfani. Gli ommer che i miner includono devono essere “validi”, vale a dire entro la sesta generazione dal blocco attuale. Dopo sei figli i blocchi orfani non possono più essere referenziati (perché includere transazioni più vecchie complicherebbe ulteriormente le cose). I blocchi ommer ricevono una ricompensa inferiore a quella di un blocco completo, ma  danno un incentivo ad i miner ad includere questi blocchi orfani e ricevere la ricompensa.

Intestazione (header) di un blocco

Abbiamo detto in precedenza che ogni blocco ha una “intestazione”, ma che cosa è esattamente ? Un “block header” è una porzione del blocco che contiene:

  • parentHash: un hash dell’intestazione del blocco padre (questo è ciò che rende il blocco parte di una “catena”)
  • ommershash: un hash della lista degli ommer del blocco corrente
  • beneficiary: l’indirizzo del conto che riceverà le fees per il mining di questo blocco
  • stateRoot: l’hash del nodo radice del trie di stato (ricordate che il trie di stato è memorizzato nell’intestazione e rende facile per i client leggeri verificare qualsiasi cosa sullo stato complessivo)
  • transactionsRoot: l’hash del nodo radice del trie che contiene tutte le transazioni elencate in questo blocco
  • receiptsRoot: l’hash del nodo radice del trie che contiene le ricevute di tutte le transazioni elencate in questo blocco
  • logsBloom: un filtro Bloom (struttura dati) che consiste in informazioni di log
  • difficulty: il livello di complessità del blocco
  • number: il numero d’ordine del blocco corrente (il blocco di genesi ha un numero di blocco pari a zero; il numero di blocco aumenta di 1 per ogni blocco successivo)
  • gasLimit: l’attuale limite di gas per blocco
  • gasUsed: la somma del gas totale utilizzato dalle transazioni di cui al presente blocco
  • timestamp: il timestamp unix della creazione del blocco
  • extraData: dati extra associati al blocco
  • mixHash: un hash che, se combinato con la nonce, dimostra che questo blocco ha effettuato calcoli sufficienti
  • nonce: un hash che, se combinato con la mixHash, dimostra che questo blocco ha effettuato calcoli sufficienti

Notare che l’header di ogni blocco contiene tre strutture trie per:

  • stato (stateRoot)
  • transazioni (transactionsRoot)
  • ricevute (receiptsRoot)

E queste tre strutture non sono altro che tre alberi di Merkle Patricia di cui abbiamo già parlato.

Log

Ethereum consente di tenere traccia di varie transazioni e messaggi. Un contratto può generare esplicitamente un registro (log) definendo gli “eventi” che vuole registrare.

Una voce di registro contiene:

  • l’indirizzo dell’account del logger
  • una serie di argomenti che rappresentano i vari eventi che l’operazione ha consentito di realizzare
  • qualsiasi dato associato a tali eventi

I log vengono memorizzati in un filtro Bloom che memorizza in modo efficiente dati infiniti.

Transaction receipt

I log memorizzati nell’header provengono dalle informazioni contenute nel log della transazione. Proprio come si riceve una ricevuta quando si acquista qualcosa in un negozio Ethereum genera una ricevuta per ogni transazione che include:

  • il numero del blocco
  • l’hash del blocco
  • l’hash della transazione
  • il gas utilizzato dalla presente transazione
  • gas complessivo utilizzato nel blocco attuale dopo l’esecuzione della transazione in corso
  • log creati durante l’esecuzione dell’operazione corrente
  • …e così via

Block difficulty

La “difficoltà” di un blocco è usata per controllare la coerenza del tempo necessario per convalidare i diversi blocchi. Il blocco genesi ha una difficoltà di 131.072 e una formula speciale viene utilizzata per calcolare la difficoltà di ogni blocco successivo. Se un determinato blocco viene convalidato più rapidamente del blocco precedente il protocollo Ethereum aumenta la difficoltà di quel blocco.

La difficoltà di un blocco influisce sulla nonce, che è l’hash che deve essere calcolato quando si estrae un blocco utilizzando l’algoritmo di proof-of-work.

Il rapporto tra la difficoltà di un blocco e la nonce è matematicamente formalizzato come:

dove Hd è la difficoltà.

L’unico modo per trovare una nonce che soddisfi una specifica soglia di difficoltà è quello di utilizzare l’algoritmo di proof-of-work ed enumerare tutte le possibilità. Il tempo atteso per trovare una soluzione è proporzionale alla difficoltà, maggiore è la difficoltà più difficile diventa trovare la nonce e, quindi, è più difficile convalidare il blocco, il che a sua volta aumenta il tempo necessario per convalidare un nuovo blocco. Quindi regolando la difficoltà di un blocco il protocollo può regolare il tempo necessario per convalidarlo.

Se al contrario il tempo di validazione di un blocco aumentasse troppo il protocollo è in grado di ridurre la difficoltà per regolare automaticamente la velocità di creazione di un blocco e mantenerla pari, in media, a 15 secondi.

Esecuzione di una transazione

Siamo arrivati ad uno degli aspetti più complessi del protocollo Ethereum: l’esecuzione di una transazione. Si supponga di inviare una transazione ad una blockchain Ethereum, cosa succede al suo stato globale per includerla ?

In primo luogo tutte le transazioni devono soddisfare una serie iniziale di requisiti per poter essere eseguite:

  • La transazione deve essere correttamente formattata in RLP . “RLP” sta per “Recursive Length Prefix” ed è un formato dati utilizzato per codificare array nidificati di dati binari, ed è il formato che Ethereum utilizza per serializzare gli oggetti.
  • Una firma valida
  • Una nonce di transazione valida . Si ricorda che la nonce di un conto è il conteggio delle transazioni inviate da quel conto. Per essere valida, la nonve di una transazione deve essere uguale alla nonce del conto del mittente.
  • Il limite di gas dell’operazione deve essere pari o superiore al gas intrinseco utilizzato dall’operazione stessa. Il gas intrinseco include:
  1. un costo predefinito di 21.000 gas per l’esecuzione dell’operazione
  2. una tariffa in gas per i dati inviati con la transazione (4 gas per ogni byte di dati o codice pari a zero e 68 gas per ogni byte di dati o codice diverso da zero)
  3. altri 32.000 gas se l’operazione è un’operazione di creazione di contratto

  • Il saldo del conto del mittente deve avere Ether sufficiente per coprire i costi “anticipati” di gas che deve pagare. Il calcolo del costo iniziale del gas è semplice: In primo luogo il limite del gas fissato nell’operazione viene moltiplicato per il prezzo del gas per determinare il costo massimo. Questo costo massimo viene quindi aggiunto al valore totale che viene trasferito dal mittente al destinatario.

Se la transazione soddisfa tutti i requisiti di validità di cui sopra si passa alla fase successiva.

In primo luogo si deduce il costo iniziale di esecuzione dal saldo del mittente  e si aumenta il nonce del conto del mittente di 1 per tener conto della transazione corrente. A questo punto possiamo calcolare il gas rimanente come limite totale di gas per la transazione meno il gas intrinseco utilizzato.

Successivamente l’operazione inizia ad essere eseguita. Durante l’esecuzione di una transazione Ethereum tiene traccia del “substate”,.un modo per registrare le informazioni generate durante la transazione e che saranno necessarie immediatamente dopo il suo completamento. In particolare contiene:

  • Self-destruct set: una serie (eventuale) di conti che saranno scartati dopo il completamento dell’operazione
  • Log series: punti di controllo archiviati ed indicizzabili in merito all’esecuzione del codice della macchina virtuale
  • Refund balance: l’importo da rimborsare sul conto del mittente dopo la transazione. Ricorderete che abbiamo detto che lo storage in Ethereum costa denaro e che un mittente viene rimborsato per la riduzione del suo utilizzo.  Ethereum tiene traccia di ciò utilizzando un contatore di rimborso che è inizialmente valorizzato a zero ed aumenta ogni volta che il contratto cancella un dato dallo storage.

Successivamente vengono elaborati i vari calcoli richiesti dalla transazione.

Una volta che tutti i passaggi richiesti dalla transazione sono stati elaborati, supponendo che non siamo uno stato non valido, lo stato viene completato con la determinazione della quantità di gas inutilizzato da rimborsare al mittente. Oltre al gas inutilizzato al mittente viene rimborsato anche un certo importo del “refund balance” di cui sopra.

Una volta che il mittente è stato rimborsato:

  • gli Ether per il gas vengono dati al miner
  • il gas utilizzato dalla transazione viene aggiunto al contatore del gas di blocco (che tiene traccia del gas totale utilizzato da tutte le transazioni nel blocco ed è utile per la convalida di un blocco)
  • tutti gli account nel Self-destruct set (se presente) vengono cancellati

Al termine ci ritroviamo con il nuovo stato ed una serie di log creati dalla transazione.

Ora che abbiamo trattato le basi dell’esecuzione delle transazioni esaminiamo alcune delle differenze tra le contract creation e le message call.

Contract creation

Ricordate che in Ethereum ci sono due tipi di account: account di contratto e account di proprietà esterna. Quando diciamo che una transazione “crea un contratto” intendiamo che lo scopo della transazione è quello di creare un nuovo account di contratto.

Per creare un nuovo account di contratto va prima dichiarato l’indirizzo del nuovo account con una formula speciale. Poi il nuovo account va inizializzato:

  • Azzerando il nonce
  • Se il mittente ha inviato una quantità di Ether come valore con la transazione, impostando il saldo dell’account a tale valore
  • Deducendo dal saldo del mittente il valore aggiunto al saldo di questo nuovo account
  • Impostando lo storage come vuoto
  • Impostando il codeHash del contratto come l’hash di una stringa vuota

Una volta inizializzato l’account possiamo effettivamente crearlo utilizzando il codice init inviato con la transazione. Ciò che accade durante l’esecuzione dell’init può variare  a seconda del costruttore del contratto, può ad esempio aggiornare lo storage dell’account, creare altri contract account, effettuare altre message calls, ecc.

Quando il codice per inizializzare un contratto viene eseguito utilizza del gas, ma l’operazione non consente di utilizzare più gas di quello rimanente. Se lo fa, l’esecuzione genererà un’eccezione di out-of-gas (OOG) e terminerà. Se una transazione termina a causa di un’eccezione di out-of-gas lo stato viene ripristinato al punto immediatamente precedente la transazione stessa ed il mittente non riceve alcun rimborso per il gas speso prima dell’esaurimento. Tuttavia, se il mittente ha inviato un valore in Ether con la transazione, tale valore sarà rimborsato anche se la creazione del contratto fallisce.

Se il codice di inizializzazione viene eseguito con successo viene pagato il costo per la creazione del contratto. Questo rappresenta un costo di storage ed è proporzionale alla dimensione del codice del contratto creato. Se non c’è abbastanza gas residuo per pagare questo costo finale, la transazione dichiara di nuovo un’eccezione di out-of-gas e si interrompe.

Se tutto va bene e l’operazione termina senza eccezioni allora il gas rimasto inutilizzato viene restituito al mittente della transazione ed Ethereum cambia il suo stato complessivo.

Message calls

L’esecuzione di una message call è simile a quella di una contract creation, ma con alcune differenze.

L’esecuzione di una message call non include alcun codice init in quanto non vengono creati nuovi account. Tuttavia può contenere dei dati di input, se questi sono stati forniti dal mittente della transazione. Una volta eseguite le message call producono anche un componente aggiuntivo contenente i dati di uscita, componente che viene utilizzato se un’operazione successiva ha bisogno di questi dati.

Come nel caso delle contract creation, se l’esecuzione di una message call viene interrotta perché il gas è esaurito o perché la transazione non è valida (ad esempio per overflow della pila, per destinazione non valida o istruzioni non valide), il gas utilizzato non viene rimborsato al chiamante originale, tutto il gas rimanente inutilizzato viene consumato e lo stato viene ripristinato al punto immediatamente precedente all’operazione.

Fino all’ultimo aggiornamento di Ethereum non c’era modo di fermare o ripristinare l’esecuzione di una transazione senza che il sistema consumasse tutto il gas fornito. Per esempio, diciamo di aver creato un contratto che ha generato un errore quando un chiamante non era autorizzato ad eseguire una transazione. Nelle versioni precedenti di Ethereum  il gas rimanente sarebbe stato consumato, e nulla sarebbe stato rimborsato al mittente. Ma l’aggiornamento Byzantium  include un nuovo codice “revert” che permette a un contratto di interrompere l’esecuzione, ripristinare i cambiamenti di stato senza consumare il gas rimanente e restituire una causale per il fallimento della transazione. Se una transazione termina a causa di un “revert”, il gas inutilizzato viene ora restituito al mittente.

Modello di esecuzione delle transazioni

Finora abbiamo analizzato la sequenza di passaggi che devono essere effettuati per eseguire una transazione dall’inizio alla fine. Ora vedremo come una transazione viene effettivamente eseguita all’interno della Ethereum Virtual Machine (EVM).

La EVM è una macchina virtuale completa di Turing, come definito in precedenza, con l’unica differenza che una tipica macchina completa di Turing non ha i limiti di esecuzione legati al gas che ha una EVM. Pertanto, in questo caso, la quantità totale di calcolo che può essere eseguito è intrinsecamente limitata dalla quantità di gas fornito.

Inoltre la EVM ha un’architettura logica di esecuzione a pila, è cioè una macchina che utilizza una pila “last-in, first-out” per memorizzare valori temporanei.

La dimensione di ogni elemento dello stack di una EVM è di 256 bit e lo stack ha una dimensione massima di 1024.

la memoria di una EVM, in cui memorizza gli elementi come array di byte, è volatile (non permanente).

Una EVM è dotata anche di storage. A differenza della memoria, lo storage  non è volatile e viene mantenuto come parte dello stato del sistema. Inoltre una EVM memorizza il codice del proprio programma separatamente, in una ROM virtuale accessibile solo tramite istruzioni speciali, differenziandosi così da una tipica architettura di von Neumann in cui il codice di programma viene memorizzato o in memoria o nello storage.

Una EVM ha anche un proprio linguaggio di programmazione, l’EVM bytecode, che è il linguaggio in cui vengono compilati gli smart contracts scritti tipicamente con linguaggi di programmazione di livello superiore, come Solidity.

Parliamo ora dell’effettiva esecuzione delle transazioni.

Prima di eseguire un particolare calcolo, il processore si assicura che le seguenti informazioni siano disponibili e valide:

  • Stato del sistema
  • Gas rimanente per il calcolo
  • Indirizzo dell’account proprietario del codice che si sta eseguendo
  • Indirizzo del mittente dell’operazione che ha dato origine a tale esecuzione
  • Indirizzo dell’account che ha causato l’esecuzione del codice (potrebbe essere diverso dal mittente originale)
  • Prezzo del gas dell’operazione che ha dato origine a tale esecuzione
  • Dati di ingresso per questa esecuzione
  • Valore (in Wei) passato a questo conto come parte dell’esecuzione corrente
  • Codice macchina da eseguire
  • Intestazione del blocco corrente
  • Profondità della message call o dello stack della contract creation

All’inizio dell’esecuzione la memoria e lo stack sono vuoti e il contatore del programma è a zero.

PC: 0 STACK: [] MEM: [], STORAGE: {}

La EVM esegue quindi la transazione in modo ricorsivo calcolando lo stato del sistema e quello della macchina per ogni loop. Lo stato del sistema è semplicemente lo stato globale di Ethereum. Lo stato della macchina comprende:

  • gas disponibile
  • contatore di programma
  • Contenuto della memoria
  • numero di word attivi in memoria
  • contenuto della pila

Gli elementi della pila vengono aggiunti o rimossi dalla parte più a sinistra della serie.

Ad ogni ciclo la corretta quantità di gas viene detratta dal gas residuo ed il contatore del programma aumenta.

Alla fine di ogni ciclo ci sono tre possibilità:

  1. La macchina raggiunge uno stato di eccezione (ad es. gas insufficiente, istruzioni non valide, elementi della pila insufficienti, elementi della pila che traboccherebbero oltre il limite di 1024, destinazione JUMP/JUMPI non valida, ecc.) e quindi deve essere arrestata con le eventuali modifiche scartate.
  2. La sequenza continua nel ciclo successivo
  3. La macchina si arresta in modo controllato (fine del processo di esecuzione)

Supponendo che l’esecuzione non raggiunga uno stato di eccezione e raggiunga un arresto “controllato” o normale la macchina genera lo stato risultante, il gas rimanente in seguito a questa esecuzione, il substate generato e l’output risultante.

Finalizzazione di un blocco

Vediamo ora come viene finalizzato un blocco di molte transazioni.

Quando diciamo “finalizzare” può significare due cose diverse a seconda che il blocco sia nuovo o esistente. Se si tratta di un nuovo blocco ci riferiamo al processo necessario per il mining di tale blocco. Se si tratta di un blocco esistente allora stiamo parlando del suo processo di convalida. In entrambi i casi vi sono quattro requisiti per la “finalizzazione” di un blocco:

  1. Convalidare (o, se si tratta di mining, determinare) gli ommer: Ogni blocco di ommer all’interno dell’intestazione del blocco deve essere valido e deve essere entro la sesta generazione dal blocco attuale.
  2. Convalidare (o, se si tratta di mining, determinare) le transazioni: Il gasUsed del blocco deve essere uguale al gas cumulativo utilizzato per le transazioni elencate nel blocco (ricordate che durante l’esecuzione di una transazione teniamo traccia del contatore del gas del blocco che tiene traccia del gas totale utilizzato da tutte le transazioni nel blocco).
  3. Applicare le ricompense (solo se si tratta di mining): L’indirizzo del beneficiario si aggiudica 5 Ether per il mining (l’estrazione) del blocco (con la proposta EIP-649 questo premio di 5 ETH sarà presto ridotto a 3 ETH). Inoltre, per ogni ommer, all’attuale beneficiario del blocco viene assegnato un ulteriore 1/32 del premio attuale per il blocco. Infine al beneficiario del blocco o dei blocchi di ommer viene assegnato anche un determinato importo (esiste una formula speciale per questol calcolo).
  4. Convalidare (o, se si tratta di mining, calcolarne uno valido) lo stato ed i nonce: Assicurarsi che tutte le transazioni ed i cambiamenti di stato risultanti siano applicati, quindi definire il nuovo blocco come lo stato dopo che il premio di blocco è stato applicato allo stato finale della transazione. La verifica si effettua controllando questo stato finale rispetto al trie di stato memorizzato nell’intestazione.

“Proof of work” del mining

Nella sezione “Blocchi” abbiamo parlato brevemente del concetto di difficoltà dei blocchi. L’algoritmo che dà significato al concetto di difficoltà di un blocco è chiamato Proof of Work (PoW).

L’algoritmo di proof-of-work di Ethereum è chiamato “Ethash” (precedentemente noto come Dagger-Hashimoto).

L’algoritmo è formalmente definito come:

dove m è il mixHash, n è il 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, un insieme di dati.

Nella sezione “Blocchi”, abbiamo visto i vari elementi che esistono nell’intestazione di blocco. Due di queste componenti erano chiamate mixHash e nonce. Come ricorderete:

  • mixHash è un hash che, se combinato con il nonce, dimostra che il blocco corrente ha eseguito calcoli sufficienti.
  • nonce è un hash che, combinato con il mixHash, dimostra che il blocco corrente ha eseguito calcoli sufficienti

La funzione PoW viene utilizzata per valutare queste due voci.

Le modalità di calcolo del mixHash e della nonce  sono complesse, ma limitandoci ad una descrizione ad alto livello funziona così:

Per ogni blocco viene calcolato un “seed” (seme). Questo seed è diverso per ogni “epoca”, dove ogni epoca ha una lunghezza di 30.000 blocchi. Per la prima epoca il seed è l’hash di una serie di 32 byte di zeri. Per ogni epoca successiva è pari all’hash del precedente hash del seed. Usando questo seed un nodo può calcolare una “cache” pseudo casuale.

Questa cache è incredibilmente utile perché abilita il concetto di “nodi leggeri”, di cui si è parlato in precedenza. Lo scopo dei nodi leggeri è quello di permettere a determinati nodi di verificare in modo efficiente una transazione senza l’onere di memorizzare l’intero set di dati della catena di blocchi. Un nodo leggero può verificare la validità di una transazione basandosi esclusivamente su questa cache, perché la cache può rigenerare il blocco specifico che deve verificare.

Usando la cache un nodo può generare il “dataset” DAG dove ogni elemento del dataset dipende da un piccolo numero di elementi selezionati preudo-casualmente dalla cache. Per essere un miner è necessario generare questo set di dati completo; tutti i client e miner memorizzano questo set di dati che cresce linearmente nel tempo.

I miner possono quindi prendere elementi casuali dal set di dati ed elaborarli attraverso una funzione matematica per effettuarne un hash congiunto in un “mixHash”. Un miner genera ripetutamente tali mixHash fino a quando l’output ha un valore di nonce inferiore al valore nominale desiderato. Quando l’output soddisfa questo requisito questa nonce è considerata valida e il blocco può essere aggiunto alla catena.

Il mining come meccanismo di sicurezza
In generale, lo scopo dell’algoritmo di PoW è quello di dimostrare in modo crittograficamente sicuro che una particolare quantità di calcolo è stata effettuata per generare una certa quantità di output (cioè la nonce). Questo perché non c’è modo migliore per trovare una nonce che sia al di sotto della soglia richiesta se non quello di enumerare tutte le possibilità. I risultati 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à. Più alta è la difficoltà, più tempo ci vuole per risolvere la nonce. In questo modo l’algoritmo PoW dà significato al concetto di difficoltà che viene utilizzato per rafforzare la sicurezza della blockchain.

Che cosa intendiamo per sicurezza della blockchain ? È semplice: vogliamo creare una blockchain di cui TUTTI si fidino. Come abbiamo detto in precedenza se esistessero più catene gli utenti perderebbero la fiducia perché non sarebbero in grado di determinare ragionevolmente quale catena sia quella “valida”. Al fine di consentire ad un gruppo di utenti di accettare lo stato memorizzato su una blockchain abbiamo bisogno di una singola blockchain canonica.

Questo è esattamente ciò che fa l’algoritmo PoW: assicura che una particolare catena di blocchi rimanga canonica nel futuro, rendendo incredibilmente difficile per un hacker la creazione di nuovi blocchi che sovrascrivano una certa parte della cronologia (ad esempio cancellando le transazioni o creando transazioni false) o il mantenimento di una forchetta. Per far convalidare il blocco, un hacker dovrebbe risolvere la nonce più velocemente di chiunque altro nella rete, in modo tale che la rete ritenga che la sua catena sia la più pesante (basata sui principi del protocollo GHOST di cui abbiamo parlato). Ciò è impossibile a meno che l’aggressore non disponga di più della metà della potenza mineraria della rete, uno scenario noto come majority 51% attack.

Il mining come meccanismo di distribuzione della ricchezza

Oltre a fornire una blockchain sicura l’algoritmod di PoW è anche un modo per distribuire ricchezza a coloro che spendono la loro potenza elaborativa per fornire questa sicurezza. Ricordate che un minatore riceve una ricompensa per l’estrazione di un blocco composta da:

  • una ricompensa costante di 5 ether per il blocco “vincente” (che sarà presto ridotta a 3 ether)
  • il costo del gas speso all’interno del blocco dalle operazioni incluse nel blocco stesso
  • una ricompensa supplementare per l’inclusione degli ommer nel blocco

Al fine di garantire che l’utilizzo del meccanismo di consenso PoW per la sicurezza e la distribuzione della ricchezza sia sostenibile nel lungo periodo Ethereum si sforza di instillare queste due proprietà:

  • Renderlo accessibile a quante più persone possibile. In altre parole le persone non dovrebbero avere bisogno di hardware specializzato o non comune per eseguire l’algoritmo. Lo scopo è quello di rendere il modello di distribuzione della ricchezza il più aperto possibile, in modo che chiunque possa fornire qualsiasi quantità di potenza di calcolo in cambio di Ether.
  • Ridurre la possibilità che un singolo nodo (o un piccolo insieme di nodi) realizzi un profitto sproporzionato. Qualsiasi nodo che sia in grado di realizzare un profitto sproporzionato avrebbe una grande influenza sulla determinazione della blockchain canonica, e questo è un problema perché riduce la sicurezza della rete.

Nella blockchain Bitcoin un problema che si pone in relazione alle due proprietà di cui sopra è che l’algoritmo PoW è una funzione hash SHA256. Il punto debole di questo tipo di funzione è che può essere risolto in modo molto più efficiente utilizzando hardware specializzato, noto anche come ASIC.

Al fine di mitigare questo problema Ethereum ha scelto di rendere il suo algoritmo PoW (Ethhash) sequenzialmente difficile da memorizzare. Ciò significa che l’algoritmo è progettato in modo tale che il calcolo delle nonce richieda molta memoria e larghezza di banda. I grandi requisiti di memoria rendono difficile per un computer utilizzare la propria memoria in parallelo per calcolare più nonces simultaneamente, e i requisiti di banda elevata rendono difficile per un computer super-veloce scoprire più nonce simultaneamente. Ciò riduce il rischio di centralizzazione e crea condizioni di maggiore parità per i nodi che effettuano la verifica.

Conclusioni

Che dire, c’è molto da “digerire” in questo post per poi (volendo) approfondire sul il libro giallo e sul libro bianco di Ethereum, tuttavia spero che abbiate trovato utile questa visione d’insieme.

Se trovate degli errori segnalateli pure utilizzando la sezione Contatti del sito o lasciando un commento al post.

[Fonte: liberamente tradotto da questo articolo]

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.

Top
%d blogger hanno fatto clic su Mi Piace per questo: