Desarrollar una aplicación «blockchain» desde cero en Python

Desarrollar una aplicación «blockchain» desde cero en Python

Traducido desde el inglés Develop a blockchain application from scratch in Python. Actualizado 01/2022.

Este tutorial introduce a desarrolladores de Python, de cualquier nivel de programación, al blockchain. Descubrirás exactamente qué es un blockchain implementando un blockchain público desde cero y creando una simple aplicación para desplegarlo.

Serás capaz de crear puntos de acceso para diferentes funciones del blockchain, tales como añadir una transacción, usando el microframework Flask, y luego ejecutar los programas en múltiples máquinas para crear una red descentralizada. También verás cómo desarrollar una interfaz de usuario simple que interactúe con el blockchain y almacene la información para cualquier uso, como pagos peer-to-peer, conversaciones o comercio electrónico.

Python es un lenguaje de programación fácil de entender, esa es la razón por la que lo he elegido para este tutorial. A medida que avances con él, implementarás un blockchain público y lo verás en acción. El código para una aplicación de ejemplo completa, escrita usando puro Python, está en GitHub.

Obtener el código

La lógica radica en el archivo views.py. Para realmente entender blockchain desde el principio, avancemos sobre él.

Requisitos

  • Conocimiento básico de programación en Python [véase el tutorial de Python].
  • Conocimiento de API REST.
  • Familiaridad con el microframework Flask (no es excluyente, pero ayuda).
Fundamentos

Blockchain público vs. privado

Una red blockchain pública, como la red Bitcoin, está completamente abierta al público, y cualquiera puede unirse y participar.

En la otra mano, negocios que requieren de mayor privacidad, seguridad y velocidad en las transacciones optan por una red blockchain privada, donde los participantes necesitan una invitación para unirse. Saber más.

En 2008, un artículo titulado Bitcoin: un sistema de dinero electrónico peer-to-peer fue publicado por un individuo (o tal vez un grupo) llamado Satoshi Nakamoto. El artículo combinaba técnicas criptográficas y una red peer-to-peer sin la necesidad de confiar en una autoridad centralizada (como los bancos) para realizar pagos de una persona a otra. Nació Bitcoin. Además de Bitcoin, el mismo artículo introdujo un sistema distribuido para almacenar información (ahora conocido popularmente como «blockchain»), que tiene un campo de aplicación mucho más amplio que únicamente pagos o criptomonedas.

Desde entonces, el interés por el blockchain ha estallado en prácticamente toda la industria. Blockchain es ahora la tecnología debajo de criptomonedas completamente digitales como Bitcoin, tecnologías de computación distribuida como Ethereum, y frameworks de código abierto como Hyperledger Fabric, sobre donde está montada la IBM Blockchain Platform.

¿Qué es «blockchain»?

Blockchain es una forma de almacenar información digital. La información puede ser literalmente cualquier cosa. Para Bitcoin, son transacciones (registros de transferencias de Bitcoins de una cuenta a otra), pero pueden incluso ser archivos; no importa. La información está almacenada en forma de bloques, que están enlazados (o encadenados) usando hashes criptográficos. De aquí el nombre «blockchain» [block = bloque, chain = cadena, «cadena de bloques»].

Toda la magia radica en el modo en que la información es añadida y almacenada en el blockchain. Esencialmente un blockchain es una lista enlazada que contiene información ordenada, con algunas restricciones como las siguientes:

  • Los bloques no pueden modificarse una vez añadidos.
  • Existen reglas específicas para añadir información.
  • Tiene una arquitectura distribuida.

Hacer cumplir estas restricciones produce algunas características altamente deseables:

  • Inmutabilidad y durabilidad de la información.
  • No hay un solo punto de control o falla.
  • Un registro verificable del orden en que se añadió la información.

Ahora bien, ¿cómo es que aquellas restricciones dan lugar a estas características? Nos iremos acercando a ello a medida que las vayamos implementando. Empecemos.

Acerca de la aplicación

Definamos qué es lo que hará la aplicación que estamos desarrollando. Nuestro objetivo es construir un sitio web simple que permita a los usuarios compartir información. Puesto que el contenido estará almacenado en el blockchain, será inmutable y permanente. Los usuarios podrán interactuar con la aplicación a través de una sencilla interfaz web.

Seguiremos un procedimiento de abajo hacia arriba para implementar las cosas. Empecemos por definir la estructura de la información que estaremos almacenando en el blockchain. Una publicación (un mensaje publicado por cualquier usuario en nuestra aplicación) será identificada por tres cosas esenciales:

  1. Contenido
  2. Autor
  3. Fecha de publicación [timestamp]

1. Almacenar transacciones en bloques

Estaremos almacenando información en nuestro blockchain en un formato que es ampliamente usado: JSON. Así es como se verá una publicación almacenada en el blockchain:

{
"author": "nombre_del_autor",
"content": "Algunos pensamientos que el autor quiere compartir",
"timestamp": "El momento en que el contenido fue creado"
}

El término genérico «información» es a menudo reemplazado en internet por el término «transacción». Por ende, para evitar confusiones y ser consistentes, estaremos empleando el término «transacción» para referirnos a información publicada en nuestra aplicación de ejemplo.

