I blockchain sono una struttura dati inventata da Satoshi Nakamoto, pseudonimo dell’inventore della criptovaluta bitcoin, che nel 2008 la utilizzò per risolvere il problema del double spending. Il successo di tale struttura dati non è però legata esclusivamente al suo utilizzo nell’infrastruttura bitcoin, ma più in generale nella possibilità di utilizzarla per realizzare registri pubblici distribuiti praticamente inviolabili.
Definizione
Da un punto di vista prettamente informatico il blockchain è una catena o lista di blocchi (di dati) che hanno la caratteristica di include l’hash (funzione matematica non invertibile che mappa una stringa di lunghezza arbitraria in una stringa di lunghezza predefinita) che identifica il blocco in modo univoco e che permette il collegamento con il blocco successivo nella catena.
La peculiarità che rende il registro non modificabile è che l’hash del blocco è calcolato includendo anche l’hash del blocco precedente. Se, ad esempio, andiamo a modificare i dati del blocco 1 in figura, anche il suo hash risulterà differente e conseguentemente l’hash di tutti i blocchi che lo seguono.
L’operazione di hashing, però, di per se non è una operazione computazionalmente difficile, ma come vedremo viene resa tale introducendo dei vincoli sul suo valore. Inoltre, alterare una catena blockchain in un server non è sufficiente, in quanto tutti i server che partecipano alla gestione del registro dovrebbero approvare la modifica. Quest’ultimo aspetto non è comunque oggetto del presente articolo, che si concentra prevalentemente sulla definizione della struttura dati.
Struttura Dati del Blocco
Innanzitutto definiamo la classe che implementa la struttura dati di un blocco. Come accennato conterrà l’hash corrente, l’hash del blocco precedente ed i dati. Inoltre è presente un timestamp ed un intero nonce
la cui utilità sarà evidente quando parleremo di mining.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class Block { private String hash; // Block hash private String previousHash; // Previous block hash private String data; // Block data private long timeStamp; // As number of milliseconds private int nonce; // Number generated during mining // Block Constructor public Block(String data) { this.data = data; this.timeStamp = System.currentTimeMillis(); } // Getter and Setter @Override public String toString() { return previousHash + Long.toString(timeStamp) + Integer.toString(nonce) + data; } } |
Calcolo dell’Hash
Per il calcolo dell’hash utilizziamo le api di Java Security ed in particolare la classe MessageDigest. L’operazione restituisce però una sequenza di byte quindi per convertirlo in una stringa “stampabile” la codifichiamo in esadecimale con le funzionalità del pacchetto Apache Commons Codec. Per comodità collochiamo la funziona in una classe di utilità Utils
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static String applySha256(String input){ try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); //Applies sha256 to our input, byte[] hash = digest.digest(input.getBytes("UTF-8")); return Hex.encodeHexString( hash ); } catch(Exception e) { throw new RuntimeException(e); } } |
Integrità della Blockchain
Verificare l’integrità della blockchain significa verificare la correttezza dell’hash del singolo blocco e la corrispondenza dell’hash del blocco precedente (ad eccezione del primo blocco della catena che ovviamente non ha l’hash del blocco precedente). La funzione di verifica della validità riceve quindi in input una coppia di blocchi consecutivi ed esegue i test indicati:
1 2 3 4 5 6 7 8 9 10 11 12 |
public boolean isValid( Block previous, Block current ) { //compare registered hash and calculated hash if ( !current.getHash().equals( calculateHash( current ) ) ) { System.out.println("Current Hashes not equal"); return false; } //compare previous hash (if defined) and registered previous hash if ( previous != null && !previous.getHash().equals( current.getPreviousHash() ) ) { System.out.println("Previous Hashes not equal"); return false; } } |
Ovviamente è sufficiente che un solo blocco della catena non soddisfi le condizioni affinché l’intera blockchain risulti non valida e quindi corrotta. Immaginando di memorizzare la blockchain in una variabile blockchain
di tipo ArrayList
la funzione di verifica sarà:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public Boolean isChainValid() { Block previous = null; boolean valid = true; for ( Block current : this.blockChain ) { if ( !isValid(previous, current) ) { System.out.println( "Block " + current.getHash() + " is NOT valid "); valid = false; } else { System.out.println( "Block " + current.getHash() + " is valid "); } previous = current; } return valid; } |
Mining
Il mining è l’operazione principale eseguita sulla blockchain e consiste sostanzialmente nel generare un nuovo blocco e manipolarlo opportunamente affinché possa essere aggiunto alla blockchain mantenendola in uno stato di validità. Molto semplicemente significa calcolarne l”hash ed aggiungerlo alla catena inserendo il riferimento al blocco precedente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public ArrayList<Block> blockChain = new ArrayList<Block>(); public void addBlock(String data) { Block block = new Block( data ); if ( blockChain.isEmpty() ) { block.setPreviousHash( "0" ); } else { block.setPreviousHash( blockChain.get( blockChain.size() - 1 ).getHash() ); } block.setHash( calculateHash( block ) ); blockChain.add( block ); } public String calculateHash( Block block ) { return Utils.applySha256( block.toString() ); } |
Nella realtà le cose non si svolgono nel modo indicato. La sicurezza dell’intera infrastruttura della blockchain si basa su due requisiti:
- la verifica dell’integrità di una blockchain deve essere veloce in modo da poter essere eseguita su catene anche molto grandi in tempi rapidi;
- il calcolo dell’hash per l’inserimento di un nuovo blocco nella catena deve essere un’operazione computazionalmente difficile e richiedere quindi tempi lunghi. Questo al fine di rendere praticamente impossibile la corruzione del dato di un blocco che, come detto, richiederebbe il calcolo dell’hash su tutta la catena con tempi “biblici”.
La soluzione presentata soddisfa il primo requisito ma non il secondo. Per rendere difficile l’operazione di mining l’architettura blockchain introduce un semplice vincolo sull’hash generato. Il vincolo è quello che l’hash debba iniziare con un numero di caratteri 0
pari ad una costante del sistema chiamata difficulty
. E’ a tale scopo che è introdotta la nonce
nella struttura dati del singolo blocco. L’idea del mining è quella di modificare la nonce
fino a quando l’hash calcolato non soddisfi il vincolo imposto.
1 2 3 4 5 6 7 8 9 10 |
public void mineBlock( Block block ) { String hash = calculateHash(block); while( !hash.startsWith( target ) ) { block.setNonce( block.getNonce() + 1 ); hash = calculateHash( block ); } block.setHash( hash ); System.out.println("Block Mined!!! : " + hash); } |
Nella funzione minBlock()
la variabile target
non è altro che una stringa con tanti 0
quanto è il valore della difficulty
. Nel sistema bitcoin la difficulty è un parametro che viene aggiornato periodicamente al fine di rendere sempre più difficile il mining, anche in considerazione dell’upgrade tecnologico degli hardware utilizzati.
Tornando al nostro esempio l’operazione di inserimento di un nuovo blocco diviene:
1 2 3 4 5 6 7 8 9 10 |
public void addBlock(String data) { Block block = new Block( data ); if ( blockChain.isEmpty() ) { block.setPreviousHash( "0" ); } else { block.setPreviousHash( blockChain.get( blockChain.size() - 1 ).getHash() ); } mineBlock( block ); blockChain.add( block ); } |
Mentre all’operazione di verifica della validità dei una blockchain aggiungiamo la condizione che l’hash soddisfi il vincolo della difficulty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public boolean isValid( Block previous, Block current ) { //compare registered hash and calculated hash if ( !current.getHash().equals( calculateHash( current ) ) ) { System.out.println("Current Hashes not equal"); return false; } //compare previous hash (if defined) and registered previous hash if ( previous != null && !previous.getHash().equals( current.getPreviousHash() ) ) { System.out.println("Previous Hashes not equal"); return false; } //check if hash is solved if ( !current.getHash().startsWith( target ) ) { System.out.println("This block hasn't been mined"); return false; } return true; } |
Codice Sorgente
Il codice sorgente completo anche di un Main()
che esegue un tentativo di corruzione dei dati della blockchain è scaricabile qui blockchain.