Guia para construir una blockchain. Parte 13

En este post, examinaremos la topología de las redes peer-to-peer y contrastaremos esto con el modelo cliente-servidor, que es el modelo dominante de computación en Internet. Después de esto, configuraremos una red peer-to-peer de Blancoin en la dirección de loopback 127.0.x.x. Esta red es útil para probar blancoin. En el post anterior, creamos la mayor parte de la funcionalidad de un nodo de minería de blancoin. Sin embargo, no exponemos los nodos de minería a la red de blancoin ni indicamos cómo
puede hacerse. Hay una serie de preguntas pendientes sobre los nodos de minería.
¿Cómo recibe las transacciones un nodo de minería? ¿Cómo maneja un nodo los bloques que son minados por otros nodos? ¿Cómo informa un nodo de minería a otros nodos que acaba de minar un bloque? ¿Cómo obtiene una copia un nuevo nodo de minería que no tiene una cadena de bloques de la cadena de bloques distribuida? En este post, responderemos estas preguntas mediante la construcción de una interfaz API que habilita a los nodos de la red peer-to-peer para interoperar entre sí. Para seguir este post, deberá estar familiarizado con Python y la programación concurrente , así como JSON-RPC.

el modelo cliente servidor

El modelo cliente-servidor es la topología de red dominante en Internet. En este modelo topológico, los nodos en Internet son servidores o clientes. Un cliente que quiere una tarea realizada se pone en contacto con un servidor y realiza una solicitud. El servidor puede optar por responder a la solicitud o ignórarla. Algunos de los servidores también pueden ser clientes atendidos por otros servidores.
Esta topología se puede ilustrar como se muestra eabajo:

Topologia cliente-servidor

Este diagrama muestra a los clientes interactuando con los servidores. Las líneas representan lineas bidireccionales de comunicación entre un cliente y un servidor (solicitud-respuesta). Los clientes hacen las solicitudes y los servidores responden a estas solicitudes. Por ejemplo, un navegador web (cliente) solicita una página web de un servidor, y el servidor responde enviando una página HTML al cliente. Para nuestros propósitos, solo necesitamos observar que existe un grado de centralización en esta topología. Hay dos tipos de nodos, servidores y clientes. Los servidores responden a las solicitudes y puede optar por ignorar algunas de las solicitudes que se realizan. Los servidores controlan el flujo de información en la red.

redes peer to peer

Una red blockchain o una red de criptomonedas para el caso es una red de pares de igual a igual. En una red peer-to-peer (P2P), todos los nodos de la red son iguales en capacidad y no hay supernodos. En una red P2P, los nodos son pares entre sí. En el contexto del blancoin, esto significa que normalmente todos los nodos mantienen una cadena de bloques completa y tienen la capacidad de minar bloques. Por supuesto, algunos nodos pueden optar por no participar en la minería, y otros nodos pueden, para sus propios fines, optan por mantener solo una parte de la cadena de bloques. Como hemos visto anteriormente, los nodos de blancoin no confían entre sí, pero la red puede converger hacia un consenso en cuanto al estado de la red, es decir, la cadena de bloques. Esta falta de centralización en la red es la razón por la que las redes blockchain son resistentes
y no se pueden censurar o destruir. Esto contrasta con las redes cliente-servidor, donde todo la red se puede degradar simplemente apagando los servidores. La figura de abajo muestra la topología bruta de una red blockchain.

Una red peer to peer

Este diagrama muestra una cantidad de nodos que son idénticos entre sí en términos
de funcionalidad, interactuando en una red. Las líneas representan canales bidireccionales de comunicación entre nodos pares. Este tipo de topología se denomina red de pares. Los nodos pueden entrar en el sistema y salir de él sin degradar la eficacia de la red. Además, los nodos pueden formar enlaces de comunicación con otros nodos de forma transitoria no permamente.

Los intentos anteriores de establecer redes de monedas alternativa se basaron en el el modelo cliente-servidor. Estos sistemas monetarios eran fáciles de identificar y cerrar.
Además, atrajeron procesos penales motivados, a menudo basados en dudosa jurisprudencia política. La fuerza de Bitcoin es que no se puede cerrar. Satoshi Nakamoto, consciente de la potencialidad del litigio penal, desapareció de la escena después de poner la red Bitcoin en funcionamiento en 2011.

Los nodos en una red P2P no tienen ID únicos a nivel mundial que distingan a uno nodo de otro. Todos los nodos son conceptualmente iguales entre sí. Los nodos pueden solo diferenciarse entre sí en función de sus direcciones IP. Uno de las consecuencias del anonimato de este nodo es que la red es vulnerable a los ataques Sybil. En un ataque Sybil, un nodo se hace pasar por otro nodo o varios otros nodos.

levantando un nodo