Las transacciones están empaquetadas en bloques. Un bloque puede contener una o más transacciones. Los bloques que contienen las transacciones son generados con frecuencia y añadidos al blockchain. Dado que puede haber múltiples bloques, cada uno debería tener un identificador único:

class Block:
    def __init__(self, index, transactions, timestamp):
        """
        Constructor de la clase `Block`.
        :param index: ID único del bloque.
        :param transactions: Lista de transacciones.
        :param timestamp: Momento en que el bloque fue generado.
        """
        self.index = index 
        self.transactions = transactions 
        self.timestamp = timestamp

2. Agregar firmas digitales a los bloques

Nos gustaría prevenir cualquier tipo de manipulación en la información almacenada dentro de un bloque, y la detección es el primer paso para eso. Para detectar si la información de un bloque ha sido manipulada, podemos usar funciones hash criptográficas.

Una función hash es una función que toma información de cualquier tamaño y a partir de ella produce otra información de un tamaño fijo (llamada hash), que generalmente sirve para identificar la información de entrada.

Las características de una función hash ideal son:

  • Debe ser computacionalmente fácil de calcular.
  • Debe ser determinista: la misma información siempre resulta en la generación del mismo hash.
  • Debe ser uniformemente aleatoria: incluso el cambio de un simple bit de la información de entrada debe cambiar por completo el hash.

Las consecuencias de esto son:

  • Es imposible adivinar la información de entrada a partir del hash de salida (la única forma es probar todas las combinaciones de información de entrada posibles).
  • Si se conoce tanto la información de entrada como el hash, simplemente se puede pasar la información de entrada por la función hash para verificar el hash en cuestión.

La asimetría entre los esfuerzos requeridos para averiguar el hash desde una información de entrada (algo muy fácil) y aquellos para averiguar la información de entrada a partir de un hash (casi imposible) es de lo que blockchain saca provecho para obtener las características deseadas.

Hay varias funciones hash populares. Aquí hay un ejemplo en Python usando la función hash SHA-256.

>>> from hashlib import sha256
>>> data = b"Un poco de informacion de longitud variada"
>>> sha256(data).hexdigest()
'976cb22d161e5bd6225b543c04743015daa8ee4fcbb01a5c489e33d01b2f951f'
>>> # No importa cuántas veces lo ejecutes, siempre retorna la misma cadena de 256 caracteres.
>>> sha256(data).hexdigest()
'976cb22d161e5bd6225b543c04743015daa8ee4fcbb01a5c489e33d01b2f951f'
>>> # Se agrega un carácter al final.
>>> data = b"Un poco de informacion de longitud variada2"
>>> sha256(data).hexdigest()
'd3b1df2ef471d726dc5521200338f5626ddbcccf8463c33709ab9ea04f18c7b9'
# ¡Nótese cómo el hash resultante cambió completamente!

Vamos a guardar el hash de cada uno de los bloques en un campo dentro de nuestro objeto Block para que actúe como una huella dactilar digital (o firma digital) de la información que contiene:

from hashlib import sha256
import json
 
def compute_hash(block):
    """
    Convierte el bloque en una cadena JSON y luego retorna el hash
    del mismo.
    """
    block_string = json.dumps(block.__dict__, sort_keys=True)
    return sha256(block_string.encode()).hexdigest()

Nota: En la mayoría de las criptomonedas, aun las transacciones individuales en el bloque están cifradas, para formar un árbol hash (también conocido como árbol de Merkle), y la raíz del árbol puede ser usada como el hash del bloque. No es una condición necesaria para el funcionamiento del blockchain, por lo que lo omitiremos para mantener las cosas ordenadas y simples.

3. Encadenar los bloques

Bien, hemos montado los bloques. El blockchain supuestamente tiene que ser una colección de bloques. Podemos almacenar todos los bloques en una lista de Python (el equivalente a un arreglo o array). Pero esto no es suficiente, pues ¿qué sucede si alguien intencionalmente reemplaza un bloque en la colección? Crear un nuevo bloque con transacciones alteradas, calcular el hash y reemplazarlo por cualquier otro bloque anterior no es algo muy complejo en nuestra implementación actual.

Necesitamos una solución para asegurarnos de que cualquier cambio en los bloques anteriores invalide la cadena entera. La forma en que Bitcoin resuelve esto es creando una dependencia mutua entre bloques consecutivos al encadenarlos por el hash del bloque inmediatamente anterior a ellos. Con «encadenar» nos referimos a incluir el hash del bloque anterior en el actual, a través de un nuevo campo llamado previous_hash.

Pero si cada bloque está enlazado al anterior por el campo previous_hash, ¿qué sucede con el primer bloque de todos? El primer bloque es llamado el bloque génesis y puede ser generado manualmente o por alguna lógica única. Agreguemos el campo previous_hash a la clase Block e implementemos la estructura inicial de nuestra clase Blockchain (ver Listado 1).

