Desarrollo de una aplicación blockchain en Python

Python es un lenguaje de programación fácil de entender y lo usan millones de programadores, por eso lo elegí para este ejemplo. El código de este ejemplo se puede descargar de mi GitHub.

¿Qué es “blockchain”?

Blockchain es una forma de almacenar datos digitales. Los datos pueden ser literalmente cualquier cosa. Para Bitcoin, son las transacciones (registros de transferencias de Bitcoin de una cuenta a otra), pero incluso pueden ser archivos. Los datos se almacenan en forma de bloques, que están vinculados (o encadenados) entre sí mediante hashes criptográficos, de ahí el nombre “blockchain”.

Toda la “magia” radica en la forma en que estos datos se almacenan y se agregan a la cadena de bloques. Una cadena de bloques es esencialmente una lista vinculada que contiene datos ordenados, con algunas restricciones:

  • Los bloques no se pueden modificar una vez agregados.
  • Hay reglas específicas para agregarle datos.
  • Su arquitectura está distribuida.

Estas características aportan beneficios como:

  • Inmutabilidad y durabilidad de los datos.
  • Puede seguir funcionando si algún nodo falla.
  • Trazabilidad verificable del orden en que se agregaron los datos.

Vamos a implementarlo usando un enfoque de abajo hacia arriba. Comencemos por definir la estructura de los datos que almacenaremos en la cadena de bloques. Una publicación es un mensaje que publica cualquier usuario en nuestra aplicación. Cada publicación constará de tres elementos esenciales:

  • Contenido.
  • Autor.
  • Marca de tiempo.

Almacenar transacciones en bloques

Vamos a almacenar datos en nuestra cadena de bloques en un formato de sobra conocido: JSON. La estructura básica de transacción que usaremos será esta:

{ 
"author": "some_author_name", 
"content": "Some thoughts that author wants to share", 
"timestamp": "The time at which the content was created" 
}

Las transacciones se empaquetan en bloques. Un bloque puede contener una o varias transacciones por lo que la cadena es una lista de bloques (eslabones) concatenados. Debido a que puede haber varios bloques, cada bloque debe tener un ID único:

class Block:
    def __init__(self, index, transactions, timestamp):
        """
        Constructor for the `Block` class.
        :param index: Unique ID of the block.
        :param transactions: List of transactions.
        :param timestamp: Time of generation of the block.
        """
        self.index = index 
        self.transactions = transactions 
        self.timestamp = timestamp

2. Agregando “huellas digitales” a los bloques

Unas de las principales características/ventajas de los bloques de la tecnologia “blockchain” es que un bloque no puede ser manipulado. Para conseguirlo vamos a utilizar funciones hash criptográficas.

Como no es el objetivo de este post explicar el funcionamiento de las funciones “hash”, lo podríamos resumir muy rápidamente diciendo que una función “hash” es una función que a partir de unos datos de cualquier tamaño, produce una cadena alfanumérica de tamaño fijo (un “hash”). Las características de una función hash ideal son:

  • Debería ser fácil de calcular.
  • Debe ser determinista, lo que significa que los mismos datos siempre darán como resultado el mismo hash.
  • Debe ser uniformemente aleatorio, lo que significa que incluso un cambio de un solo bit en los datos debería cambiar el hash de manera significativa.

En consecuencia, es prácticamente imposible adivinar los datos de entrada dado el hash. (La única forma es probar todas las combinaciones de entrada posibles).

Almacenaremos el hash del bloque en un campo dentro de nuestro objeto Block, y actuará como una huella digital (o firma) de los datos contenidos en él:

from hashlib import sha256
import json

def compute_hash(block):
    """
    Returns the hash of the block instance by first converting it
    into JSON string.
    """
    block_string = json.dumps(self.__dict__, sort_keys=True)
    return sha256(block_string.encode()).hexdigest()

3. Encadenando los bloques