Cuando un nodo en una red peer-to-peer se inicia, se conecta a uno o más nodos seed y consulta estos nodos para las direcciones de otros nodos en la red. Un nodo seed actúa como un servidor de directorio que mantiene las direcciones de los nodos. Una consulta a un servidor de directorio permite que un nodo se una a la red P2P y se comunique con un subconjunto de nodos. Diferentes nodos pueden contactar con diferentes nodos seed durante la fase de arranque, y en consecuencia, cada uno de estos nodos probablemente tendrá una vista diferente de la red. Cada nodo de una red P2P mantiene una lista de direcciones de nodos que conoce y puede por lo tanto, también actuará como servidor de directorio, si así lo desea. Estos nodos pueden responder a una consulta y proporcionar direcciones de nodos proporcionando su lista de direcciones de nodo. Las tablas hash distribuidas (DHT) son la forma más general en que las direcciones de nodo se puede proporcionar en una red P2P. En tal red P2P, un subconjunto de los nodos en el
red, si no todos, mantienen una tabla hash distribuida de direcciones. Un DHT es un
almacén de pares valor-clave que asigna un espacio de claves a un espacio de direcciones. Cualquier nodo de la red puede optar por mantener una parte del DHT. Un nodo que se está iniciando inunda la red P2P que buscan nodos que mantengan una tabla hash distribuida de direcciones. Estos nodos son nodos seed en una red de tipo DHT. Para nuestros propósitos, simplemente designaremos uno o más nodos P2P para que actúen como servidores de directorio que responden a consultas de direcciones de nodo. Una implementación de DHT para la red de blancoin, aunque no es difícil, nos llevaria mucho tiempo.

Ver por ejemplo:

http://bnrg.cs.berkeley.edu/~adj/publications/paper-files/tapestry_jsac.pdf, https://github.com/haojin2/Chord-p2p-DHT, http://nms.lcs.mit.edu/papers/chord.pdf

Pastry: Scalable, Decentralized Object Location and Routing for Large-Scale Peer-
to-Peer Systems: http://www.cs.rice.edu/~druschel/publications/Pastry.pdf

El modelo de Napster. Napster fue un servicio de intercambio de archivos MP3 P2P y un pionero en el desarrollo de redes P2P. Dado que se basaba en un servidor de directorio centralizado, era vulnerable al cierre.

En la Figura de abajo, el nodo A solicita a un servidor de directorio (seed) que le proporcione una lista de direcciones de otros nodos de la red. El servidor de directorio responde con una lista de direcciones y también agrega la dirección del nodo de consulta a su lista de nodos. Los nodos proporcionados por el servidor de directorio se convierten en los vecinos del nodo solicitante.

Arrancando un nodo con un servidor de directorio en una red peer to peer

propagacion de datos en una red p2p

En las redes Bitcoin y Blancoin, las transacciones y los bloques recién extraídos se propagan en toda la red mediante un proceso llamado inundación(flooding). Cada nodo de la red escucha en un puerto específico para transacciones que ocurren en la red y para nuevos bloques que se han minado. Cuando un nodo recibe una nueva transacción o un nuevo bloque, procesa el nuevo bloque o transacción y también puede transmitirlo a otros nodos en su lista de direcciones.
La figura de abajo muestra una transacción que se propaga en una red.

Propagacion de transacciones en una red P2P

Este diagrama muestra una transacción, representada por cuatro discos apilados, siendo
propagado desde el nodo D. El nodo D consulta su lista de direcciones y entrega la transacción a los nodos A y C. El nodo C consulta su lista de direcciones y entrega la transacción a los nodos E y F. La propagación continúa como lo indican las flechas en el diagrama. Este proceso de inundación permite que las transacciones y los nuevos bloques se propaguen rápidamente a través de la red. Por supuesto, y como comentamos en el último post, los nodos validan los bloques y las transacciones antes de reenviar una transacción o un bloque recién extraído a un vecino. Es posible que le preocupe que las transacciones y los bloques se propaguen sin cesar en la red. Hay dos formas de asegurarse de que esto no ocurra. La forma convencional es incluir un parámetro de tiempo de vida (TTL) con cada transacción o bloque. Considera como ejemplo, una nueva transacción. Los metadatos de esta transacción tendrán un tiempo de vida (TTL), con un valor de 256, por ejemplo. Cuando un nodo recibe esta transacción, disminuye el TTL y luego retransmite la transacción. Si un nodo recibe una transacción con un TTL de 0, simplemente descarta la transacción. El segundo enfoque es la forma en que Bitcoin y Blancoin manejan el enrutamiento de las transacciones. Un nodo Bitcoin descarta la transacción entrante si el nodo tiene esta transacción en su caché de transacciones en espera de ser procesadas (en el mempool), o si la transacción ya está en la cadena de bloques, o si la transacción no es válida. De otra manera, el nodo retransmite la transacción a los nodos en su lista de direcciones.

Tenga en cuenta que el mecanismo de propagación de transacciones también proporciona a cada nodo que recibe una transacción la dirección del nodo que propaga la transacción. El nodo receptor puede agregar esta dirección a su lista de direcciones. Esta característica permite a los nodos receptores expandir su imagen de la red agregando direcciones previamente desconocidas a sus listas de direcciones. Debido al algoritmo de inundación, cada nodo normalmente tendrá un conjunto diferente de transacciones que está procesando, y en particular, un nodo puede no recibir algunas transacciones que se propagan a través de la red. Un nodo puede cesar por completo de recibir transacciones si hay una partición de red temporal o si el nodo se cae. Tenga en cuenta que los nodos pueden recibir transacciones en un orden diferente al momento en que
fueron creados.

minado concurrente

Como se indicó en el último post, algunas de las funciones del módulo mining están destinadas a operar simultáneamente con los servidores RPC. Para facilitar esto, agregue la palabra clave async a las siguientes dos funciones en el módulo hmining, make_candidate_block y mine_block por ejemplo:

async def mine_block(candidate_block: 'dictionary')
async def make_candidate_block()

