Guia para construir una blockchain. Parte 14

En este post, examinaremos el mantenimiento de la cadena de bloques de blancoin. Los siguientes temas seran discutidos:

  1. Cómo un nuevo nodo de helio con una cadena de bloques vacía adquiere bloques de la red de blancoin y se sincroniza con la blockchain distribuida. Este procedimiento para adquirir bloques también se aplica si un nodo se cae o si hay una particion de red temporal.
  2. Reconstrucción de la base de datos de Chainstate.
  3. Reconstrucción de la base de datos blk_index.

mantenimiento de la blockchain

Un nuevo nodo que se una a la red peer-to-peer de Blancoin deberá obtener una cadena de bloques. Además, la cadena de bloques mantenida por un nodo puede necesitar sincronización si se cae o si hay una partición de red temporal.
La interfaz de red Blancoin proporciona dos llamadas a procedimientos remotos para manejar estos casos de uso:

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

get_blockchain_height obtiene la altura de la cadena de bloques mantenida por un nodo remoto. La llamada get_block obtiene un bloque específico de un nodo remoto. Ambos de estas llamadas a procedimientos remotos se discutieron en el post anterior.
Te dejo desarrollar pruebas para obtener bloques. En un entorno de producción y
para mejorar el rendimiento, querrá mejorar el código para proporcionar descarga simultánea de bloques desde varios nodos remotos.

ingenieria de fiabilidad en Blancoin

Una aplicación que gestiona transacciones financieras debe tener una fiabilidad extrema. El principio básico es que la aplicación debe implementar el patrón de arquitectura más simple posible para lograr sus objetivos. En el post anterior, vimos que la arquitectura distribuida peer-to-peer de Blancoin crea una arquitectura de red que no se puede degradar fácilmente. Todos los nodos en el ecosistema son iguales en capacidad, y cualquiera puede crear una instancia de un nodo y unirse a la red de Blancoin. Además, Bitcoin y Blancoin tienen requisitos muy modestos y por lo tanto, puede funcionar con hardware básico de bajo costo. Es completamente factible ejecutar un nodo Blancoin o nodo Bitcoin en una computadora portátil. Además, vimos en la sección anterior que un nodo de Blancoin siempre puede sincronizar su cadena de bloques con la cadena de bloques distribuida en caso de que el nodo deje de funcionar o si hay una partición de red inesperada. Aparte de estas características de confiabilidad, Blancoin y Bitcoin implementan un solo punto de fallo en la arquitectura. Este punto de falla es el archivo de bloque, que es un texto de archivo codificado simple. Una cadena de bloques es la colección de estos archivos. Un nodo de Blancoin puede reconstruir todas las estructuras de datos y bases de datos que necesita para su operación desde la cadena de bloques (la colección de archivos de bloque). Blancoin utiliza dos claves para
almacenar el valor, la base de datos Chainstate y la base de datos blk_index, y asegura por diseño que estas bases de datos no son puntos críticos de fallo. En otras palabras, Blancoin siempre puede reconstruir estas bases de datos siempre que tenga todos los archivos de bloque. Los bloques de Blancoin son archivos de texto codificados que son de solo lectura. Dado que una cadena de bloques es una estructura inmutable, las actualizaciones y eliminaciones de archivos de bloques no son necesarias ni compatibles.
Las operaciones de creación y lectura son manejadas por el sistema operativo subyacente. El siguiente fragmento de código del módulo hblockchain muestra cómo es la persistencia del bloque implementado en Blancoin:

def add_block(block: "dictionary") -> "bool":
"""
add_block: adds a block to the blockchain. Receives a block. The block
attributes are checked for validity and each transaction in the block is
tested for validity. If there are no errors, the block is written to a file
as asequence of raw bytes. Then the block is added to the blockchain.
The chainstate database and the blk_index databases are updated.
returns True if the block is added to the blockchain and False otherwise
"""
try:
# validate the received block parameters
if validate_block(block) == False:
raise(ValueError("block validation error"))
# validate the transactions in the block
# update the chainstate database
for trx in block['tx']:
# first transaction in the block is a coinbase transaction
if block["height"] == 0 or block['tx'][0] == trx: zero_inputs = True
else: zero_inputs = False
if tx.validate_transaction(trx, zero_inputs) == False:
raise(ValueError("transaction validation error"))
if hchaindb.transaction_update(trx) == False:
raise(ValueError("chainstate update transaction error"))
# serialize the block to a file
if (serialize_block(block) == False):
raise(ValueError("serialize block error"))
# add the block to the blockchain in memory
blockchain.append(block)
# update the blk_index
for transaction in block['tx']:
blockindex.put_index(transaction["transactionid"], block["height"])
except Exception as err:
print(str(err))
logging.debug('add_block: exception: ' + str(err))
return False
return True
def serialize_block(block: "dictionary") -> "bool":
"""
serialize_block: serializes a block to a file using pickle.
Returns True if the block is serialized and False otherwise.
"""
index = len(blockchain)
filename = "block_" + str(index) + ".dat"
# create the block file and serialize the block
try:
f = open(filename, 'wb')
pickle.dump(block, f)
except Exception as error:
logging.debug("Exception: %s: %s", "serialize_block", error)
f.close()
return False
f.close()
return True
Once a block has been validated, it is persisted to a specific directory. 1 You will want
the blocks written into the data directory. The function serialize_block writes a block into
a directory. As you will notice, it is exceptionally simple. Each block is written to a file
named
"block" + "height of block " + ".dat"
This simple naming scheme indexes the block files by height. The genesis block has
height zero.
The collection of all of the blocks in the directory is the blockchain.


