Vai al contenuto

Costruire una blockchain da zero con Python (parte 1)

Negli articoli precedenti ho descritto cosa è una blockchain e uno dei possibili usi futuri. In questo articolo entreremo più nel dettaglio tecnico di cosa c’è all’interno e come si implementa una blockchain partendo dalle basi. Utilizzeremo per i nostri esperimenti Javascript e Python che sono i linguaggi maggiormente diffusi e facilmente comprensibili da tutti.

La maggior parte dei motori di blockchain sono sviluppati in c/c++ (Bitcoin, EOS, ecc…), Go (Hyperledger Fabric, Ethereum), Java (Ethereum), Rust , Haskell (Cardano) e/o Ruby (Ethereum). Inoltre, alcuni motori combinano molti linguaggi di programmazione per robustezza e facilità d’uso per gli sviluppatori, come ad esempio Ethereum.

La Blockchain

Avendo già ampiamente  definito la blockchain, diremo quindi cosa non è una blockchain: Blockchain non è Bitcoin, blockchain non è una valuta digitale. Blockchain quindi è una tecnologia che esisteva ancora prima delle cryptovalute.

Supponiamo di avere un database MySql qualsiasi, all’interno del quale sono definite delle tabelle con dei campi. Possiamo interagire con esso con le classiche operazioni CRUD (creazione, lettura, aggiornamento e cancellazione) e i vari linguaggi di programmazione e librerie di connessione in maniera ormai molto performante e discretamente sicura. I database rappresentano i principali contenitori di informazioni e dati presenti in rete. Ma c’è un problema: il database è centralizzato, può essere facilmente compromesso da malintenzionati e quindi non c’è modo di fidarsi del tutto di ciò che è memorizzato al suo interno. Inoltre il database ha bisogno di un gestore e gli utenti non hanno potere sui propri dati.

E’ qui che entra in gioco la blockchain: per fornire una tecnologia per distribuire dati in maniera trasparente, affidabile, indipendente dalle persone, automatico, immutabile, decentralizzato e indistruttibile.

Come dice la parola stessa, blockchain è una catena di blocchi. Ciascun blocco è come se fosse una tabella di un database, ma con la peculiarità di non poter essere cancellati o aggiornati. Ciascun blocco contiene al suo interno delle informazioni essenziali (Transazioni, nonce, bit target, difficoltà, timestamp, hash blocco precedente, hash blocco corrente, albero Markle, ID blocco, ecc…). Ciascun blocco viene verificato crittograficamente e concatenato per formare una catena immutabile di blocchi chiamata blockchain o ledger.

La stessa catena viene quindi distribuita a tutti i nodi (computer o minatori) attraverso la rete tramite una rete P2P.

Quindi, invece di avere un database centralizzato, tutte le transazioni (dati) condivise tra i nodi sono contenute in blocchi, che sono concatenati insieme formando un registro. Questo registro rappresenta tutti i dati nella blockchain. Tutti i dati nel registro sono protetti da hashing crittografico e firma digitale e convalidati da un algoritmo di consenso Proof of Work (PoW) o più recentemente Proof of Stake (PoS). I nodi della rete partecipano per garantire che tutte le copie dei dati distribuiti nella rete siano le stesse.

I concetti chiave da tenere in mente sono i seguenti:

  1. Hash crittografico e firma digitale
  2. Registro immutabile
  3. Rete P2P
  4. Algoritmo di consenso (PoW, PoS, PBFT, ETc…)
  5. Convalida dei blocchi (estrazione mineraria, forgiatura, ecc…)

Quando usare la blockchain?

Non è detto che la blockchain sia la panacea di tutti i mali. I casi in cui andrebbe utilizzata una blockchain sono principalmente i casi in cui si richiede un’elevata sicurezza, oppure quando i dati non possono (o non devono) essere modificati (prova dell’esistenza) oppure non possono essere negati (non disconoscibilità). Una blockchain tuttavia è molto più lenta rispetto a un database centralizzato (Bitcoin ad esempio  impiega in media 10 minuti per convalidare un blocco di transazioni), anche se alcuni algoritmi di consenso sono più veloci rispetto ad altri.

Chi usa la blockchain