Con este simple cambio, estas dos funciones se vuelven capaces de operar simultáneamente. La función start_mining es un hilo concurrente que inicia la minería de bloques. Agregue el siguiente bloque de código al final del módulo hmining:

#####################################
# start mining in a thread:
# $(virtual) python hmining.py
#####################################
if __name__ == "__main__":
#Create the Threads
t1 = threading.Thread(target=start_mining, args=(), daemon=True)
t1.start()
t1.join()

Al iniciar este módulo desde la línea de comando, se iniciará la extracción de bloques en un hilo.

simulando la red blancoin en localhost

En este post, simularemos y probaremos nodos de minería de blancoin en una red de pares alojada en una red localhost (127.0.x.x). Ya deberías haber creado un subdirectorio llamado simulated_network bajo la raíz de Blancoin. El directorio simulated_network debe estar en la ruta de su intérprete de Python. Cree un directorio llamado node_1 bajo el directorio simulated_network. A continuación, copie todos los subdirectorios en la raíz de Blancoin con la excepción del directorio unit_tests y el directorio simulated_network en el subdirectorio simulated_network /node_1.
Elimine todos los archivos del subdirectorio simulated_network /node_1/data. El árbol de directorios node_1 contiene todo el código Python necesario para un nodo que se ejecuta en la red de blancoin simulada. A continuación, cree un árbol de subdirectorio seed_1/hnetwork bajo el directorio simulated_network. Copie el siguiente código del programa en un archivo llamado directory_server.py y guárde este archivo en el subdirectorio seed_1/hnetwork. Este código especifica un servidor de directorio en
la red de blancoin. En esta etapa, hemos especificado un nodo seed y un nodo de minería de blancoin para nuestra red simulada. Ahora puede copiar y pegar el árbol del directorio node_1 en el directorio simulated_network tantas veces como desee y cambie el nombre de la raíz de estos subárboles como se desee. Esto le dará un subdirectorio para cada nodo blancoin en la red simulada. También puede especificar más de un nodo seed.

También podemos usar imágenes y contenedores de Docker para configurar la red de blancoin en localhost. Docker es una solución de cuasi-virtualización que crea aplicaciones en contenedores. Los contenedores se pueden implementar en varios hosts sin necesidad de canalizaciones de implementación complicadas o de cualquier cambio de código fuente en los contenedores.

Puede consultar el siguiente recurso de Docker para obtener más información: Ian Miell y Aidan Sayers, Docker en la práctica, Publicaciones Manning, 2016.

Veamos el código del módulo directory_server. Este código de programa está en seed_1/hnetwork:

'''
start a directory server on 127.0.0.69:8081
'''
import hmining
from tornado import ioloop, web
from jsonrpcserver import method, async_dispatch as dispatch
import ipaddress
import json
import threading
import pdb
import logging
# A Fake list of addresses for testing only
address_list = [ "127.0.0.10:8081", "127.0.0.11:8081", "127.0.0.12:8081",
"127.0.0.13:8081", \
"127.0.0.14:8081", "127.0.0.15:8081", "127.0.0.16:8081",
"127.0.0.17:8081", \
"127.0.0.18:8081", "127.0.0.19:8081", "127.0.0.20:8081" ]
logging.basicConfig(filename="debug.log",filemode="w", \
format='server: %(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG)
@method
async def get_address_list():
with hmining.semaphore:
ret = address_list
return ret
@method
async def register_address(address: "string"):
global address_list
try:
# validate IP address and port format: addr:port
if address.find(":") == -1: return "error-invalid address"
addr_list = address.split(":")
addr_list[1]=addr_list[1].strip()
if len(addr_list[0]) == 0 or len(addr_list[1]) == \
0: return "error-invalid address"
if int(addr_list[1]) <= 0 or int(addr_list[1]) >= \
65536: return "error-invalid address"
_ = ipaddress.ip_address(addr_list[0])
addr = addr_list[0] + ":" + addr_list[1]
if addr not in address_list:
with hmining.semaphore:
address_list.append(addr)
return address
except Exception as err:
logging.debug('directory server::register address - ' + str(err))
return "error-register address"
class MainHandler(web.RequestHandler):
async def post(self):
request = self.request.body.decode()
logging.debug('decoded server request = ' + str(request))
response = await dispatch(request)
print(response)
self.write(str(response))
app = web.Application([(r"/", MainHandler)])
# start the network interface for a node on the local loop: 127.0.0.69:8081
if __name__ == "__main__":
app.listen(address="127.0.0.69", port="8081")
logging.debug('server running')
print("directory server started at 127.0.0.69:8081")
ioloop.IOLoop.current().start()

Este script de Python iniciará un servidor de directorio JSON-RPC asíncrono en la red localhost en la dirección IP 127.0.0.69 y el puerto 8081.

Este servidor de directorio mantiene una lista llamada address_list de las direcciones IP y
puertos en los que están escuchando otros nodos de blancoin. Este servidor de directorio realiza dos tareas: proporciona las direcciones de los servidores RPC (nodos de minería de blancoin) en la red, cuando así se solicite, y registra las direcciones de los nodos del servidor RPC. Tenga en cuenta que el recurso compartido address_list está protegido de las carreras de datos a través del uso de semáforos. Para iniciar este servidor de directorio, acceda al directorio seed_1/hnetwork y ejecute el script directory-server.py en su entorno virtual Blancoin:

>(virtual) python directory_server.py

Cada nodo de Blancoin que no es un servidor de directorio mantiene un cliente JSON-RPC y un servidor JSON-RPC. Los clientes RPC realizan solicitudes a otros servidores RPC en la red P2P de Blancoin y procesar las respuestas que se reciben. Del mismo modo, los servidores RPC responden a solicitudes realizadas por otros clientes RPC en la red.
Para cada directorio de nodos (node_1, node_2 …) que haya creado (que no sea el
directorios seed), copie el siguiente código de programa en el archivo networknode.py y guarde este archivo en los respectivos subdirectorios node_x/hnetwork.
Echemos un vistazo a este código de programa:

'''
netnode: implementation of a synchronous RPC-Client node that makes remote
procedure calls to RPC servers using the JSON RPC protocol version 2, and
a JSON-RPC server that responds to requests from RPC client nodes.
'''
import blk_index as blkindex
import hblockchain
import hchaindb
import hmining
import networknode
from tornado import ioloop, web
from jsonrpcserver import method, async_dispatch as dispatch
from jsonrpcclient.clients.http_client import HTTPClient
import ipaddress
import threading
import json
import pdb
import logging
import os
import sys
logging.basicConfig(filename="debug.log",filemode="w", \
format='server: %(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG)
##################################
# JSON-RPC Client
##################################
def hclient(remote_server, json_rpc):
'''
sends a synchronous request to a remote RPC server
'''
try:
client = HTTPClient(remote_server)
response = client.send(json_rpc)
valstr = response.text
val = json.loads(valstr)
print("result: " + str(val["result"]))
print("id: " + str(val["id"]))
return valstr
except Exception as err:
logging.debug("node_client: " + str(err))
return '{"jsonrpc": "2.0", "result":"error", "id":"error"}'
#######################################
# JSON-RPC Server
#######################################
address_list = []
@method
async def receive_transaction(trx):
"""
receives a transaction that is propagating on the Helium network
"""
try:
hmining.semaphore.acquire()
ret = hmining.receive_transaction(trx)
hmining.semaphore.release()
if ret == False: return "error: invalid transaction"
return "ok"
except Exception as err:
return "error: " + err
@method
async def receive_block(block):
"""
receives a block that is propagating on the Helium network
"""
try:
ret = hmining.receive_block(block)
if ret == False: return "error: invalid block"
return "ok"
except Exception as err:
return "error: " + err
@method
async def get_block(height):
"""
returns the block with the given height or an error if the block
does not exist
"""
with hmining.semaphore:
if len(hblockchain.blockchain) == 0:
return ("error: empty blockchain")
if height < 0 or height > hblockchain.blockchain[-1]["height"]:
return "error-invalid block height"
block = json.dumps(hblockchain.blockchain[height])
return block
@method
async def get_blockchain_height():
"""
returns the height of the blockchain. Note that the first block has
height 0
"""
with hmining.semaphore:
if hblockchain.blockchain == []: return -1
height = hblockchain.blockchain[-1]["height"]
return height
@method
async def clear_blockchain():
"""
clears the primary and secondary blockchains.
"""
pdb.set_trace()
with hmining.semaphore:
hblockchain.blockchain.clear()
hblockchain.secondary_blockchain.clear()
return "ok"
class MainHandler(web.RequestHandler):
async def post(self):
request = self.request.body.decode()
logging.debug('decoded server request = ' + str(request))
response = await dispatch(request)
print(response)
self.write(str(response))
def startup():
'''
start node related systems
'''
try:
# remove any locks
os.system("rm -rf ../data/heliumdb/*")
os.system("rm -rf ../data/hblk_index/*")
# start the Chainstate Database
ret = hchaindb.open_hchainstate("../data/heliumdb")
if ret == False: return "error: failed to start Chainstate
database"
else: print("Chainstate Database running")
# start the LevelDB Database blk_index
ret = blkindex.open_blk_index("../data/hblk_index")
if ret == False: return "error: failed to start blk_index"
else: print("blkindex Database running")
except Exception:
return "error: failed to start Chainstate database"
return True
app = web.Application([(r"/", MainHandler)])
######################################################
# start the network interface for the server node on
# the localhost loop: 127.0.0.19:8081
#####################################################
if __name__ == "__main__":
app.listen(address="127.0.0.51", port="8081")
logging.debug('server node is running')
print("network node starting at 127.0.0.19:8081")
###############################
# start this node
###############################
if startup() != True:
logging.debug('server node resource failure')
print("stopping")
sys.exit()
logging.debug('server node is running')
print('server node is running')
################################
# start the event loop
################################
ioloop.IOLoop.current().start()

Esta base de código aprovecha nuestra implementación JSON-RPC. Tenga en cuenta que
este nodo en particular está escuchando en 27.0.019: 8081. Cada nodo de red en su red simulada debe escuchar en una dirección IP única (en localhost).
Una solicitud realizada por un cliente RPC tiene el siguiente formato sintáctico:

rpc = json.dumps({"jsonrpc":"2.0","method":"receive_
block","params":{"block": blocks[0]}, "id":21})
ret = networknode.hclient("http://127.0.0.51:8081", rpc)

El argumento de la función dump es la llamada a procedimiento remoto en formato JSON-RPC . El valor del segundo argumento es el método que se invocará en el servidor remoto (Receive_block). El valor del tercer argumento son los argumentos para pasar a la función remota. Los argumentos son un diccionario de Python. El tercer argumento será el diccionario vacío, {}, si invocamos un método remoto que no toma ningún parámetro de función. El cuarto argumento es una identificación numérica que identifica nuestra solicitud. El servidor remoto devolverá esta identificación con su respuesta. Este comando JSON-RPC se empaqueta como una cadena antes de colocarlo en la red. Ahora estamos listos para enviar este comando al servidor remoto. networknode es el módulo de Python que contiene el cliente y el servidor RPC. La función networknode.hclient enviará el comando al servidor remoto. El primer argumento de networknode.hclient es la dirección IP y el puerto del servidor JSON-RPC remoto que es llamado. Tenga en cuenta que la dirección incluye el protocolo (http) que se utiliza para comunicarse con este servidor. El segundo argumento es nuestro comando RPC empaquetado.

El servidor JSON remoto normalmente responderá con una respuesta con este tipo de
formato sintáctico:

'{"jsonrpc": "2.0", "result": ["127.0.0.10:8081",
"127.0.0.11:8081"],"id":5}'

Este es un objeto JSON codificado como una cadena. Podemos convertir esto en un diccionario Python con la función json.loads (). El primer elemento clave especifica la versión json rpc que se usa para formatear la respuesta. El segundo elemento clave es «result». El valor de esta clave es lo que nos interesa. La tercera clave, id, identifica el id de la solicitud a la que pertenece esta respuesta. En caso de error, el servidor responderá en el siguiente formato:

'{"jsonrpc": "2.0", "error": "message", "id": 5 }'
or:
'{"jsonrpc": "2.0", "result": "error: message", "id": 5 }'

La segunda parte del módulo networknode es el servidor JSON-RPC. Examina la llamada
app.listen en este módulo:

app.listen(address="127.0.0.19", port="8081")

Esto especifica que el servidor escuchará en el puerto 8081 en la dirección IP 127.0.0.19. Cada nodo de red debe escuchar en una combinación de puerto y dirección IP única. Por último, tenga en cuenta que los recursos compartidos están protegidos de las carreras de datos mediante el uso de semáforos.
Para iniciar este servidor en la red simulada, acceda al directorio hnetwork del nodo y ejecute el script networknode.py en el entorno virtual de Blancoin.

>(virtual) python networknode.py

interfaz de red para los nodos de Blancoin

Los nodos de la red P2P de criptomonedas Blancoin realizan las siguientes tareas de red:

  1. Consultar direcciones de nodo y obtener listas de direcciones de uno o
    más servidores de directorio.
  2. Proporcione sus listas de direcciones a otros nodos solicitantes.
  3. Consultar bloques en otros nodos y procesar los bloques recibidos.
  4. Obtenga la altura de la cadena de bloques de un nodo.
  5. Recibir transacciones y bloques que se propagan en la red y
    propagarlos a otros nodos.

La especificación para la interfaz de red Blancoin es la siguiente. Todas las funciones
en la interfaz se implementan como llamadas a procedimientos remotos JSON. Un procedimiento en un servidor JSON-RPC se invoca mediante HTTP. El servidor RPC procesa la llamada a procedimiento remoto de forma asincrónica. Cada nodo de minería de blancoin implementa todas las funciones que se describen a continuación en su módulo networknode y, por lo tanto, es un servidor JSON-RPC y un cliente.

script de arranque de Blancoin

directory_server.py
networknode.py

Hay dos tipos de scripts de arranque: un script de arranque del servidor de directorio llamado directory_server.py y un script de arranque de nodo llamado networknode.py.
directory_server.py arranca un servidor de directorio que proporciona a los nodos solicitantes un listado de direcciones de nodos de blancoin.
networknode.py es un script de Python que inicia un nodo Blancoin y coloca el nodo RPC
del servidor en un estado operativo en la red P2P. Como se indicó anteriormente, cada nodo en la red Blancoin que no es un servidor de directorio implementa un RPC
servidor y cliente.

resolucion de direcciones

get_address_list() -> "string"
register_address(address: "string") -> "string"

get_address_list: esto invoca un nodo seed (servidor de directorio) con una solicitud para enviar una lista de direcciones de nodo. El servidor remoto devuelve una lista en cadena de direcciones de nodos P2P.

register_address: un nodo registra su dirección (como 127.0.0.34:8081) con una semilla
nodo o un servidor de directorio.

iniciacion de la blockchain

clear_blockchain() -> "string"

Esto borra las cadenas de bloques primaria y secundaria de un nodo. Devuelve la cadena «ok» si se borran las cadenas de bloques.

busqueda en la blockchain

Hay dos llamadas a procedimientos remotos para realizar consultas de blockchain:

get_blockchain_height() -> "integer"
get_block(block_no: "int") -> "stringified block or an error string"

get_blockchain_height: Esto solicita a un nodo remoto que proporcione la altura de su
blockchain. El solicitante recibe un entero. Esta función es utilizada por un nodo para determinar si su blockchain está sincronizado con el blockchain de algún otro nodo o para
poblar una cadena de bloques vacía. Se producirá una condición de desincronización si la altura de una cadena de bloques del nodo es sustancialmente menor que la altura de las cadenas de bloques de otros nodos debido a condiciones tales como tiempo de inactividad o falla de la red.
get_block: Esto solicita a un nodo remoto que envíe un bloque de altura determinada. Esta función devuelve un bloque o una cadena de error si se realiza una solicitud para un
bloque inexistente. Una cadena de error contendrá la palabra error.

propagacion de bloque

receive_block(block: "string") -> "ok or an error string"

Receive_block: un nodo (que actúa como cliente JSON-RPC) envía un bloque a un nodo remoto (actuando como un servidor JSON-RPC). El argumento de la función es un bloque de cadena. El bloque puede ser un bloque recién extraído o un bloque que se está propagando en la red P2P de Blancoin.
La función devuelve una cadena de error en caso de error. Una cadena de error contendrá
la palabra error.

propagacion de transacciones

receive_transaction (transacción: "cadena") -> "ok o an error string

Un nodo recibe una transacción a través de una llamada a procedimiento remoto iniciada por un nodo que actúa como un cliente JSON-RPC. Esta función devuelve una cadena de error en caso de error. Una cadena de error contendrá la palabra error.

espacio de proceso del nodo

Nuestra red P2P simulada es, a todos los efectos prácticos, idéntica a una red de blancoin
que se ejecuta en Internet. Un aspecto a tener en cuenta es que el servidor JSON-RPC y cliente para cada nodo se ejecuta en su propio espacio de proceso, y en particular, no hay
memoria compartida o código de programa aunque todo se esté ejecutando en una sola máquina. Los clientes solo pueden comunicarse con los servidores a través de HTTP y JSON-RPC versión 2.0.

pytest de la interface de red

Para iniciar la prueba, asegúrese de haber creado la red simulada como se describió
antes e inicie algunos nodos de servidor y uno o más nodos de servidor de directorio. Al hacer esto, asegúrese de estar en el entorno virtual de Blancoin cuando cree una instancia de los servidores y que cada servidor está escuchando en una dirección única. Nuestro código Pytest usa un directorio de servidor escuchando en la direccion 127.0.0.69:8081 y un servidor JSON-RPC para un nodo de minería remoto que está escuchando en la direccion 127.0.0.51:8081. Finalmente, copie networknode.py en el directorio unit_tests
e inicie un servidor en 127.0.0.19:8081.
Copie el siguiente código del programa Pytest en un archivo llamado test_hnetwork.py y guárde este archivo en el subdirectorio unit_tests del directorio raíz de Blancoin. Las pruebas en este archivo son pruebas de integración, es decir, ejecutan las llamadas al procedimiento remoto en una red simulada real y devuelven los resultados reales obtenidos.

Vaya al directorio unit_tests de la raíz de Blancoin y ejecute las pruebas con

>(virtual) pytest test_hnetwork.py -s

Puedes expandir la salida usando la opcion -s

o tambien con :

$(virtual) pytest test_hnetwork.py -s -k test_get_address_list

La opción -k solo prueba test_get_address_list e ignora las otras pruebas.

Debería ver pasar seis pruebas. Tenga en cuenta que la ejecución de estas pruebas emitirá
fragmentos de transacciones de la red simulada a medida que se construye.

###########################################################################
# test a helium network
###########################################################################
import blk_index as blkindex
import hchaindb
import hmining
import hconfig
import hblockchain
import networknode
import rcrypt
import tx
import json
import logging
import pdb
import pytest
import os
import secrets
import sys
import time
"""
log debugging messages to the file debug.log
"""
logging.basicConfig(filename="debug.log",filemode="w", \
format='client: %(asctime)s:%(levelname)s:%(message)s', level=logging.
DEBUG)
def setup_module():
os.system("rm -rf ../data/heliumdb/*")
os.system("rm -rf ../data/hblk_index/*")
os.system("rm *.log")
# start the Chainstate Database
ret = hchaindb.open_hchainstate("../data/heliumdb")
if ret == False: return "error: failed to start Chainstate
database"
else: print("Chainstate Database running")
# start the LevelDB Database blk_index
ret = blkindex.open_blk_index("../data/hblk_index")
if ret == False: return "error: failed to start blk_index"
else: print("blkindex Database running")
def teardown_module():
"""
remove the debug logs, close databases
"""
hchaindb.close_hchainstate()
blkindex.close_blk_index()
# unspent transaction fragment values [{fragmentid:value}]
unspent_fragments = []
"""
log debugging messages to the file debug.log
"""
logging.basicConfig(filename="debug.log",filemode="w", \
format='client: %(asctime)s:%(levelname)s:%(message)s', level=logging.
DEBUG)
def startup():
"""
start the databases
"""
# start the Chainstate Database
ret = hchaindb.open_hchainstate("heliumdb")
if ret == False: return "error: failed to start Chainstate database"
else: print("Chainstate Database running")
# start the LevelDB Database blk_index
ret = blkindex.open_blk_index("hblk_index")
if ret == False: return "error: failed to start blk_index"
else: print("blkindex Database running")
def stop():
"""
stop the databases
"""
hchaindb.close_hchainstate()
blkindex.close_blk_index()
# unspent transaction fragment values [{fragmentid:value}]
unspent_fragments = []
######################################################
# Make A Synthetic Random Transaction For Testing
# receives a block no and a predicate indicating
# whether the transaction is a coinbase transaction
######################################################
def make_random_transaction(blockno, is_coinbase):
txn = {}
txn["version"] = "1"
txn["transactionid"] = rcrypt.make_uuid()
if is_coinbase == True: txn["locktime"] = hconfig.conf["COINBASE_LOCKTIME"]
else: txn["locktime"] = 0
# the public-private key pair for this transaction
transaction_keys = rcrypt.make_ecc_keys()
# previous transaction fragments spent by this transaction
total_spendable = 0
#######################
# Build the vin array
#######################
txn["vin"] = []
# genesis block transactions have no prior inputs.
# coinbase transactions do not have any inputs
if (blockno > 0) and (is_coinbase != True):
max_inputs = secrets.randbelow(hconfig.conf["MAX_INPUTS"])
if max_inputs == 0: max_inputs = hconfig.conf["MAX_INPUTS"] - 1
# get some random previous unspent transaction
# fragments to spend
ind = 0
ctr = 0
while ind < max_inputs:
# get a random unspent fragment from a previous block
index = secrets.randbelow(len(unspent_fragments))
frag_dict = unspent_fragments[index]
key = [*frag_dict.keys()][0]
val = [*frag_dict.values()][0]
if val["blockno"] == blockno:
ctr += 1
if ctr == 10000:
print("failed to get random unspent fragment")
return False
continue
unspent_fragments.pop(index)
total_spendable += val["value"]
tmp = hchaindb.get_transaction(key)
if tmp == False:
print("cannot get fragment from chainstate: " + key)
assert tmp != False
assert tmp["spent"] == False
assert tmp["value"] > 0
# create a random vin element
key_array = key.split("_")
signed = rcrypt.sign_message(val["privkey"], val["pubkey"])
ScriptSig = []
ScriptSig.append(signed)
ScriptSig.append(val["pubkey"])
txn["vin"].append({
"txid": key_array[0],
"vout_index": int(key_array[1]),
"ScriptSig": ScriptSig
})
ctr = 0
ind += 1
#####################
# Build Vout list
#####################
txn["vout"] = []
# genesis block
if blockno == 0:
total_spendable = secrets.randbelow(10_000_000) + 50_000
# we need at least one transaction output for non-coinbase
# transactions
if is_coinbase == True: max_outputs = hconfig.conf["MAX_OUTPUTS"]
else:
max_outputs = secrets.randbelow(hconfig.conf["MAX_OUTPUTS"])
if max_outputs <= 1: max_outputs = 2
ind = 0
while ind < max_outputs:
tmp = rcrypt.make_SHA256_hash(transaction_keys[1])
tmp = rcrypt.make_RIPEMD160_hash(tmp)
ScriptPubKey = []
ScriptPubKey.append("<DUP>")
ScriptPubKey.append("<HASH-160>")
ScriptPubKey.append(tmp)
ScriptPubKey.append("<EQ-VERIFY>")
ScriptPubKey.append("<CHECK-SIG>")
if is_coinbase == True:
value = hmining.mining_reward(blockno)
else:
amt = int(total_spendable/max_outputs)
value = secrets.randbelow(amt) # helium cents
if value == 0:
value = int(amt / 10)
total_spendable -= value
assert value > 0
assert total_spendable >= 0
txn["vout"].append({
"value": value,
"ScriptPubKey": ScriptPubKey
})
# save the transaction fragment
fragid = txn["transactionid"] + "_" + str(ind)
fragment = {}
fragment[fragid] = { "value":value,
"privkey":transaction_keys[0],
"pubkey":transaction_keys[1],
"blockno": blockno
}
unspent_fragments.append(fragment)
#print("added to unspent fragments: " + fragid)
if total_spendable <= 0: break
ind += 1
return txn
########################################################
# Build some random Synthetic Blocks For Testing.
# Makes num_blocks, sequentially linking each
# block to the previous block through the prevblockhash
# attribute.
# Returns an array of synthetic blocks
#########################################################
def make_blocks(num_blocks):
ctr = 0
blocks = []
global unspent_fragments
unspent_fragments.clear()
hblockchain.blockchain.clear()
while ctr < num_blocks:
block = {
"prevblockhash": "",
"version": "1",
"timestamp": int(time.time()),
"difficulty_bits": 20,
"nonce": 0,
"merkle_root": "",
"height": ctr,
"tx": []
}
# make a random number of transactions for this block
# genesis block is ctr == 0
if ctr == 0: num_transactions = 200
else:
num_transactions = secrets.randbelow(50)
if num_transactions == 0: num_transactions = 40
txctr = 0
while txctr < num_transactions:
if ctr > 0 and txctr == 0: is_coinbase = True
else: is_coinbase = False
trx = make_random_transaction(ctr, is_coinbase)
assert trx != False
block["tx"].append(trx)
txctr += 1
if ctr > 0:
block["prevblockhash"] = \
hblockchain.blockheader_hash(hblockchain.blockchain[ctr - 1])
ret = hblockchain.merkle_root(block["tx"], True)
assert ret != False
block["merkle_root"] = ret
ret = hblockchain.add_block(block)
assert ret == True
blocks.append(block)
ctr+= 1
return blocks
def test_register_address():
'''
test address registration with the directory server at 127.0.0.69:8081'
'''
#test 1
ret = networknode.hclient("http://127.0.0.69:8081",
'{"jsonrpc":"2.0","method":"register_address","params":{"address":
"127.0.0.19:8081"},"id":1}')
result = (json.loads(ret))["result"]
print(result)
assert result.find("error") == -1
#test 2
ret = networknode.hclient("http://127.0.0.69:8081",
'{"jsonrpc":"2.0","method":"register_address","params":{"address":
"127.0.0.19"},"id":2}')
result = (json.loads(ret))["result"]
print(result)
assert result.find("error") >= 0
#test 3
ret = networknode.hclient("http://127.0.0.69:8081",
'{"jsonrpc":"2.0","method":"register_address","params":{"address":
"127.0.0:8081"},"id":3}')
result = (json.loads(ret))["result"]
print(result)
assert result.find("error") >= 0
def test_get_address_list():
'''
get an address list from a directory server. These are synthetic
addresses,
servers are not running at these addresses.
'''
ret = networknode.hclient("http://127.0.0.69:8081",
'{"jsonrpc":"2.0","method":"get_address_
list","params":{},"id":4}')
assert (ret.find("error")) == -1
retd = json.loads(ret)
assert "127.0.0.10:8081" in retd["result"]
print(ret)
def test_get_blockchain_height():
""" test get the height of some node's blockchain"""
blocks = make_blocks(2)
assert len(blocks) == 2
# clear the primary and secondary blockchains for this test
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "clear_blockchain",
"params": {}, "id": 10}')
assert ret.find("ok") != -1
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[0]}, "id":11})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "get_blockchain_height",
"params": {}, "id": 12}')
assert ret.find("error") == -1
height = (json.loads(ret))["result"]
assert height == 0
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[1]}, "id":13})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "get_blockchain_height",
"params": {}, "id": 14}')
assert ret.find("error") == -1
height = (json.loads(ret))["result"]
assert height == 1
def test_receive_block():
"""
send a block to a remote node. The remote node returns ok
if the block is appended to its received_blocks list,
otherwise returns an error string
"""
blocks = make_blocks(2)
assert len(blocks) == 2
# clear the primary and secondary blockchains for this test
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "clear_blockchain",
"params": {}, "id": 10}')
assert ret.find("ok") != -1
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[0]}, "id":8})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[1]}, "id":9})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
def test_receive_transaction():
"""
send a transaction to a remote node. The remote node returns ok
if the transaction is appended to its mempool.
Otherwise returns an error string
"""
blocks = make_blocks(2)
assert len(blocks) == 2
# clear the primary and secondary blockchains for this test
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc":"2.0", "method": "clear_blockchain", "params": {},
"id": 15}')
assert ret.find("ok") >= 0
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_transaction",
"params":{"trx":blocks[0]["tx"][0]},"id":16})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") >= 0
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_transaction",
"params":{"trx": blocks[0]["tx"][1]}, "id":17})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") >= 0
def test_clear_blockchain(monkeypatch):
"""
builds and clears the primary and secondary blockchains of a node
"""
# clear the primary and secondary blockchains for this test
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc":"2.0", "method": "clear_blockchain", "params": {},
"id": 19}')
assert ret.find("ok") >= 0
monkeypatch.setattr(tx, "validate_transaction", lambda x,y: True)
num_blocks = 20
blocks = make_blocks(num_blocks)
assert len(blocks) == num_blocks
tmp = hblockchain.blockheader_hash(blocks[0])
assert tmp == blocks[1]["prevblockhash"]
assert blocks[1]["height"] == 1
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[0]}, "id":21})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "get_blockchain_height",
"params": {}, "id": 22}')
assert ret.find("error") == -1
height = (json.loads(ret))["result"]
# value of height attribute of the latest block in the blockchain
# meaning there is one block in the blockchain
assert height == 0
rpc = json.dumps({"jsonrpc":"2.0","method":"receive_block",
"params":{"block": blocks[1]}, "id":23})
ret = networknode.hclient("http://127.0.0.51:8081",rpc)
assert ret.find("ok") != -1
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc": "2.0", "method": "get_blockchain_height",
"params": {}, "id": 24}')
assert ret.find("error") == -1
height = (json.loads(ret))["result"]
# value of height attribute of the latest block in the blockchain
# meaning there are two blocks in the blockchain
assert height == 1
# clear the primary and secondary blockchains for this test
ret = networknode.hclient("http://127.0.0.51:8081",
'{"jsonrpc":"2.0", "method": "clear_blockchain", "params": {},
"id": 25}')
assert ret.find("ok") >= 0

Resumen

En este post, hemos hablado de las dos principales topologías de red en Internet, la topología cliente-servidor y la topología peer-to-peer (P2P). También hemos desarrollado
el código de programa necesario para conectar los nodos de Blancoin a la red Blancoin P2P. Para lograr esto, construimos un servidor JSON-RPC y una interfaz de cliente JSON-RPC para cada nodo minero. También creamos un servidor de directorio o un nodo seed. Nuestro código de interfaz permite un nodo de minería para hacer lo siguiente:

  1. Obtener las direcciones de otros nodos de un servidor de directorio.
  2. Recibir transacciones de la red y propagarlas más lejos.
  3. Recibir bloques que se propagan en la red y propagarlos más lejos.
  4. Obtener la altura de la cadena de bloques de un nodo.
  5. Solicitar a un nodo que le envíe un determinado bloque.
  6. Borrar las cadenas de bloques primarias y secundarias.

En el próximo post, abordaremos el problema de serializar la cadena de bloques en el disco. También mostraremos cómo un nodo puede reconstruir todo su entorno blockchain, incluidas las bases de datos de una cadena de bloques serializada.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s