Bien, ahora configuraremos los bloques. Como deciamos anteriormente, la cadena de bloques es una serie de bloques consecutivos. Podriamos almacenar todos los bloques en la lista de Python (el equivalente a una matriz). Pero esto no seria suficiente, porque ¿qué pasa si alguien reemplaza intencionalmente un bloque viejo con un bloque nuevo en la colección? Crea un nuevo bloque con transacciones alteradas, calcula el hash y lo reemplaza por cualquier bloque más antiguo.

Necesitamos una forma de asegurarnos de que cualquier cambio en los bloques anteriores invalide toda la cadena. La forma que tiene Bitcoin de hacer esto es crear dependencia entre los bloques, encadenándolos con el hash del bloque inmediatamente anterior a ellos. Para encadenar, vamos a incluir el hash del bloque anterior en el bloque actual, almacenandolo en un nuevo campo llamado, por ejemplo previous_hash.

Bien, si cada bloque está vinculado al bloque anterior a través del campo previous_hash, ¿qué pasa con el primer bloque? Ese bloque se llama bloque génesis y se puede generar manualmente o mediante alguna lógica. Agreguemos el campo previous_hash a la clase Block e implementemos la estructura inicial de nuestra clase Blockchain.

from hashlib import sha256
import json
import time


class Block:
    def__init__(self, index, transactions, timestamp, previous_hash):
        """
        Constructor for the `Block` class.
        :param index:         Unique ID of the block.
        :param transactions:  List of transactions
        :param timestamp:     Time of generation of the block.
        :param previous_hash: Hash of the previous block in the chain which this block is part of.                                        
        """
        self.index = index
        self.transactions = transactions
        self.timestamp = timestamp
        self.previous_hash = previous_hash # Adding the previous hash field

    def compute_hash(self):
        """
        Returns the hash of the block instance by first converting it
        into JSON string.
        """
        block_string = json.dumps(self.__dict__, sort_keys=True) # The string equivalent also considers the previous_hash field now
        return sha256(block_string.encode()).hexdigest()

class Blockchain:

    def __init__(self):
        """
        Constructor for the `Blockchain` class.
        """
        self.chain = []
        self.create_genesis_block()

    def create_genesis_block(self):
        """
        A function to generate genesis block and appends it to
        the chain. The block has index 0, previous_hash as 0, and
        a valid hash.
        """
        genesis_block = Block(0, [], time.time(), "0")
        genesis_block.hash = genesis_block.compute_hash()
        self.chain.append(genesis_block)

    @property
    def last_block(self):
        """
        A quick pythonic way to retrieve the most recent block in the chain. Note that
        the chain will always consist of at least one block (i.e., genesis block)
        """
        return self.chain[-1]

Ahora, si cambia el contenido de cualquiera de los bloques anteriores:

  • El hash de ese bloque anterior cambiaría.
  • Esto conducirá a una discrepancia con el campo previous_hash en el siguiente bloque.
  • Dado que los datos de entrada para calcular el hash de cualquier bloque también consisten en el campo previous_hash, el hash del siguiente bloque también cambiará.

En última instancia, se invalida toda la cadena que sigue al bloque reemplazado y la única forma de solucionarlo es volver a calcular toda la cadena.

4. Implementar un algoritmo de prueba de trabajo


Sin embargo, sigue existiendo la posibilidad de que cambiemos el bloque anterior y los hashes de todos los bloques siguientes ya se pueden volver a calcular con bastante facilidad y crear una cadena de bloques válida diferente. Para evitar esto, podemos aprovechar la asimetría en los esfuerzos de las funciones hash para hacer que la tarea de calcular el hash sea difícil y aleatoria. Así es como hacemos esto: en lugar de aceptar cualquier hash para el bloque, le agregamos la restricción de que nuestro hash debe comenzar con “n ceros a la izquierda”, donde n puede ser cualquier número entero positivo.

