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.