construyendo una blockchain simulada

El siguiente código de programa crea una cadena de bloques simulada con una altura de 500 bloques. Una cadena de bloques sintética de este tipo puede ser muy útil para varios casos de uso:

  1. Prueba de blockchains durante la fase de desarrollo
  2. Examinar el comportamiento de la cadena de bloques cuando sus parámetros son
    cambiados
  3. Examinar la resistencia y confiabilidad de la cadena de bloques

Copie este código en un archivo de Python llamado simulated_blockchain.py y guárdelo en el directorio unit_tests. Después de esto, ejecútelo en el entorno virtual Blancoin, con

> (virtual) simulated_blockchain.py

Debería ver una cadena de bloques de 500 bloques en el directorio unit_tests. Puede construir blockchains de altura arbitraria proporcionando el parámetro de altura requerido a la función make_blocks.
Es necesario hacer una advertencia. A medida que las transacciones ocurren en la cadena de bloques simulada, aumentará la proporción de transacciones fragmentadas con productos muy pequeños. Debido a esta fragmentación, las compilaciones de blockchain para alturas muy altas fallarán. La consolidación de las salidas de transacciones fragmentadas generalmente se manejan mediante Wallets. Puedes construir un
Consolidador de transacciones fragmentadas para nuestra cadena de bloques simulada. Esto te proporcionará información muy valiosa sobre cómo se construyen y evolucionan las cadenas de bloques con el tiempo.

######################################################
# simulated blockchain constructor
######################################################
import blk_index as blkindex
import hchaindb
import hmining
import hconfig
import hblockchain
import rcrypt
import tx
import json
import logging
import pdb
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 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 = 1
else:
max_outputs = secrets.randbelow(hconfig.conf["MAX_OUTPUTS"])
if max_outputs == 0: max_outputs = 6
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:
pdb.set_trace()
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
num_transactions = 2
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
###################################################
# make the simulated blockchain
###################################################
startup()
make_blocks(500)
print("finished synthetic blockchain construction")
stop()

restricciones de archivo de directorio

Un nodo Blancoin en ejecución acumulará una gran cantidad de archivos de bloque en su directorio de datos. Esto plantea el problema del número máximo de archivos que se pueden escribir en un directorio. Esta restricción depende del sistema operativo subyacente. En Linux con el sistema de archivos ext4, no hay límite teórico en la cantidad de archivos que se pueden escribir a un directorio. No obstante, podemos optar por escribir bloques en varios subdirectorios utilizando un algoritmo de fragmentación, como el siguiente:

create n sub-directories of the data directory: 0, 1,2,3,...n-1
for each block:
compute the SHA-256 hash of the block.
convert this hash into an integer: hash_num.
compute the n-modulus of hash_num: hash_mod = hash_num%n
write the block into directory hash_mod

mantenimiento de la chainstate

En una transacción típica de criptomonedas, una entidad gasta valores de transacciones anteriores que han sido recibidos y no gastados. La base de datos de Chainstate realiza un seguimiento de todos los valores no gastados en el ecosistema de criptomonedas. Cada uno de estos valores se representa como un fragmento de transacción. En cada momento, el Chainstate proporciona una instantánea de los fragmentos gastados y no gastados en el sistema. Si un nodo de Blancoin tiene la cadena de bloques, siempre puede reconstruir la base de datos de Chainstate. El siguiente algoritmo en pseudocódigo logra esto:

for each block:
for each transaction in the block:
let transaction id be trnid.
let ctr = 0.
for each vout element in the transaction:
fragment_id = trnid + "_" + string(ctr)
value = vout["value"]
hash = RIPEMD160(SHA256(public_key))
chainstate[fragment_id] = {"value": value,
"spent": False,
"tx_chain": "",
"pkhash": hash
}
ctr += 1
for each vin element in the transaction:
fragment_id = vin["txid] + "_" + vin["vout_index"]
chainstate[fragment_id] = {"value": value,
"spent": True,
"tx_chain": some_fragment,
"pkhash": hash
}

El siguiente código de programa reconstruye la base de datos de Chainstate desde cero desde la colección de archivos de bloque. Notarás que este código aprovecha el módulo hchaindb y, en particular, utiliza la función update_transaction en este módulo para construir la base de datos Chainstate. Para ejercitar este código, copie este código en el archivo build_chainstate.py y guárdalo en el directorio unit_tests. Luego haz una cadena de bloques simulada con una altura de 500 bloques en el directorio unit_tests. Finalmente, elimine la base de datos Chainstate, blancoin_db en el subdirectorio unit_tests/blancoin (rm -rf blancoindb), si existe. El comando siguiente reconstruirá el Chainstate:

> (virtual) python build_chainstate.py
###########################################################################
# build_chainstate: Builds the chainstate database from the blockchain files.
###########################################################################
import hblockchain
import hchaindb
import pickle
import os.path
import pdb
def startup():
"""
start the chainstate database
"""
# start the Chainstate Database
ret = hchaindb.open_hchainstate("heliumdb")
if ret == False:
print("error: failed to start Chainstate database")
return
else: print("Chainstate Database running")
def stop():
"""
stop the chainstate database
"""
hchaindb.close_hchainstate()
def build_chainstate():
'''
build the Chainstate database from the blockchain files
'''
blockno = 0
tx_count = 0
try:
while True:
# test whether the block file exists
block_file = "block" + "_" + str(blockno) + ".dat"
if os.path.isfile(block_file)== False: break
# load the block file into a dictionary object
f = open(block_file, 'rb')
block = pickle.load(f)
# process the vout and vin arrays for each block transaction
for trx in block["tx"]:
ret = hchaindb.transaction_update(trx)
tx_count += 1
if ret == False:
raise(ValueError("failed to rebuild chainstate. block
no: " + \
str(blockno)))
blockno += 1
except Exception as err:
print(str(err))
return False
print("transactions processed: " + str(tx_count))
print("blocks processed: " + str(blockno))
return True
# start the chainstate build
print("start build chainstate")
startup()
build_chainstate()
print("chainstate rebuilt")
stop()

mantenimiento de blk_index

En otro post discutimos la base de datos blk_index. Esta es un registro de pares valor clave que determina el bloque en el que se ubica una transacción en particular. Las claves son las los ID de transacción y los valores son las alturas de los bloques. Como la base de datos Chainstate, la base de datos blk_index se puede reconstruir desde los archivos de blockchain. El siguiente pseudocódigo reconstruye blk_index a partir de la cadena de bloques:

for block in blockchain:
for transaction in block:
blk_index[transaction[transactionid]] = block["height"]

El siguiente módulo, build_blk_index, reconstruye el registro blk_index. Copia el
siguiente código en el archivo build_blk_index.py y guárdelo en el directorio unit_tests.
Elimine el subdirectorio hblk_index, si existe. Ahora podemos reconstruir la base de datos blk_index:

> (virtual) python build_index.py
#########################################################################
# build_blk_index: rebuilds the blk_index database from the blockchain
#########################################################################
import blk_index
import pickle
import os.path
import pdb
def startup():
"""
start/create the blk_index database
"""
# start the blk_index Database
ret = blk_index.open_blk_index("hblk_index")
if ret == False:
print("error: failed to start blk_index database")
return
else: print("blk_index Database running")
return True
def stop():
"""
stop the blk_index database
"""
blk_index.close_blk_index()
def build_blk_index():
'''
build the blk_index database from the blockchain
'''
blockno = 0
tx_count = 0
try:
while True:
# test whether the block file exists
block_file = "block" + "_" + str(blockno) + ".dat"
if os.path.isfile(block_file)== False: break
# load the block file into a dictionary object
f = open(block_file, 'rb')
block = pickle.load(f)
# process the transactions in the block
for trx in block["tx"]:
ret = blk_index.put_index(trx["transactionid"], blockno)
tx_count += 1
if ret == False:
raise(ValueError("failed to rebuild blk_index. block
no: " \
+ str(blockno)))
blockno += 1
except Exception as err:
print(str(err))
return False
print("transactions processed: " + str(tx_count))
print("blocks processed: " + str(blockno))
return True
# start the build_index build
print("start build blk_index")
ret = startup()
if ret == False:
print("failed to open blk_index")
os._exit(-1)
build_blk_index()
print("blk_index rebuilt")
stop()

conclusion

En este post, hemos considerado algunos de los aspectos del mantenimiento importantes del Blancoin. Estos han incluido obtener una cadena de bloques para un nuevo nodo y sincronizar la blockchain por un nodo si un nodo se cae o si hay una particion de red temporal. También hemos expuesto el código para reconstruir la base de datos de Chainstate y la base de datos blk_index desde los archivos de blockchain.
En el próximo post, desarrollaremos una wallet de usuario para Blancoin.

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 )

Foto de Facebook

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

Conectando a %s