Sabemos que a menos que cambiemos los datos del bloque, el hash no cambiará y, por supuesto, no queremos cambiar los datos existentes. ¿Asi que que hacemos? ¡Sencillo! Agregaremos algunos datos ficticios que podemos cambiar. Introduzcamos un nuevo campo en nuestro bloque llamado nonce. Un nonce es un número que podemos seguir cambiando hasta que obtengamos un hash que satisfaga nuestra restricción. El nonce que satisface la restricción sirve como prueba de que se ha realizado algún cálculo. Esta técnica es una versión simplificada del algoritmo Hashcash utilizado en Bitcoin. El número de ceros especificado en la restricción determina la dificultad de nuestro algoritmo de prueba de trabajo (cuanto mayor sea el número de ceros, más difícil será descifrar el nonce).

Además, debido a la asimetría, la prueba de trabajo es difícil de calcular pero muy fácil de verificar una vez que averigua el nonce (solo tiene que ejecutar la función hash nuevamente):

class Blockchain:
    # difficulty of PoW algorithm
    difficulty = 2

    """
    Previous code contd..
    """

    def proof_of_work(self, block):
        """
        Function that tries different values of the nonce to get a hash
        that satisfies our difficulty criteria.
        """
        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

Hay que tener en cuenta que no existe una lógica específica para descubrir el nonce rápidamente; es solo fuerza bruta. La única manera de acelerar el calculo es utilizar chips que estén especialmente diseñados para calcular la función hash en un número menor de instrucciones de CPU.

5. Agregar bloques a la cadena

Para agregar un bloque a la cadena, primero tendremos que verificar que:

  • Los datos no han sido alterados (la prueba de trabajo proporcionada es correcta).
  • El orden de las transacciones se conserva (el campo previous_hash del bloque que se agregará apunta al hash del último bloque de nuestra cadena).

Veamos el código para agregar bloques a la cadena:

class Blockchain:
    """
    Previous code contd..
    """

    def add_block(self, block, proof):
        """
        A function that adds the block to the chain after verification.
        Verification includes:
        * Checking if the proof is valid.
        * The previous_hash referred in the block and the hash of a latest block
          in the chain match.
        """
        previous_hash = self.last_block.hash

        if previous_hash != block.previous_hash:
            return False

        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):
        """
        Check if block_hash is valid hash of block and satisfies
        the difficulty criteria.
        """
        return (block_hash.startswith('0' * Blockchain.difficulty) and
                block_hash == block.compute_hash())

Las transacciones se almacenarán inicialmente como un grupo de transacciones no confirmadas. El proceso de poner las transacciones no confirmadas en un bloque y calcular la prueba de trabajo se conoce como minado de bloques. Una vez que se determina el nonce que satisface nuestras restricciones, podemos decir que se ha minado un bloque y se puede poner en la cadena de bloques.

En la mayoría de las criptomonedas (incluido Bitcoin), los mineros pueden recibir alguna criptomoneda como recompensa por gastar su poder de cómputo para calcular una prueba de trabajo. Así es como se ve nuestra función de minería:

class Blockchain:

    def __init__(self):
        self.unconfirmed_transactions = [] # data yet to get into blockchain
        self.chain = []
        self.create_genesis_block()

    """
    Previous code contd...
    """

    def add_new_transaction(self, transaction):
        self.unconfirmed_transactions.append(transaction)

    def mine(self):
        """
        This function serves as an interface to add the pending
        transactions to the blockchain by adding them to the block
        and figuring out proof of work.
        """
        if not self.unconfirmed_transactions:
            return False

        last_block = self.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)
        self.add_block(new_block, proof)
        self.unconfirmed_transactions = []
        return new_block.index

Podeis ver el codigo entero en mi GitHub

6. Crear interfaces

Bien, ahora es el momento de crear interfaces para que nuestro nodo blockchain interactúe con la aplicación que vamos a construir. Usaremos un micro-framework de Python popular llamado Flask para crear una API REST que interactúa e invoca varias operaciones en nuestro nodo blockchain. Si has trabajado con algún framework web anteriormente, el siguiente código no debería ser difícil de seguir.

from flask import Flask, request
import requests

# Initialize flask application
app =  Flask(__name__)

# Initialize a blockchain object.
blockchain = Blockchain()