Le Blockchain vanno oltre le criptovalute perchè può essere utilizzata in diversi settori, tra cui immobiliare, in ambito santario, nella finanza, nelle catene di approvigionamento (tracciamento degli articoli dai fornitori ai clienti, fornisce autenticità), sicurezza informatica, meccanismi di voto e ovviamente criptovalute (Bitcoin, Ethereum, Hyperledger Fabric, EOS, Chainlink, Cardano, Dogecoin, etc…).

Tipi di blockchain

Ci sono 3 tipi di blockchain:

  • Privato: viene utilizzata solo internamente e quando conosciamo gli utenti (es. Hyperledger Fabric)
  • Pubblico: tutti possono vedere cosa sta succedendo (Bitcoin, Ethereum)
  • Ibrido: si tratta di una combinazione dei primi due.

Costruire una blockchain

Il più semplice è utilizzare blockchain open-source precostruiti come Ethereum (applicazioni distribuite, altcoin, DEFI, NFT, ecc.), Fabric (blockchain privata), EOS, Cardano, ecc. che forniscono tutti gli strumenti necessario e non si ha a che fare con l’implementazione del motore interno che è abbastanza complesso da sviluppare. Se nessuno di questi soddisfa i nostri requisiti, possiamo creane uno da zero o tramite un fork di uno open source già presente su github. Ad esempio, litecoin e bitcoin cash sono dei fork di bitcoin. Quest’ultimo metodo richiede una squadra di sviluppatori perchè richiede molto lavoro.

In questo articolo, costruiremo un prototipo di blockchain da zero in modo da comprendere la macchina a stati della blockchain. Utilizzeremo Python per sviluppare il prototipo che ci aiuterà a comprendere i concetti descritti in precedenza. Ecco cosa faremo:

  • creazione di un blocco
  • aggiunta di dati nell’header e nel body
  • hashing del blocco
  • concatenazione dei blocchi

Il motore della nostra Blockchain

L’obiettivo è creare un’applicazione che permetta agli utenti di condividere informazioni pubblicando post. Il contenuto verrà archiviato sulla blockchain, che non verrà più modificato. Gli utenti potranno interagire con l’applicazione attraverso una semplice interfaccia web. Useremo l’approccio dal basso verso l’alto. Inizia definendo la struttura dei dati che memorizzeremo nella blockchain. Ogni post includerà tre elementi essenziali:

{ 
  "author": "nome dell'autore", 
  "content": "Le informazini che l'autore vuole condividere", 
  "timestamp": "La data esatta di creazione del contenuto"
}

Ogni post sarà una transazione, la quale verrà impacchettata in un blocco. Un blocco può contenere una o più transazioni. I blocchi contenenti transazioni vengono generati regolarmente e aggiunti alla blockchain. Poiché ci sono molti blocchi, ogni blocco avrà un  ID unico.

Creiamo dunque un nuovo file node_server.py e iniziamo a scrivere le nostre classi:

class Block:
     def __init__ (self, index, transactions, timestamp):
         # Costruttore per la classe Block
         # param index: ID Univoco del blocco
         # param transactions: lista delle transazioni
         # param timestamp: data di creazione del blocco
         self.index = index
         self.transactions = transactions
         self.timestamp = timestamp

Per evitare che vengano archiviati dati falsi all’interno dei blocchi utilizzeremo una funzione di hash. Una funzione hash è una funzione che accetta dati di qualsiasi dimensione e crea un output di una dimensione fissa. Viene spesso utilizzata per determinare i dati di input. Le funzioni hash ideali hanno spesso le seguenti caratteristiche:

  • sia facile da calcolare
  • gli stessi dati risulteranno sempre nello stesso valore hash
  • anche un singolo cambiamento di bit nei dati cambierà significativamente il valore hash di output

Le funzioni di hashing vengono usate anche perchè è quasi impossibile risalire all’input attraverso l’hash (l’unico modo sarebbe quello di provare tutti i possibili casi di input). Se conosciamo sia l’input che il valore di hash risultante, dobbiamo solo passare l’input tramite la funzione hash per verificare che il valore hash fornito sia corretto.

from hashlib import sha256
import json

def compute_hash(block):
     # Prende in input il blocco, converte il blocco in una stringa JSON 
     # e ritorna il valore hash corrispondente.
     block_string = json.dumps(self .__ dict__, sort_keys = True)
     return sha256(block_string.encode()).hexdigest() #funzione hash,

