Vai al contenuto

Costruire una blockchain da zero con Python (parte 2)

Nel precedente articolo abbiamo visto come creare da zero una Blockchain, in particolare abbiamo sviluppato un semplice motore di blockchain che riprende i concetti di base presenti in bitcoin. Proviamo adesso a sviluppare la parte restante, integrando le funzioni mancanti che permettono il pieno funzionamento della blockchain in rete. Iniziamo!

L’interfaccia

Lavorando sempre all’interno del file node_server.py, creeremo l’interfaccia per interagire con la blockchain che abbiamo appena creato. Utilizzeremo un popolare microframework Python chiamato Flask per creare API REST interattive e chiamare vari azioni nel nostro nodo blockchain. Come qualsiasi framework Web, anche Flask ci aiuta ad ottimizzare il codice e renderlo il più leggibile possibile.

from flask import Flask, request #importiamo le librerie necessarie di Flask 
import requests 

# Creo l'istanze dell'applicazione Flask passando __name__come argomento 
app = Flask (__ name__) 

# Inizializzo la blockchain (con il relativo il blocco di genesi) 
blockchain = Blockchain()

Adesso, abbiamo bisogno di un percorso (endpoint) per inserire una nuova transazione nell’array delle transazioni pendenti attraverso il submit di un form (con method=post), che creeremo più avanti. Per il momento, ci serve definire questa funzione:

@app.route('/new_transaction', methods=['POST'])
def new_transaction():
    tx_data = request.get_json() # dati in arrivo dal form attraverso il submit
    required_fields = ["author", "content"]

    # controllo tutti i campi obbligatori
    for field in required_fields:
        if not tx_data.get(field):
            # ritorna un errore 404 nel caso uno dei due campi fosse vuoto
            return "Invalid transaction data", 404  

    tx_data["timestamp"] = time.time() # aggiunge la data in formato timestamp
    blockchain.add_new_transaction(tx_data) # aggiunge la transazione nell'array delle transazioni pendenti
    return "Success", 201

Sviluppiamo di seguito, che verrà utilizzata dai nodi peers, un endpoint che restituisce una copia della catena sottoforma di stringa JSON. Questo endpoint potrà essere usato per interrogare tutti i blocchi e le transazioni presenti nella blockchain:

@app.route('/chain', methods=['GET'])
def get_chain():
    chain_data = []
    for block in blockchain.chain:
        chain_data.append(block.__dict__)
    return json.dumps({"length": len(chain_data),
                       "chain": chain_data,
                       "peers": list(peers)})

Ecco l’endpoint che permette di inviare una richiesta di mining convalidando le transazioni non verificate (se ovviamente l’array ha delle transazioni pendenti). Inoltre sviluppiamo un semplicissimo endpoint che ritorna le transazioni attualmente in stato di pending.

@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
    result = blockchain.mine()
    if not result:
        return "Nessuna transazione da minare"
    return "Block #{} è stato minato.".format(result)

@app.route('/pending_tx')
def get_pending_tx():
    return json.dumps(blockchain.unconfirmed_transactions)

Stabilire un meccanismo di consenso e dispersione

C’è un problema sulla blockchain in esecuzione su un computer. Sebbene abbiamo collegato i blocchi con il valore hash e applicato PoW, non è ancora possibile fidarsi di una singola entità (in questo caso, una singola macchina). Abbiamo bisogno di avere dati distribuiti o più nodi server per mantenere la blockchain in maniera distribuita. Quindi, per passare da un singolo nodo a una rete peer-to-peer, creiamo un meccanismo in modo che un nuovo nodo possa conoscere gli altri peer nella rete.

Un nuovo nodo che si unisce alla rete può chiamare la funzione  register_with_existing_node attraverso il suo endpoint /register_with per registrarsi con i nodi esistenti nella rete. La funzione svolgerà i seguenti compiti:

  • Richiede al nodo remoto di aggiungere un nuovo peer all’elenco di peer esistente
  • Inizializza facilmente la blockchain del nuovo nodo recuperando il nodo remoto
  • Risincronizza la blockchain con la rete se quel nodo non è più connesso alla rete
# Impostiamo innanzitutto un set vuoto che conterrà l'indirizzo host degli altri nodi della rete
peers = set()

# Endpoint per aggiungere un nuovo peer alla rete
@app.route('/register_node', methods=['POST'])
def register_new_peers():
    # Recupera l'indirizzo dell'host del nodo peer
    node_address = request.get_json()["node_address"]
    
    # nel caso in cui non viene fornito l'indirizzo, ritorna un errore
    if not node_address:
        return "Invalid data", 400 

    # Aggiungi l'indirizzo del nodo al set
    peers.add(node_address)

    # Ritorna la blockchain
    return get_chain()

