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.

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 este tutorial, 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

  • Un conocimiento básico de programación en Python [véase el tutorial de Python].
  • El microframework Flask (para crear puntos de acceso para el servidor blockchain).
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 (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 encadenados usando hashes. 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, que produce algunas características altamente deseables:

  • Inmutabilidad de la historia.
  • Invulnerabilidad del sistema.
  • Persistencia de la información.
  • No hay un solo punto de falla.

Ahora bien, ¿cómo es que blockchain obtiene 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.

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 tiempo en el que el contenido fue creado"
}

El término genérico “información” es generlamente 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. Además, un bloque puede contener una o más transacciones. Los bloques que contienen las transacciones son generados frecuentemente y añadidos al blockchain. Dado que puede haber múltiples bloques, cada uno tendría que tener un identificador único:

class Block:
    def __init__(self, index, transactions, timestamp):
        self.index = [] 
        self.transactions = transactions 
        self.timestamp = timestamp

2. Hacer los bloques inmutables

Nos gustaría detectar cualquier tipo de manipulación en la información almacenada dentro de un bloque. En blockchain, esto se hace usando una función hash.

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, que generalmente sirve para identificar la entrada [input]. Aquí hay un ejemplo en Python usando la función hash sha256.

>>> 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'

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

  • Debería ser computacionalmente fácil de calcular.
  • Incluso el cambio de un simple bit de la información debería cambiar por completo el hash.
  • No debería ser posible adivinar la información de entrada a partir del hash de salida.

Ahora sabes qué es una función hash. 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 digital de la información que contiene:

from hashlib import sha256
import json
 
def compute_hash(block): 
     """ 
    Una función que crea el hash del bloque.
    """ 
    block_string = json.dumps(self.__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 [array]). Pero esto no es suficiente, porque ¿qué sucedería 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 una gran tarea en nuestra implementación actual, porque mantendremos la inmutabilidad y el orden de los bloques.

Necesitamos una solución para asegurarnos que cualquier cambio en los bloques anteriores invalide la cadena entera. Una forma de hacer esto es encadenar los bloques por su hash. Al encadenarlos de esta forma, queremos decir incluir el hash del bloque anterior en el actual. Así, si el contenido de cualquiera de los bloques anteriores cambia, el hash del bloque va a cambiar, llevando a una discrepancia con el campo previos_hash en el próximo bloque.

Perfecto, cada bloque está enlazado al anterior por el campo previous_hash, ¿pero qué sucede con el primer bloque de todos? El primer bloque es llamado el bloque génesis y es generado manualmente o por alguna lógica única, en la mayoría de los casos. 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 neustra clase Blockchain

from hashlib import sha256
import json
import time
class Block: 
    def__init__(self, index, transactions, timestamp, previous_hash): 
        self.index = index 
        self.transactions = transactions 
        self.timestamp = timestamp
        self.previous_hash = previous_hash 
 
    def compute_hash(self): 
        block_string = json.dumps(self.__dict__, sort_keys=True) 
        return sha256(block_string.encode()).hexdigest()

Y aquí está nuestra clase Blockchain:

class Blockchain:
 
    def __init__(self):
        self.unconfirmed_transactions = [] # información para insertar en el 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):
        return self.chain[-1]

4. Implementar un algoritmo de prueba de trabajo

Aunque 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 prevenir esto, tenemos que 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 agregaremos alguna condición. Agreguemos una condición que nuestro hash deba empezar con dos ceros. Además, sabemos que a menos que cambiemos el contenido del bloque, el hash no va a cambiar.

Por lo que vamos a introducir 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 condición. El número de ceros prefijados (el valor 2, en nuestro caso) decide la “dificultad” de nuestro algoritmo de prueba de trabajo. Además, notarás que nuestra 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.

5. Añadir bloques a la cadena

Para agregar un bloque a la cadena, primero tendremos que verificar si la prueba de trabajo es correcta y si el campo previous_hash del bloque a añadir luego es igual al hash del último bloque en 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.
        """
        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 son 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 es 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:
    """
    Código anterior...
    """
 
    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 nuestro nodo para interactuar con otros compañeros así como con la aplicación que vamos a crear. Estaremos utilizando Flask para crear una REST-API que interactúe con nuestro nodo. Aquí está el código para ello:

from flask import Flask, request
import requests
 
app =  Flask(__name__)
 
# la copia del nodo del blockchain
blockchain = Blockchain()

Necesitamos un punto de acceso para nuestra aplicación para enviar una nueva transacción. Éste será utilizado por nuestra aplicación para añadir nueva información (publicaciones) al blockchain:

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

@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)
 
 
# punto de acceso para obtener las transacciones
# no confirmadas
@app.route('/pending_tx')
def get_pending_tx():
    return json.dumps(blockchain.unconfirmed_transactions)
 
 
app.run(debug=True, port=8000)

Ahora, si te gustaría, puedes 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 código que hemos implementado hasta ahora está pensado para ser ejecutado en una sola computadora. Incluso aunque estamos enlazando bloques con hashes, todavía no podemos confiarnos de una única entidad. Necesitamos múltiples nodos para mantener nuestro blockchain. Entonces, creemos un punto de acceso para permitirle a un nodo tener conciencia de otros compañeros en la red:

# la dirección the otros miembros que participan en la red
peers = set()
 
# punto de acceso para añadir nuevos compañeros a la red.
@app.route('/add_nodes', methods=['POST'])
def register_new_peers():
    nodes = request.get_json()
    if not nodes:
        return "Invalid data", 400
    for node in nodes:
        peers.add(node)
 
    return "Success", 201

Te habrás dado cuenta que hay un problema con los múltiples nodos. Debido a la manipulación intencional o por razones inintencionales, 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. Necesitamos consensuar.

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:

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 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 también para enviar nueva. Puede haber múltiples nodos también:

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


CONNECTED_NODE_ADDRESS = "http://127.0.0.1:8000"
 
posts = []

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

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)

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.

Para ejecutar la aplicación:

  1. Inicia el servidor del nodo blockchain:
    python node_server.py

  2. Ejecuta la aplicación:
    python run_app.py

Deberías ver la aplicación corriendo en http://localhost:5000.

  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:

Verificar transacciones

Tal vez hayas notado una falla en la aplicación: cualquiera puede cambiar cualquier nombre y publicar cualquier contenido. 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 actúan como una firma digital. La clave pública solo puede decodificar el contenido encriptado por la correspondiente clave privada. Las transacciones serán verificadas usando la clave pública del autor antes de añadir cualquier bloque. De esta manera, sabremos exactamente quién escribió el mensaje.

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.

Acerca del autor

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



Deja un comentario