Necesitamos un “end point” para que nuestra aplicación envíe una nueva transacción. Esto será utilizado por nuestra aplicación para agregar nuevos datos (transacciones) a la cadena de bloques:

# Flask's way of declaring end-points
@app.route('/new_transaction', methods=['POST'])
def new_transaction():
    tx_data = request.get_json()
    required_fields = ["author", "content"]

    for field in required_fields:
        if not tx_data.get(field):
            return "Invalid transaction data", 404

    tx_data["timestamp"] = time.time()

    blockchain.add_new_transaction(tx_data)

    return "Success", 201

Aquí hay un “end point” para devolver la copia de la cadena.

@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})

Aquí se solicita al nodo que extraiga las transacciones no confirmadas (si las hay). Lo usaremos para iniciar un comando para minar desde nuestra propia aplicación:

@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
    result = blockchain.mine()
    if not result:
        return "No transactions to mine"
    return "Block #{} is mined.".format(result)

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

7. Establecer consenso y descentralización

Hasta este punto, la cadena de bloques que hemos implementado está destinada a ejecutarse en una sola computadora. A pesar de que estamos vinculando bloques con hashes y aplicando la prueba de restricción de trabajo, todavía no podemos confiar en una sola entidad (en nuestro caso, una sola máquina). Necesitamos que los datos se distribuyan, necesitamos múltiples nodos que mantengan la cadena de bloques. Por lo tanto, para realizar la transición de un solo nodo a una red de igual a igual, primero creemos un mecanismo para permitir que un nuevo nodo conozca a otros pares de la red:

# Contains the host addresses of other participating members of the network
peers = set()

# Endpoint to add new peers to the network
@app.route('/register_node', methods=['POST'])
def register_new_peers():
    # The host address to the peer node 
    node_address = request.get_json()["node_address"]
    if not node_address:
        return "Invalid data", 400

    # Add the node to the peer list
    peers.add(node_address)

    # Return the blockchain to the newly registered node so that it can sync
    return get_chain()