# Endpoint per registrare un nuovo nodo. Al suo interno 
# effettua una chiamata alla rotta "register_node" per
# registrare il nodo corrente con il nodo remoto specificato
# nella richiesta POST e alla fine aggiorna la rete blockchain
@app.route('/register_with', methods=['POST'])
def register_with_existing_node():
    node_address = request.get_json()["node_address"]
    if not node_address:
        return "Invalid data", 400

    data = {"node_address": request.host_url}
    headers = {'Content-Type': "application/json"}

    # Richiesta di registrazione con il nodo remoto 
    response = requests.post(node_address + "/register_node", data=json.dumps(data), headers=headers)

    if response.status_code == 200:
        global blockchain
        global peers
        # aggiorna la catena e i peers
        chain_dump = response.json()['chain']
        blockchain = create_chain_from_dump(chain_dump) # crea la blockchain per il nuovo nodo peer
        peers.update(response.json()['peers'])
        return "Registration successful", 200
    else:
        # Se viene sollevato un errore, l'API ritorna un response
        return response.content, response.status_code

def create_chain_from_dump(chain_dump):
    generated_blockchain = Blockchain()
    generated_blockchain.create_genesis_block()
    for idx, block_data in enumerate(chain_dump):
        if idx == 0:
            continue # salta il blocco di genesi
        block = Block(block_data["index"],
                       block_data["transactions"],
                       block_data["timestamp"],
                       block_data["previous_hash"],
                       block_data["nonce"])
       proof = block_data['hash']
       added = generated_blockchain.add_block(block, proof)
       if not added:
           raise Exception("Catena manomessa!")
   return generated_blockchain

Esiste un problema: quando la rete è formata da molti nodi, a causa di problemi intenzionali o non (come ad esempio la latenza di rete), la copia della catena di alcuni nodi potrebbe essere diversa. In tal caso i nodi devono concordare una versione della catena per mantenere l’integrità dell’intero sistema. In altre parole, dobbiamo raggiungere un consenso.

Un semplice algoritmo di consenso è quello di stabilire valida la catena più lunga se le catene dei nodi peer appaiono biforcute. Il motivo di questa scelta è che la catena più lunga dimostra il maggior lavoro svolto (PoW è molto meticoloso):

class Blockchain

   ...

    def check_chain_validity(cls, chain):
        # funzione che controlla se l'interna blockchain è corretta
        result = True
        previous_hash = "0"

        # il ciclo for viene eseguito per tutti i blocchi presenti nella catena
        for block in chain:
            block_hash = block.hash
            # cancella il campo hash per ricalcolare il valore hash usando la funzione compute_hash
            delattr(block, "hash")

            if not cls.is_valid_proof(block, block.hash) or previous_hash != block.previous_hash:
                result = False
                break

            block.hash, previous_hash = block_hash, block_hash

        return result

def consensus():
    # La funzione implementa un semplice algoritmo di consenso. 
    # Va a controllare la lunghezza della catena. Se la lunghezza trovata è maggiore di 
    # tutte quelle possibili, allora la catena viene sostituita.
    global blockchain

    longest_chain = None
    current_len = len(blockchain.chain)

    for node in peers:
        response = requests.get('{}/chain'.format(node))
        length = response.json()['length']
        chain = response.json()['chain']
        if length > current_len and blockchain.check_chain_validity(chain):
            # trovata una catena di lunghezza superiore e viene sostituita
            current_len = length
            longest_chain = chain

    if longest_chain:
        blockchain = longest_chain
        return True

    return False

Successivamente dobbiamo sviluppare un modo per qualsiasi nodo di informare la rete che ha estratto un blocco in modo che tutti possano aggiornare la propria blockchain e passare all’estrazione di altri blocchi. Altri nodi potrebbero dover solo verificare il PoW e aggiungere il blocco appena estratto alla rispettiva catena (ricorda che la verifica è facile quando si conosce il nonce):

# Endpoint che aggiunge il blocco appena minato alla catena.
# Il blocco richiede di essere verificato prima di poter essere inserito alla catena.
@app.route('/add_block', methods=['POST'])
def verify_and_add_block():
    block_data = request.get_json()
    block = Block(block_data["index"],
                  block_data["transactions"],
                  block_data["timestamp"],
                  block_data["previous_hash"])

    proof = block_data['hash']
    added = blockchain.add_block(block, proof)

    if not added:
        return "Il blocco è stato scartato dal nodo", 400

    return "Block aggiunto alla catena", 201

def announce_new_block(block):
    # Funzione che informa la rete dopo aver minato un blocco.
    # Gli altri nodi peer devono solo verificare il PoW e 
    # aggiungere la stringa corrispondente.
    for peer in peers:
        url = "{}add_block".format(peer)
        requests.post(url, data=json.dumps(block.__dict__, sort_keys=True))