Listado 1. 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 de la clase `Block`.
        :param index: ID único del bloque.
        :param transactions: Lista de transacciones.
        :param timestamp: Momento en que el bloque fue generado.
        """
        self.index = index
        self.transactions = transactions
        self.timestamp = timestamp
        # Agregamos un campo para el hash del bloque anterior.
        self.previous_hash = previous_hash
 
    def compute_hash(self):
        """
        Convierte el bloque en una cadena JSON y luego retorna el hash
        del mismo.
        """
        # La cadena equivalente también considera el nuevo campo previous_hash,
        # pues self.__dict__ devuelve todos los campos de la clase.
        block_string = json.dumps(self.__dict__, sort_keys=True)
        return sha256(block_string.encode()).hexdigest()


class Blockchain:
 
    def __init__(self):
        """
        Constructor para la clase `Blockchain`.
        """
        self.chain = []
        self.create_genesis_block()
 
    def create_genesis_block(self):
        """
        Una función para generar el bloque génesis y añadirlo a la
        cadena. El bloque tiene index 0, previous_hash 0 y un hash
        válido.
        """
        genesis_block = Block(0, [], time.time(), "0")
        genesis_block.hash = genesis_block.compute_hash()
        self.chain.append(genesis_block)
 
    @property
    def last_block(self):
        """
        Una forma rápida y pythonica de retornar el bloque más reciente de la cadena.
        Nótese que la cadena siempre contendrá al menos un último bloque (o sea,
        el bloque génesis).
        """
        return self.chain[-1]

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

  • El hash de ese bloque anterior cambiará.
  • Esto provocará una discordancia con el campo previous_hash en el bloque siguiente.
  • Puesto que la información de entrada para computar el hash de cualquier bloque también incluye el campo previous_hash, el hash del bloque siguiente cambiará también.

Al final, toda la cadena que sigue al bloque reemplazado será invalidada, y la única forma de arreglarlo es volver a computar toda la cadena.

4. Implementar un algoritmo de prueba de trabajo

Con todo, hay un problema. Si cambiamos el bloque anterior, podemos recalcular los hashes de todos los bloques subsiguientes sencillamente y crear un blockchain diferente pero válido. Para prevenirlo, ahora sacaremos provecho de la asimetría de esfuerzo de las funciones hash que mencionamos anteriormente para hacer que la tarea de calcular un hash sea difícil y aleatoria. Así es como se hace. En lugar de aceptar cualquier hash para el bloque, le agregamos alguna restricción. Incluyamos una restricción según la cual nuestro hash deba empezar con n ceros, donde n puede ser cualquier número positivo.

Sabemos que, a menos que cambiemos el contenido del bloque, el hash no va a cambiar y, por supuesto, no queremos cambiar la información que ya existe. ¿Entonces qué hacemos? ¡Fácil! Agregaremos algo de información ficticia que luego podamos cambiar. Introduzcamos un nuevo campo en nuestro bloque llamado nonce. Un nonce es un número que cambiará constantemente hasta que obtengamos un hash que satisfaga nuestra restricción. El número de ceros especificados en la restricción decide la «dificultad» de nuestro algoritmo de prueba de trabajo (cuanto más ceros, más difícil es averiguar el nonce).

Además, a causa de la asimetría, la prueba de trabajo es difícil de calcular pero fácil de verificar una vez que averiguamos el nonce (para verificar, simplemente tienes que ejecutar la función hash nuevamente).

class Blockchain:
    # Dificultad del algoritmo de prueba de trabajo.
    difficulty = 2
 
    """
    Código anterior...
    """
 
    def proof_of_work(self, block):
        """
        Función que intenta distintos valores de nonce hasta obtener
        un hash que satisfaga nuestro criterio de dificultad.
        """
        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

Nótese que no hay una lógica concreta para averiguar el nonce rápidamente; es simplemente fuerza bruta. La única mejora importante que puedes hacer es usar un equipo específicamente diseñado para computar funciones hash en un menor número de instrucciones de CPU.

5. Añadir bloques a la cadena

Antes de agregar un bloque a la cadena tendremos que verificar:

  1. que la información no ha sido adulterada (esto es, que la prueba de trabajo provista es correcta);
  2. que el orden de las transacciones se preserve (esto es, que el campo previous_hash del bloque a añadir sea igual al hash del último bloque de nuestra cadena).

Veamos el código para insertar bloques en la cadena:

class Blockchain:
    """
    Código anterior...
    """

    def add_block(self, block, proof):
        """
        Una función que agrega el bloque a la cadena luego de la verificación.
        La verificación incluye:
        * Chequear que la prueba es válida.
        * El valor del previous_hash del bloque coincide con el hash del último
          bloque de la cadena.
        """
        previous_hash = self.last_block.hash
 
        if previous_hash != block.previous_hash:
            return False
 
        if not self.is_valid_proof(block, proof):
            return False
 
        block.hash = proof
        self.chain.append(block)
        return True
 
    def is_valid_proof(self, block, block_hash):
        """
        Chquear si block_hash es un hash válido y satisface nuestro
        criterio de dificultad.
        """
        return (block_hash.startswith('0' * Blockchain.difficulty) and
                block_hash == block.compute_hash())

Minado

Las transacciones serán almacenadas inicialmente en un conjunto de transacciones no confirmadas. El proceso de poner transacciones no confirmadas en un bloque y calcular la prueba de trabajo es conocido como el minado [mining] de bloques. Una vez que el nonce que satisface nuestra condición es averiguado, podemos decir que el bloque ha sido minado y puede ser colocado en el blockchain.

En la mayoría de las criptomonedas (incluyendo Bitcoin), los mineros suelen ser
premiados con alguna criptomoneda como una recompensa por usar su potencia de cálculo para calcular la prueba de trabajo. Así es como se ve nuestra función de minado.

class Blockchain:
  
    def __init__(self):
        # Información que todavía no ha ingresado al blockchain.
        self.unconfirmed_transactions = []
        self.chain = []
        self.create_genesis_block()

    """
    Código anterior continuado...
    """
 
    def add_new_transaction(self, transaction):
        self.unconfirmed_transactions.append(transaction)
 
    def mine(self):
        """
        Esta función sirve como una interfaz para añadir las transacciones
        pendientes al blockchain añadiéndolas al bloque y calculando la
        prueba de trabajo.
        """
        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

Muy bien, ya casi estamos. Puedes ver el código completo hasta este momento en GitHub.

6. Crear interfaces

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

from flask import Flask, request
import requests
 
# Inicializar la aplicación Flask
app =  Flask(__name__)
 
# Inicializar el objeto blockchain.
blockchain = Blockchain()

Necesitamos un punto de acceso para que nuestra aplicación envíe una nueva transacción. Este será utilizado por nuestra aplicación para añadir nueva información (publicaciones) al blockchain:

# El método de Flask para declarar puntos de acceso.
@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 "Invlaid transaction data", 404
 
    tx_data["timestamp"] = time.time()
 
    blockchain.add_new_transaction(tx_data)
 
    return "Success", 201

Aquí hay un punto de acceso para retornar la copia del blockchain que tiene el nodo. Nuestra aplicación estará usando este punto de acceso para solicitar todas las publicaciones y luego las mostrará.

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

Y aquí hay otro para solicitar al nodo que mine las transacciones sin confirmar (si es que hay alguna). Lo estaremos usando para iniciar el proceso de minado desde nuestra misma 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)

Estos puntos de acceso REST pueden ser utilizados para jugar con nuestro blockchain creando algunas transacciones y luego minándolas usando una herramienta como cURL o Postman.

7. Establecer consenso y descentralización

El blockchain que hemos implementado hasta ahora está pensado para ser ejecutado en una sola computadora. Incluso aunque estamos enlazando bloques con hashes y aplicando la restricción de la prueba de trabajo, aún no podemos confiarnos de una única entidad (una única máquina en nuestro caso). Necesitamos múltiples nodos para mantener nuestro blockchain. Entonces, para pasar de un solo nodo a una red de pares [peer-to-peer] creemos un punto de acceso para permitirle a un nodo tener conciencia de otros compañeros en la red:

# Contiene las direcciones de otros compañeros que participan en la red.
peers = set()

# Punto de acceso para añadir nuevos compañeros a la red.
@app.route('/register_node', methods=['POST'])
def register_new_peers():
    # La dirección del nodo compañero.
    node_address = request.get_json()["node_address"]
    if not node_address:
        return "Invalid data", 400

    # Añadir el nodo a la lista de compañeros.
    peers.add(node_address)

    # Retornar el blockhain al nuevo nodo registrado para que pueda sincronizar.
    return get_chain()


@app.route('/register_with', methods=['POST'])
def register_with_existing_node():
    """
    Internamente llama al punto de acceso `/register_node`
    para registrar el nodo actual con el nodo remoto especificado
    en la petición, y sincronizar el blockchain asimismo con el
    nodo remoto. 
    """
    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"}

    # Hacer una petición para registrarse en el nodo remoto y obtener
    # información.
    response = requests.post(node_address + "/register_node",
                             data=json.dumps(data), headers=headers)

    if response.status_code == 200:
        global blockchain
        global peers
        # Actualizar la cadena y los compañeros.
        chain_dump = response.json()['chain']
        blockchain = create_chain_from_dump(chain_dump)
        peers.update(response.json()['peers'])
        return "Registration successful", 200
    else:
        # si algo sale mal, pasárselo a la respuesta de la API
        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:  # el bloque es un bloque génesis, no necesita verificación
            blockchain.chain.append(block)
    return blockchain

Un nuevo nodo que participe en la red puede invocar el método register_with_existing_node (vía el punto de acceso /register_with) para registrarse en los nodos existentes en la red. Esto facilitará lo siguiente:

  • Solicitarle al nodo remoto que añada un nuevo compañero a su lista de compañeros conocidos.
  • Iniciar el blockchain del nuevo nodo con el blockchain del nodo remoto.
  • Resincronizar el blockchain con la red si el nodo se desconecta de la misma.

Sin embargo, hay un problema con los múltiples nodos. Debido a la manipulación intencional o por razones no intencionales (como latencia en la red), la copia de cadenas de algunos nodos puede diferir. En ese caso, necesitamos ponernos de acuerdo respecto de alguna versión de la cadena para mantener la integridad de todo el sistema. En otras palabras, necesitamos conseguir «consenso».

Un algoritmo simple de consenso podría ser ponernos de acuerdo respecto de la cadena válida más larga cuando las cadenas de diferentes participantes de la red aparentan divergir. La racionalidad debajo de este procedimiento es que la cadena más larga es una buena estimación de la mayor cantidad de trabajo realizado (recuérdese que la prueba de trabajo es difícil de computar):

def consensus():
    """
    Nuestro simple algoritmo de consenso. Si una cadena válida más larga es
    encontrada, la nuestra es reemplazada por ella.
    """
    global blockchain
 
    longest_chain = None
    current_len = len(blockchain)
 
    for node in peers:
        response = requests.get('http://{}/chain'.format(node))
        length = response.json()['length']
        chain = response.json()['chain']
        if length > current_len and blockchain.check_chain_validity(chain):
            current_len = length
            longest_chain = chain
 
    if longest_chain:
        blockchain = longest_chain
        return True
 
    return False

Y ahora finalmente, necesitamos desarrollar una forma para que cada nodo pueda anunciar a la red que ha minado un bloque para que todos puedan actualizar su blockchain y seguir minando otras transacciones. Otros nodos pueden simplemente verificar la prueba de trabajo y añadirla a sus respectivas cadenas:

# punto de acceso para añadir un bloque minado por alguien más a la cadena del nodo.
@app.route('/add_block', methods=['POST'])
def validate_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):
    for peer in peers:
        url = "http://{}/add_block".format(peer)
        requests.post(url, data=json.dumps(block.__dict__, sort_keys=True))

El método announce_new_block debería ser llamado luego de que un bloque ha sido minado por el nodo, para que los compañeros lo puedan añadir a sus cadenas.

8. Crear la aplicación

Bien, el backend está listo. Puedes ver el código hasta este momento en GitHub.

Ahora es tiempo de empezar a trabajar en la interfaz de nuestra aplicación. Hemos usado plantillas Jinja2 para hacer las páginas de la web y un poco de CSS para que se vea agradable.

Nuestra aplicación necesita conectarse a un nodo en la red blockchain para obtener la información y asimismo para enviar nueva. También puede haber múltiples nodos:

import datetime
import json
 
import requests
from flask import render_template, redirect, request
 
from app import app


# Nodo de la red blockchain con el que nuestra aplicación
# se comunicará para obtener y enviar información
CONNECTED_NODE_ADDRESS = "http://127.0.0.1:8000"
 
posts = []

La función fetch_posts obtiene la información del punto de acceso /chain del nodo, la procesa y la almacena localmente.

def fetch_posts():
    """
    Función para obtener la cadena desde un nodo blockchain,
    procesar la información y almacenarla localmente.
    """
    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 tomar la entrada del usuario y luego realiza una petición POST a un nodo conectado para añadir la transacción al conjunto de transacciones sin confirmar. La transacción luego es minada por la red y finalmente será obtenida una vez que recarguemos nuestro sitio web:

@app.route('/submit', methods=['POST'])
def submit_textarea():
    """
    Punto de acceso para crear una nueva transacción vía nuestra
    aplicación.
    """
    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 redirect('/')

9. Ejecutar la aplicación

¡Está lista! Puedes encontrar el código final en GitHub.

Clona el proyecto:

$ git clone https://github.com/satwikkansal/python_blockchain_app.git

Instala las dependencias:

$ cd python_blockchain_app
$ pip install -r requirements.txt

Inicia un nodo blockchain servidor:

En Linux y Mac:

$ export FLASK_APP=node_server.py
$ flask run --port 8000

En Windows:

> set FLASK_APP=node_server.py
> flask run --port 8000

Ahora una instancia de nuestro nodo blockchain está iniciada y corriendo en el puerto 8000.

Ejecuta la aplicación en una terminal diferente:

$ python run_app.py

La aplicación debería estar iniciada y corriendo en http://localhost:5000.

He aquí algunas imágenes.

  1. Intenta enviando algo de información, y deberías ver algo como esto:

  2. Presiona el botón Request to mine, y deberías ver algo así:

  3. Presiona el botón Resync, y deberías ver la aplicación resincronizando con la cadena:

Correr la aplicación con múltiples nodos

Para probar la aplicación ejecutando múltiples nodos, utiliza el punto de acceso register_with/ para registrar un nuevo nodo en la red de pares existente.

He aquí un escenario simple que querrás probar:

# este ya está corriendo
$ flask run --port 8000 &
# ejecutar nuevos nodos
$ flask run --port 8001 &
$ flask run --port 8002 &

Puedes utilizar las siguientes peticiones de cURL para registrar nodos de los puertos 8001 y 8002 en el nodo que ya está corriendo en el 8000.

$ curl -X POST \
http://127.0.0.1:8001/register_with \
-H 'Content-Type: application/json' \
-d '{"node_address": "http://127.0.0.1:8000"}'

$ curl -X POST \
http://127.0.0.1:8002/register_with \
-H 'Content-Type: application/json' \
-d '{"node_address": "http://127.0.0.1:8000"}'

Esto hará que el nodo en el puerto 8000 esté al tanto de los nodos en los puertos 8001 y 8002 y viceversa. Los nuevos nodos también sincronizarán la cadena con el nodo existente de modo que puedan participar activamente en el proceso de minado.

Para actualizar el nodo con el que la aplicación frontend se sincroniza (que por defecto es localhost:8000), cambia el campo CONNECTED_NODE_ADDRESS en el archivo views.py.

Una vez que hagas todo esto, puedes ejecutar la aplicación (python run_app.py), crear transacciones (publicar mesajes a través de la interfaz web) y, una vez que mines las transacciones, todos los nodos en la red actualizarán la cadena. La cadena de nodos también puede ser inspeccionada invocando el punto de acceso /chain usando cURL o Postman.

$ curl -X GET http://localhost:8001/chain
$ curl -X GET http://localhost:8002/chain

Verificar transacciones

Tal vez hayas notado una falla en la aplicación: cualquiera puede cambiar cualquier nombre y publicar cualquier contenido. Además, la publicación es suceptible de ser adulterada en el momento en que se envía la transacción a la red blockchain. Una forma de resolver esto es creando los usuarios usando criptografía de clave pública-privada. Cada usuario nuevo necesita una clave pública (análoga a un nombre de usuario) y una clave privada para ser capaces de publicar en nuestra aplicación. Las claves son utilizadas para crear y verificar la firma digital. He aquí como funciona:

  1. Cada transacción nueva enviada (publicación enviada) se firma con la clave privada del usuario. Esta firma se agrega a la información de la transacción junto con la información del usuario.
  2. Durante la fase de verificación, mientras se minan las transacciones, podemos verificar que el supuesto dueño de la publicación sea el mismo que el especificado en la información de la transacción y también que el mensaje no haya sido modificado. Esto puede hacerse usando la firma y la clave pública del supuesto dueño de la publicación.

Conclusión

Este tutorial cubrió los fundamentos de un blockchain público. Si lo seguiste completamente, has implementado un blockchain desde cero y creado una simple aplicación permitiendo a los usuarios compartir información en él. Nuestra implementación no es tan sofisticada como otras blockchain públicas como Bitcoin o Ethereum (y todavía tiene algunas vulnerabilidades), pero si continúas haciendo las preguntas correctas según tus requerimientos, eventualmente llegarás ahí. El aspecto clave para notar es: ¡la mayoría del trabajo de diseño de una blockchain para tu propósito consiste nada más en amalgamar conceptos ya existentes de la ciencia de la computación!

Acerca del autor

Satwik Kansal es un desarrollador de software con experiencia en tecnología sistemas distribuidos y ciencia de datos. Conéctate con él en Twitter o LinkedIn o en su sitio web.

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

60 comentarios.

  1. Hola:
    Aún no llego a Flaks. Fui desarrollando el código y finalmente bajé el archivo de HitHub (https://github.com/satwikkansal/python_blockchain_app/blob/3d252de03586ebb96acb689842ca2d451c0eec47/node_server.py).
    El archivo node_server.py tiene métodos que son llamados por otros métodos, por lo que se debe contar con el código completo, ya que es complicado depurarlo por partes.
    Al final del archivo intenté instanciar las clases correspondientes. Lo primero es crear el bloque génesis, instanciando el método create_genesis_block() de la clase Blockchain.

    Mi_Bloque_0 = Blockchain().create_genesis_block()

    pero termino con Mi_Bloque_0 = none
    Al nodo, deberemos subir un objeto de clase block que deberemos encadenar al anterior con los métodos de la clase Blockchain(), pero no soy capaz de crear ese objeto (no puedo instanciarlo) no puedo instanciar ni el bloque génesis, por lo tanto no tengo donde registrar transacciones y no puedo validar un bloque para subirlo.

    Estoy bloqueado.

    Muchas gracias

    • Recursos Python says:

      Hola, Gustavo.

      No entiendo bien lo que querés hacer, pero el bloque génesis se crea automáticamente al crear una instancia de la clase Blockchain (nótese que se llama a create_genesis_block() en el método __init__()). Por lo tanto, es suficiente con hacer:

      blockchain = Blockchain()
      # Acá ya está el bloque génesis.
      print(blockchain.chain)

      Saludos

      • Gracias por tu respuesta Recursos Python.
        Entiendo que no me entendieras, pero me entendiste. (me diste la respuesta)
        Quería generar un bloque y me dices:

        blockchain = Blockchain()
        # Acá ya está el bloque génesis.
        print(self.blockchain.chain)

        Pero me dice:

        NameError: name ‘self’ is not defined

        En realidad, el segundo bloque se debería crear cuando el génesis alcance determinado tamaño (pero no veo nada parecido)
        Solo estoy queriendo ejecutar el código para poder interpretarlo.

        Nuevamente gracias

        .

  2. Luis Dominguez says:

    Muchas Gracias, al fin logré ejecutar el programa, primero cloné tal como indicas, luego instalé las dependencias. saludos cordiales.

  3. Luis Domínguez says:

    Estimado:

    Felicidades interesante tutorial.
    Logré correr el programa pero cuando hago click en el botón me sale el siguiente mensaje de error:

    Not Found
    The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

    los dos botones resntantes funcionan .Gracias de antemano

        • Luis Domínguez says:

          Gracias por la pronta respuesta:
          Este es el c{odigo de la función:

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

          Es la versión que baje del repositorio GitHub, ojalá puedas darme el feedback para solucionar en problema. Gracias, saludos.

          • Recursos Python says:

            El código está bien. ¿Bajaste el código completo desde GitHub? Al presionar el botón, ¿se abre en una nueva pestaña la URL http://127.0.0.1:8000/mine? Si seguís con problemas te invito a que crees un tema en el foro para verlo con más detalle.

            Saludos

  4. Hola, como estás?
    Tengo una consulta: estoy con Postman intentando hacer un post request a new_transaction con los valores de author y content, pero me da error.

    El error es el siguiente:
    C:\Users\White\Documents\Python\Blockchain\Tutorial blockchain\node.py», line 109, in new_transaction
    if not tx_data.get(field):
    AttributeError: ‘NoneType’ object has no attribute ‘get’

    Entiendo que el error indica que no se recibieron datos en el request, por lo tanto es None y arroja error.
    Lo que no entiendo es por qué o como hacer para que reconozca el author y content

    Muchas gracias por el tutorial!

  5. Hola,

    Hay una cosa que no acabo de entender. Si lanzo varios nodos a la vez tal y como se explica el github, ¿sólo mina el nodo central? ¿Cómo se decide que nodo es el que mina? Me parece muy extraña esa gestión y no lo he terminado de entender. Además, si el nodo central muere, ¿la aplicación deja de funcionar? ¿Cómo haríamos para que la app funciones solo con que un nodo esté conectado?
    Gracias por el impresionante trabajo!

    • Recursos Python says:

      Hola, Javier.

      El nodo que va a minar la transacción es el definido en CONNECTED_NODE_ADDRESS. Si hay varios nodos corriendo, simplemente se puede implementar CONNECTED_NODE_ADDRESS como una lista y decidir en el momento a cuál conectarse para realizar la transacción. Habría que implementar también el criterio según el cual se decidirá a qué nodo conectarse. Para ello se podría agregar otro endpoint a los nodos que indique si el nodo está disponible para minar una transacción o si está ocupado. Así, la aplicación podría consultar a cada nodo antes de solicitarle un minado.

      Saludos

      • Perfecto eso sí tiene mucho sentido. Voy a intentar implementarlo. Entonces si no tengo mal entendido, en Bitcoin, ¿es el nodo que consigue el nounce el que se lleva la comisión de la transacción?

        Voy a intentar hacer algo similar.
        Muchas gracias!!

  6. Hola. Me parece bueno este tutorial, me lo encontré por casualidad buscando información de como aprender python para hacer blockchain. Respecto a esto mi pregunta es la siguiente: ¿Con este código puedo implementar el uso de una criptomoneda nueva si tuviera las posibilidades económicas?, Es decir, ¿Si quiero crear algo como bitcoin, con esto lo puedo hacer? Pero que no tenga nada que ver con el bitcoin ni ethereum ni otra dependecia de criptomonedas, no se si me expliqué. Y gracias, de antemano pues dio que soy completamente ignorante del tema y por ello me surge la pregunta.

    • Recursos Python says:

      Hola, Luis. Sí, podrías adaptar este código para crear una criptomoneda. Y digo «adaptar» porque recordá que blockchain es una tecnología para almacenar información de forma distribuida, independientemente de si esa información es un conjunto de transacciones de una moneda o, como en este artículo, un foro de mensajes.

      Saludos

  7. Felicitaciones gran artículo, tengo una duda , si cada usuario creado y que realicé una transacción, también mina,? En este caso llegado un gran volumen de transacciones para minar, podría darse problemas con el hardware que cada usuario pueda tener? Es así??? Recién estoy empezando a programar y me gustaría hacer algún proyecto con blockchain, muchas gracias

    • Recursos Python says:

      ¡Hola! No, los usuarios del sitio creado en este artículo no necesariamente son los mismos que minan las transacciones. Solo quienes ejecuten el node_server.py estarán minando, mientras que otras personas pueden visitar el sitio como cualquier otra web que no esté basada en blockchain.

      Saludos

  8. Estimados, está muy claro el tutorial y me funcionó en su totalidad, lo que no termino de entender es porque indican que la blockchain no se almacena en ningún lado, tengo entendido que, para minar, por ejemplo en la blockchain de Bitcoin tengo que «bajarla» y se estima que pesa unos 350 GB, lo cual es imposible de mantener en memoria, de ahí mi consulta,
    Supongamos que hay una tormenta solar o lo que fuere y el planeta entero queda sin electricidad un par de días, entonces, se perderían todas las blockchains? Ocurriría como lo que sucede con la blockchain de este tutorial, que, cuando cancelo la ejecución y vuelvo a ejecutar, se perdieron las transacciones realizadas?

    Gracias por vuestro tiempo y amabilidad

    • Recursos Python says:

      Hola, Sergio. El código de este artículo almacena la cadena de bloques en memoria (es el objeto blockchain). Para evitar que se pierda al cerrar el programa, podés almacenarlo físicamente en el disco en el formato que quieras. Así, si ocurriera el fenómeno extraordinario de que el planeta entero se quede sin electricidad, simplemente durante ese período no habría posibilidad de minar transacciones, pero la cadena de bloques (o sea, la información de todas las transacciones) no se pierde.

      Saludos

  9. Muchas gracias por el artículo. Explica muchas cosas que me costaba entender.
    Pero sigo teniendo dudas con el «servidor». Su papel y su sitio
    ¿Qué deben tener instalado los usuarios que usan la aplicación? Si es que hay algo más que el navegador.

    • Recursos Python says:

      Hola, Luis. No tienen que tener instalado nada, solo el navegador. El servidor corre en tu computadora local durante el desarrollo. Cuando se trata de un proyecto real, para correr el servidor se compra una máquina virtual que se conoce como hosting.

      Saludos

      • Hola, muchas gracias por la respuesta.
        Entonces lo había entendido bien. Hay que montar un servidor al fin y al cabo, central. Por lo tanto no estamos hablando de una aplicación descentralizada.
        ¿Se podría decir que es pseudo blockchain o semi blockchain?

        • Recursos Python says:

          Hola, Luis.

          Es un blockchain con todas las letras. Únicamente si ejecutás un solo servidor de nodos será centralizado: si se apaga ese nodo, se cae la red. La idea es ejecutar múltiples nodos en distintos servidores. Cuantos más servidores, más robusta y descentralizada es la red. Si se caen todos los mineros de Bitcoin, se destruye la red (pero solo mientras están caidos, luego pueden volver a conectarse y la información no se pierde). Pero es algo muy improbable, ya que son millones de nodos.

          Saludos

  10. Hola.

    1) Inicie el servidor del nodo blockchain:
    Luego,
    2)Ejecuta la aplicación:

    y cuando voy a ver la aplicación corriendo en http://localhost:5000, me arroja el mensaje»request.exceptions.ConnectionError». Sabes por que me aparece ese error? Que deberia hacer para poder solucionarlo? Yo hace poco que empece a estudiar Python, y no puedo encontrar el problema.
    Saludos

  11. ultima cosa
    esto va dentro de class block o fuera , a si y hay que eliminar los»>>>»?

    >>> from hashlib import sha256
    >>> data = «Alguna informacion de longitud variada».encode(«utf-8»)
    >>> sha256(data).hexdigest()
    ‘7c79eb1f1853630a00ea195b8f2979f16c1ea8016d9e487e1099537c02f513d9’
    >>> sha256(data).hexdigest() # No importa cuántas veces lo ejecutes, siempre retorna lo mismo.
    ‘7c79eb1f1853630a00ea195b8f2979f16c1ea8016d9e487e1099537c02f513d9’

  12. una pregunta, soy bastante novato y :
    primer:
    como cambio esto
    app.run(…, port=nuevo_puerto). y esto para q8ue sirve

    segundo:
    como ejecuto algo con esto, es decir done me tengo que ir y que tengi que hacer
    run_app.py.

  13. Hola, genesis_block.hash = genesis_block.compute_hash() no entiendo esta línea por el » hash» a secas sin nada en la declaración de genesis_block.hash ?es un método hash() y va a secas…

    Saludos

    • Recursos Python says:

      Hola. En Python no es necesario declarar los atributos de una clase. hash es un atributo de genesis_block que se crea en el momento en que se le asigna un valor.

      Saludos!

  14. Hola muy buenas, hay algún ejemplo donde este explicado como crear los usuarios con las claves publicas-privadas que pueda asociarse a lo ya descrito? muchas gracias genial el post!

  15. Carlos Gonzalez says:

    hola buenas tardes, me gustaría saber si eso mismo lo puedo aplicar en una gestión de automóviles, con recursos mixtos para multas, así como control de cada vehículo

    • Recursos Python says:

      Hola Carlos. Es muy amplia tu pregunta como para darte una respuesta detallada, pero en principio sí, lo podés aplicar en lo que quieras.

      Saludos

  16. Te hago una consulta, soy totalmente novato en el tema de blockchain, y queria validar un punto. Se utiliza alguna base de datos en los nodos para guardar la información? O es unicamente informacion en memoria? En el caso que se use, cual/cuales son recomendadas? Gracias!

    • Recursos Python says:

      Hola Fabian. No se usa ninguna base de datos, porque en ese caso la información estaría centralizada en un solo lugar, lo cual es contrario a la filosofía del blockchain y el peer-to-peer.

      Saludos!

      • Entiendo lo de descentralización, pero imaginaba que cada nodo tenia su base de datos con los bloques aprobados y los pendientes. Entonces la información queda en memoria unicamente? Desconozco python, pero entonces si reinicio el server se pierde la información del nodo? (igualmente entiendo que si esto pasa la información estaria en otro nodo con lo que se restauraria). Aprovecho para hacer otra consulta, tiene que haber un lugar central donde se administren los nodos y se asigne el nodo al cual se va a enviar la informacion? Sino como funciona la asignacion del CONNECTED_NODE_ADDRESS?

  17. Hola que tal, alguien tiene idea en donde puedo conseguir mayor información sobre este tema, sobre todo que sea información practica y no tan teórica? Gracias por el post, muy útil.

      • Hola, es posible crear una aplicación de intercambio de dinero tipo binance o coinbase , pero de no esa magnitud? Quiero crear una aplicación solo para usuarios médicos de mi ciudad , que puedan cambiar su dinero que reciben a Btc , eth, etc…

Deja una respuesta