La funzione sopra non fa altro che ritornare l’hash relativo al blocco in input.

  • json.dumps effettua il dump dell’oggetto self.__dict__
  • self.__dict__ oggetto di mappatura usato per memorizzare gli attributi di un oggetto.
  • La funzione hexdigest() ritorna la stringa hash

Adesso, abbiamo bisogno di un modo per garantire che qualsiasi modifica nei blocchi precedenti invalidi l’intera catena. Il modo in cui funziona Bitcoin è creare dipendenze tra blocchi successivi collegandoli immediatamente con il valore hash del blocco. Ciò significa salvare l’hash del blocco precedente nel blocco corrente. Ridefiniamo quindi il costruttore inserendo un nuovo campo di nome  previous_hash. Ecco il codice completo della classe Block.

class Block:
    def __init__(self, index, transactions, timestamp, previous_hash, nonce=0):
        self.index = index
        self.transactions = transactions
        self.timestamp = timestamp
        self.previous_hash = previous_hash
        self.nonce = nonce

    def compute_hash(self):
        # Funzione che ritorna l'hash del contenuto del blocco
        block_string = json.dumps(self.__dict__, sort_keys=True)
        return sha256(block_string.encode()).hexdigest()

E il primo blocco? E’ chiamato blocco di genesi e può essere creato manualmente o tramite una logica. Implementiamo quindi una funzione che permette di generare il primo blocco, che verrà richiamata all’interno del costruttore dell’altra classe Blockchain.

class Blockchain:

    def __init__ (self):
        # Costruttore della classe Blockchain
        self.chain = []
        self.create_genesis_block()

    def create_genesis_block(self):
        # La funzione genera il blocco di genesi e lo aggiunge alla blockchain. 
        # Il primo blocco ha un indice pari a 0, 
        # previous_hash uguale a 0 è un valore hash valido.
        genesis_block = Block(0, [], time.time(), "0")
        genesis_block.hash = genesis_block.compute_hash() #creazione dinamica dell'attributo hash
        self.chain.append(genesis_block) #aggiunge il primo blocco alla chain

    @property
    def last_block(self):
        # Una semplice proprietà che prende l'ultimo blocco della catena. 
        # P.S. Nella catena c'è sempre un primo blocco che sarà il blocco di genesi
        return self.chain[-1]

Ora, se il contenuto di qualsiasi blocco precedente cambia, cambierà l’hash del blocco precedente e quindi ci sarà una mancata corrispondenza con il campo previous_hash nel blocco successivo. Poiché i dati di input per calcolare il valore hash di qualsiasi blocco includono anche il campo previous_hash, cambierà anche il valore hash del blocco successivo.

Alla fine, l’intera catena è compromessa e l’unico modo per ripristinarla è ricalcolare l’intera catena.

Implementazione dell’algoritmo Proof-Of-Work

Il problema, però, è che il valore hash di tutti i blocchi successivi può essere facilmente ricalcolato per creare un’altra blockchain valida. Per evitare ciò, possiamo sfruttare l’asimmetria della funzione hash di cui abbiamo discusso sopra per rendere più difficile e casuale il compito di calcolare il valore hash. In altre parole, invece di accettare qualsiasi valore hash per il blocco, aggiungiamo alcuni vincoli. Aggiungiamo il vincolo che il nostro valore hash inizi con n zeri iniziali dove n è un numero intero positivo.

Qui aggiungeremo alcuni dati fittizi che possiamo modificare. Aggiungeremo un nuovo campo nonce che rappresenta un attributo del blocco che possiamo continuare a incrementare di una unità finché non calcoliamo un hash che soddisfa il nostro requisito: nel caso specifico l’hash deve cominciare con la stringa “00” con difficulty = 2.

Il fatto che il nonce soddisfi i vincoli funge da prova che alcuni calcoli sono stati eseguiti. Questa tecnica è una versione molto semplificata dell’algoritmo Hashcash utilizzato in Bitcoin. Il numero di zeri specificato nel vincolo determina la difficoltà dell’algoritmo PoW (maggiore è il numero di zeri, più difficile è trovare il nonce… con difficoltà = 4, l’hash da trovare dovrebbe iniziare con 4 zeri). Anche a causa dell’asimmetria, Proof-of-work (PoW) è difficile da calcolare ma molto facile da verificare una volta trovato il nonce (è sufficiente eseguire nuovamente la funzione hash). Aggiungiamo quindi la nuova funzione proof_of_work nella classe Blockchain:

class Blockchain:
     # Livello di difficoltà dell'algoritmo POW
     difficulty = 2

     ...

     def proof_of_work (self, block):
         # La funzione verifica diversi valori di nonce fino 
         # a quando non trova il valore hash soddisfacente.
         block.nonce = 0

         computed_hash = block.compute_hash()
         while not computed_hash.startswith('0' * Blockchain.difficulty):
             block.nonce + = 1
             computed_hash = block.compute_hash()

         return computed_hash

Aggiungere blocchi alla catena

Per aggiungere un blocco alla catena, dobbiamo prima verificare che i dati non vengono manomessi e l’ordine delle transazioni è mantenuto lo stesso.

class Blockchain:

    ...

     def add_block(self, block, proof):
         # Aggiunge un blocco alla catena dopo la verifica. La verifica include che
         # - la proof-of-work sia corretta
         # - l'hash precedente sia corretto e corrisponda al valore hash del nuovo blocco nella catena

         previous_hash = self.last_block.hash  # prende l'hash dell'ultimo blocco nella catena

         # se l'hash precedente non è uguale al previuos_hash del 
         # blocco che sto per aggiungere, ritorna falso
         if previous_hash != block.previous_hash:  
             return False

         # controlla anche la validità della proof del blocco
         if not Blockchain.is_valid_proof(block, proof):
             return False

         block.hash = proof
         self.chain.append(block)
         return True

     def is_valid_proof(self, block, block_hash):
         # Controlla se block_hash è il valore hash valido del blocco 
         # e se soddisfa i criteri di difficoltà
         return(block_hash.startswith('0' * Blockchain.difficulty) and
                 block_hash == block.compute_hash())

Estrazione

Le transazioni inserite verranno archiviate come un gruppo di transazioni non confermate. Il processo di inserimento di transazioni non confermate in un blocco e calcolo del POW è chiamato blocchi di mining. Quando viene trovato il nonce che soddisfa i vincoli, possiamo dire che un blocco è stato minato e può essere inserito nella blockchain.

Nella maggior parte delle criptovalute (incluso Bitcoin), ai minatori può essere assegnato un importo in criptovaluta come ricompensa per l’utilizzo della loro potenza di calcolo per calcolare il POW. Ecco la funzione mining:

class Blockchain:

     def __init __ (self):
         self.unconfirmed_transactions = [] # nuova aggiunta: lista delle transazioni non confermate
         self.chain = []
         self.create_genesis_block()

     ...

     def add_new_transaction(self, transaction):
         # Funzione che aggiunge della transazione all'array delle transazioni non confermate
         self.unconfirmed_transactions.append(transaction)

     def mine(self):
         # La funzione aggiunge le transazioni pendenti dentro la blockchain
         # aggiungendole al blocco e rilasciando il PoW
         if not self.unconfirmed_transactions: # se non ci sono transazioni pendenti, ritorna falso
             return False

         last_block = self.last_block # recupera l'ultimo blocco

         # - crea un nuovo blocco, assegnando un nuovo indice sulla base dell'indice dell'ultimo blocco
         # incrementato di una unità
         # - imposta dentro il blocco tutte le transazioni pendenti (array)
         # - imposta come previous_hash, l'hash dell'ultimo blocco (last_block)
         new_block = Block (index = last_block.index + 1,
                           transactions = self.unconfirmed_transactions,
                           timestamp = time.time (),
                           previous_hash = last_block.hash)

         proof = self.proof_of_work(new_block)  # calcola la proof-of-work del nuovo blocco
         self.add_block(new_block, proof)  # aggiunge il nuovo blocco alla blockchain
         self.unconfirmed_transactions = []  # svuota l'array delle transazioni pendenti
         return new_block.index  # ritorna l'indice del blocco

Bene, abbiamo sviluppato la prima parte relativa al motore della Blockchain. Nel prossimo articolo vedremo come far funzionare il motore, aggiungendo le funzioni mancanti per effettuare le richieste POST tramite una semplice interfaccia web.