La funzione  announce_new_block() dovrebbe essere chiamata dopo che ogni blocco è stato estratto dai nodi in modo che gli altri peer possano aggiungerlo alla loro catena. Per questo motivo è necessario fare una piccola modifica al metodo mine, aggiungendo qualche riga in più nel blocco else.

@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
    result = blockchain.mine()
    if not result:
        return "Nessuna transazione da minare"
    else:
        # Nuove righe di codice
        # Assicuriamoci di avere la lunghezza massima della catena, 
        # prima di comunicare con la rete
        chain_length = len(blockchain.chain)
        consensus()
        if chain_length == len(blockchain.chain):
            # annuncia a tutti i nodi peer che il blocco è stato minato
            announce_new_block(blockchain.last_block)
        return "Block #{} è stato minato.".format(blockchain.last_block.index)

Lo sviluppo dell’applicazione web

Ora è il momento di iniziare a sviluppare l’interfaccia dell’applicazione web. Abbiamo utilizzato il template engine Jinja2 per creare le view e alcuni stili CSS per migliorare l’estetica dell’interfaccia.

L’applicazione deve connettersi a un nodo nella rete blockchain per recuperare i dati e anche per inviare nuovi dati.

Nel nuovo file chiamato views.py, conterrà la logica necessaria all’applicazione web.

import datetime
import json

import requests
from flask import render_template, redirect, request

from app import app

# Indirizzo del nodo a cui collegarsi per recuperare le informazioni
CONNECTED_NODE_ADDRESS = "http://127.0.0.1:8081"

posts = []

Svilupiamo la funzione  fetch_posts che non farà altro che recuperare le informazioni della catena:

def fetch_posts():
    get_chain_address = "{}/chain".format(CONNECTED_NODE_ADDRESS)
    response = requests.get(get_chain_address)
    if response.status_code == 200:
        content = []
        chain = json.loads(response.content)
        for block in chain["chain"]:
            for tx in block["transactions"]:
                tx["index"] = block["index"]
                tx["hash"] = block["previous_hash"]
                content.append(tx)

        global posts
        posts = sorted(content,
                       key=lambda k: k['timestamp'],
                       reverse=True)

L’applicazione dispone di un modulo HTML per inserire l’input dell’utente ed effettuare una richiesta POST al nodo connesso per aggiungere transazioni all’array di transazioni pending. La transazione viene quindi estratta dalla rete e infine recuperata dopo aver ricaricato la pagina:

@app.route('/submit', methods=['POST'])
def submit_textarea():
    post_content = request.form["content"]
    author = request.form["author"]

    post_object = {
        'author': author,
        'content': post_content,
    }

    # Submit della transazione
    new_tx_address = "{}/new_transaction".format(CONNECTED_NODE_ADDRESS)

    requests.post(new_tx_address,
                  json=post_object,
                  headers={'Content-type': 'application/json'})

    # Ritorna in homepage
    return redirect('/')

Eseguire correttamente la blockchain

Occorre innanzitutto aprire una finestra della console e posizionarsi sulla cartella del nostro progetto. Le istruzioni che seguono sono ottimizzate per l’uso con sistemi operativi Windows. Apriamo una finestra della console con il comando CMD dal menu Start.

Innanzitutto installiamo le dipendenze che abbiamo usato nel nostro codice:

pip install -r requests
pip install -r Flask

Successivamente avviamo la blockchain sulla porta 8081:

set FLASK_APP=node_server.py
flask run --port 8081

Apriamo una nuova finestra di console, e digitiamo il seguente comando:

python run_app.py

L’applicazione verrà avviata all’indirizzo http: // localhost: 5000

Avvio di istanze multiple (simulazione rete peer)

set FLASK_APP=node_server.py
flask run --port 8081 | flask run --port 8082 | flask run --port 8083

In una nuova finestra di console, digitiamo il seguente comando (attenzione agli apici e alle virgolette):

curl -X POST http://127.0.0.1:8082/register_with -H "Content-Type: application/json" -d "{\"node_address\": \"http://127.0.0.1:8081\"}"
curl -X POST http://127.0.0.1:8083/register_with -H "Content-Type: application/json" -d "{\"node_address\": \"http://127.0.0.1:8081\"}"

L’applicazione viene eseguita con lo stesso comando di prima:

python run_app.py

Stavolta, quando creeremo le transazioni tramite l’interfaccia web e faremo il mining, tutti i nodi della rete aggiorneranno la catena.

La catena di ogni singolo nodo potrà essere controllata da questo comando:

curl -X GET http://localhost:8082/chain
curl -X GET http://localhost:8083/chain

Per scaricare l’intero progetto, clicca qui.