@app.route('/register_with', methods=['POST'])
def register_with_existing_node():
    """
    Internally calls the `register_node` endpoint to
    register current node with the remote node specified in the
    request, and sync the blockchain as well with the remote 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"}

    # Make a request to register with remote node and obtain information
    response = requests.post(node_address + "/register_node",
                             data=json.dumps(data), headers=headers)

    if response.status_code == 200:
        global blockchain
        global peers
        # update chain and the peers
        chain_dump = response.json()['chain']
        blockchain = create_chain_from_dump(chain_dump)
        peers.update(response.json()['peers'])
        return "Registration successful", 200
    else:
        # if something goes wrong, pass it on to the API response
        return response.content, response.status_code


def create_chain_from_dump(chain_dump):
    blockchain = Blockchain()
    for idx, block_data in enumerate(chain_dump):
        block = Block(block_data["index"],
                      block_data["transactions"],
                      block_data["timestamp"],
                      block_data["previous_hash"])
        proof = block_data['hash']
        if idx > 0:
            added = blockchain.add_block(block, proof)
            if not added:
                raise Exception("The chain dump is tampered!!")
        else:  # the block is a genesis block, no verification needed
            blockchain.chain.append(block)
    return blockchain

Un nuevo nodo que participa en la red puede invocar el método register_with_existing_node (a través del / register_with endpoint) para registrarse con los nodos existentes en la red. Esto ayudará con lo siguiente:

  • Solicitar al nodo remoto que agregue un nuevo par a su lista de pares conocidos.
  • Inicializando la cadena de bloques del nuevo nodo con la del nodo remoto.
  • Volver a sincronizar la cadena de bloques con la red si el nodo se desconecta.

De todas formas, existe un problema con varios nodos dado que la copia de cadenas de algunos nodos puede diferir (por ejemplo por latencia de la red). En ese caso, los nodos deben acordar cuál de las versiones de la cadena es la válida (debemos lograr un consenso).

Un algoritmo de consenso podría ser establecer que la cadena válida es la más larga.

class Blockchain
    """
    previous code continued...
    """
    def check_chain_validity(cls, chain):
        """
        A helper method to check if the entire blockchain is valid.            
        """
        result = True
        previous_hash = "0"

        # Iterate through every block
        for block in chain:
            block_hash = block.hash
            # remove the hash field to recompute the hash again
            # using `compute_hash` method.
            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():
    """
    Our simple consensus algorithm. If a longer valid chain is
    found, our chain is replaced with it.
    """
    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):
              # Longer valid chain found!
            current_len = length
            longest_chain = chain

    if longest_chain:
        blockchain = longest_chain
        return True

    return False

A continuación, necesitamos desarrollar una forma para que cualquier nodo anuncie a la red que ha extraído un bloque para que todos puedan actualizar su blockchain. Los otros nodos pueden verificar la prueba de trabajo y agregar el bloque minado a sus respectivas cadenas:

# endpoint to add a block mined by someone else to
# the node's chain. The node first verifies the block
# and then adds it to the chain.
@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 "The block was discarded by the node", 400

    return "Block added to the chain", 201


def announce_new_block(block):
    """
    A function to announce to the network once a block has been mined.
    Other blocks can simply verify the proof of work and add it to their
    respective chains.
    """
    for peer in peers:
        url = "{}add_block".format(peer)
        requests.post(url, data=json.dumps(block.__dict__, sort_keys=True))

El método announce_new_block debe llamarse después de que el nodo extraiga cada bloque para que los pares puedan agregarlo a sus cadenas.

@app.route('/mine', methods=['GET'])
def mine_unconfirmed_transactions():
    result = blockchain.mine()
    if not result:
        return "No transactions to mine"
    else:
        # Making sure we have the longest chain before announcing to the network
        chain_length = len(blockchain.chain)
        consensus()
        if chain_length == len(blockchain.chain):
            # announce the recently mined block to the network
            announce_new_block(blockchain.last_block)
        return "Block #{} is mined.".format(blockchain.last_block.index

8. Crear la aplicación

Ahora es el momento de empezar a trabajar en la interfaz de nuestra aplicación.

La aplicación necesita conectarse a un nodo en la red blockchain para obtener y enviar los datos (puede haber varios nodos).

import datetime
import json

import requests
from flask import render_template, redirect, request

from app import app

# Node in the blockchain network that our application will communicate with
# to fetch and add data.
CONNECTED_NODE_ADDRESS = "http://127.0.0.1:8000"

posts = []

La función fetch_posts obtiene los datos del punto final / cadena del nodo, analiza los datos y los almacena en local.

def fetch_posts():
    """
    Function to fetch the chain from a blockchain node, parse the
    data, and store it locally.
    """
    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)

La aplicación tiene un formulario HTML para la identificación de usuario y luego realiza una solicitud POST a un nodo conectado para agregar la transacción al grupo de transacciones no confirmadas. Luego, la red extrae la transacción y finalmente se recupera una vez que actualizamos nuestra página web:

@app.route('/submit', methods=['POST'])
def submit_textarea():
    """
    Endpoint to create a new transaction via our application
    """
    post_content = request.form["content"]
    author = request.form["author"]

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

    # Submit a transaction
    new_tx_address = "{}/new_transaction".format(CONNECTED_NODE_ADDRESS)

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

    # Return to the homepage
    return redirect('/')

Conclusión

Con esto deberíamos ser capaces de levantar una cadena de bloques desde 0 y crear una aplicación simple que permita a los usuarios compartir información en la cadena de bloques. Obviamente no es tan sofisticada/completa como Bitcoin o Etherum pero espero que sirva para entender el funcionamiento básico y responder gran parte de las preguntas que pueden surgir a perfiles técnicos sobre el funcionamiento interno de estas.

Fuente: https://developer.ibm.com/technologies/blockchain/tutorials/develop-a-blockchain-application-from-scratch-in-python/

Leave a comment

Your email address will not be published. Required fields are marked *