Guia para construir una blockchain. Parte 15

En este post, examinaremos la construcción de la wallet de Blancoin. En particular, discutiremos los siguientes temas:

  1. ¿Qué es una wallet o billetera?
  2. La interfaz de wallet de Blancoin
  3. Código del programa de cartera de Blancoin
  4. Código Pytest de la wallet de Blancoin

La implementación de la wallet de referencia de Bitcoin se basa en el acceso a la Blockchain de Bitcoin. Del mismo modo, cualquier nodo de Blancoin que tenga la blockchain completa o tiene acceso a este blockchain puede crear y mantener una wallet.
Este post crea una Wallet de Blancoin de línea de comandos básica que implementa
funcionalidad. Una vez que tenemos una implementación de la funcionalidad central, podemos embellecer nuestra Wallet con capacidades adicionales. Por ejemplo, podemos establecer una GUI alrededor de nuestra Wallet de línea de comandos.

¿que es una wallet?

Una Wallet de blancoin es un objeto que realiza las siguientes funciones:

  1. Crea y mantiene pares de claves públicas y privadas
  2. Registra los blancoins recibidos por el monedero.
  3. Registra los blancoins transferidos a otras entidades.
  4. Registra el saldo final de blancoin de la billetera (a partir del hora de la consulta)
  5. Crea transacciones y las propaga en la red Blancoin
  6. Guarda los datos de la Wallet en archivos

interfaz de la wallet de blancoin

Una Wallet de blancoin mantiene la siguiente estructura de datos de diccionario:

wallet_state = {
"keys": [], # list of private-public key tuples
"received": [], # values received by the wallet holder
"spent": [], # values transferred by the wallet holder
"received_last_block_scanned": 0,
"spent_last_block_scanned": 0
}

wallet_state describe el estado de la wallet en un momento dado.
wallet [«keys»] es una lista de las tuplas de pares de claves público-privadas propiedad de la wallet. La aplicación de wallet puede crear claves públicas y privadas.
wallet [«received»] es una lista de todos los valores recibidos por el titular de la wallet. Cada elemento en esta lista tiene la estructura:

{
"value": value
"blockno": block_height
"fragmentid: transaction["transactionid"] + "_" + str(index)
"public_key" = key_pair[1]
}

wallet [«spent»] es una lista de todos los valores transferidos por el titular de la wallet. Cada elemento en esta lista tiene la misma estructura que la wallet [“received”].
wallet [«selected_last_block_scanned»] es el último bloque de la cadena de bloques que se ha escaneado cuando se escaneó la cadena de bloques en busca de valores recibidos.
wallet [«spent_last_block_scanned»] es el último bloque de la cadena de bloques que se ha escaneado cuando se escaneó la cadena de bloques en busca de valores transferidos por el titular de la wallet.

Nuestra billetera Helium contiene las siguientes funciones de interfaz:

create_keys() -> "public, private key tuple"

Esta función crea un par de claves asimetricas que se pueden utilizar en una transacción.

value_received(block_height -> "integer" = 0) -> "boolean"

value_received crea una lista de valores de blancoin que han sido recibidos por el poseedor de la billetera. block_height es un parámetro opcional. Esta función devuelve False si la función falla y True en caso contrario.
value_received crea o actualiza el valor clave wallet_state [«received»].El algoritmo para esta función se basa en la observación de que un elemento vout de una transacción que transfiere valor al titular de la wallet contiene una clave pública generada por
el poseedor de la billetera en la forma RIPEMD-160 (SHA-256 (public_key)).
El pseudocódigo para esta función es:

let values_received = []
for each block where block["height"] >= block_height:
for each transaction trx in the block:
for each vout array element in trx:
for each public_key, pk, owned by the wallet holder:
if RIPEMD-160(SHA-256(pk)) == vout_element["ScriptPubKey][3]:
values_received.append {
value: vout_element[value]
block: block["height"]
transaction: trx[transactionid]
public_key: pk
}

La función value_spent crea una lista de valores de blancoin que han sido transferidos por el titular de la wallet a otras entidades. block_height es un parámetro opcional que dice a la función que obtenga los valores transferidos después del bloque con la altura dada (incluido este bloque).

value_spent(block_height -> "integer" = 0) -> "list"

Esta función se basa en la observación de que cada valor transferido por el poseedor de la wallet de alguna entidad será identificado por un elemento vin en la transacción que contiene la clave pública del possedor de la wallet. El siguiente pseudocódigo implementa esta funcionalidad:

let values_spent = []
for each block where block["height"] >= block_height:
for each transaction trx in the block:
for each vin list element, vin_element, in trx:
for each public_key, pk, owned by the wallet holder:
if pk == vin_element["ScriptPubKey][1]:
pointer = vin_element[transactionid] + "_" + \
vin_element[vout_index]
values_spent.append {
value: vin_element[value],
block: block["height"],
transaction: pointer,
public_key: pk
}
save_wallet() → "boolean"

La función save_wallet guarda wallet_state. En particular, wallet_state se conserva para
el archivo de disco wallet.dat:

load_wallet() -> "dictionary"

Esta función carga todos los datos de la wallet de wallet.dat en la estructura wallet_state.
Tenga en cuenta que debido a que el estado de la wallet persiste, no tenemos que interrogar a toda la cadena de bloques para actualizar la wallet. Simplemente necesitamos examinar los parametros de los bloques received_last_block_scanned y spent_last_block_scanned en adelante.

codigo de la wallet de blancoin

Copie el siguiente código del programa en un archivo llamado wallet.py y guárdelo en el directorio wallet :

###########################################################################
# wallet.rb: a command-line wallet for Helium
###########################################################################
import hblockchain
import rcrypt
import pdb
import pickle
# the state of the wallet
wallet_state = {
"keys": [], # list of private-public key tuples
"received": [], # values received by wallet holder
"spent": [], # values transferred by the wallet
holder
"received_last_block_scanned": 0,
"spent_last_block_scanned": 0
}
def create_keys():
key_pair = rcrypt.make_ecc_keys()
wallet_state["keys"].append(key_pair)
def initialize_wallet():
wallet_state["received"] = []
wallet_state["spent"] = []
wallet_state["received_last_block_scanned"] = 0
wallet_state["spent_last_block_scanned"] = 0
def value_received(blockno:"integer"=0) -> "list" :
"""
obtains all of the helium values received by the wallet holder by examining
transactions in the blockchain. Updates the wallet state.
{
value: <integer>,
ScriptPubKey: <list>
}
"""
hreceived = []
rvalue = {}
# get values received from the blockchain
for block in hblockchain.blockchain:
for transaction in block["tx"]:
ctr = -1
for vout in transaction["vout"]:
ctr += 1
for key_pair in wallet_state["keys"]:
if rcrypt.make_RIPEMD160_hash(rcrypt.make_SHA256_
hash(key_pair[1])) \ == vout["ScriptPubKey"][2]:
rvalue["value"] = vout["value"]
rvalue["blockno"] = block["height"]
rvalue["fragmentid"] = transaction["transactionid"]
+ "_" + str(ctr)
rvalue["public_key"] = key_pair[1]
hreceived.append(rvalue)
break
# update the wallet state
if block["height"] > wallet_state["received_last_block_scanned"]:
wallet_state["received_last_block_scanned"] = block["height"]
for received in hreceived:
wallet_state["received"].append(received)
return
def value_spent(blockno:"integer"=0):
"""
obtains all of the helium values transferred by the wallet holder by
examining
transactions in the blockchain. Update the wallet state.
"""
hspent = []
tvalue = {}
# get values spent from blockchain transactions
for block in hblockchain.blockchain:
for transaction in block["tx"]:
ctr = -1
for vin in transaction["vin"]:
ctr += 1
for key_pair in wallet_state["keys"]:
if rcrypt.make_RIPEMD160_hash(rcrypt.make_SHA256_
hash(key_pair[1])) \ == vin["ScriptSig"][1]:
tvalue["value"] = vin["value"]
tvalue["blockno"] = block["height"]
tvalue["fragmentid"] = transaction["transactionid"]
+ "_" + str(ctr)
tvalue["public_key"] = key_pair[1]
hspent.append(tvalue)
break
# update the wallet state
if block["height"] > wallet_state["spent_last_block_scanned"]:
wallet_state["spent_last_block_scanned"] = block["height"]
for spent in hspent:
wallet_state["spent"].append(spent)
return
def save_wallet() -> "bool":
"""
saves the wallet state to a file
"""
try:
f = open('wallet.dat', 'wb')
pickle.dump(wallet_state, f)
f.close()
return True
except Exception as error:
print(str(error))
return False
def load_wallet() -> "bool":
"""
loads the wallet state from a file
"""
try:
f = open('wallet.dat', 'rb')
global wallet_state
wallet_state = pickle.load(f)
f.close()
return True
except Exception as error:
print(str(error))
return False

wallet.py contiene funciones que escanean la cadena de bloques y construyen una lista de todos los valores de transacción recibidos, así como los valores de transacción enviados. Ya que el estado de la wallet mantiene los marcadores del último bloque que se ha escaneado cuando estas listas están construidas (received_last_block_scanned y spent_last_block_scanned), no es necesario volver a escanear la parte de la cadena de bloques que se escanea para crear estas listas siempre ya que la wallet solo usa claves públicas y privadas que se crean usando create_keys de la funcion wallet.
La función initialize_wallet se puede llamar en caso de que decidamos escanear la blockchain nuevamente desde el bloque génesis.

codigo pytest de blancoin

El siguiente código de programa implementa Pytests para nuestra billetera Blancoin. Estas pruebas son pruebas de integración que utilizan una cadena de bloques simulada. Copie este código en un archivo llamado test_wallet.py y guarde el archivo en el directorio unit_tests. Como de costumbre, podemos ejecutar estas pruebas con:

(virtual) $ pytest test_wallet.rb

La ejecución de estas pruebas debe indicar 13 pruebas aprobadas:

######################################################
# test_wallet.rb
######################################################
import hchaindb
import hmining
import hconfig
import hblockchain
import rcrypt
import tx
import wallet
import json
import logging
import os
import pdb
import pytest
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():
"""
start the databases
"""
# 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")
# make a simulated blockchain with height 5
make_blocks(5)
def teardown_module():
"""
stop the databases
"""
hchaindb.close_hchainstate()
# unspent transaction fragment values [{fragmentid:value}]
unspent_fragments = []
keys = []
######################################################
# 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()
global keys
keys.append(transaction_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]
if frag_dict["blockno"] == blockno or frag_dict["value"] < 10:
ctr += 1
if ctr == 10000:
print("failed to get random unspent fragment")
return False
continue
key = frag_dict["key"]
unspent_fragments.pop(index)
assert unspent_fragments.count(frag_dict) == 0
total_spendable += frag_dict["value"]
tmp = hchaindb.get_transaction(key)
if tmp == False:
print("cannot get fragment from chainstate: " + key)
assert tmp["spent"] == False
assert tmp["value"] > 0
# create a random vin element
key_array = key.split("_")
signed = rcrypt.sign_message(frag_dict["privkey"], frag_
dict["pubkey"])
ScriptSig = []
ScriptSig.append(signed)
ScriptSig.append(frag_dict["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(10000000) + 50000
# 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 = hconfig.conf["MAX_OUTPUTS"]
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)
if amt == 0: break
value = secrets.randbelow(amt) # helium cents
if value == 0: value = int(amt)
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 = {
"key": fragid,
"value":value,
"privkey":transaction_keys[0],
"pubkey":transaction_keys[1],
"blockno": blockno
}
unspent_fragments.append(fragment)
print("added to unspent fragments: " + fragment["key"])
if total_spendable <= 50: 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
total_tx = 0
blocks = []
global unspent_fragments
unspent_fragments.clear()
hblockchain.blockchain.clear()
while ctr < num_blocks:
block = {}
block["prevblockhash"] = ""
block["version"] = hconfig.conf["VERSION_NO"]
block["timestamp"] = int(time.time())
block["difficulty_bits"] = hconfig.conf["DIFFICULTY_BITS"]
block["nonce"] = hconfig.conf["NONCE"]
block["merkle_root"] = ""
block["height"] = ctr
block["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 <= 1: num_transactions = 25
txctr = 0
while txctr < num_transactions:
if ctr > 0 and txctr == 0: coinbase_trans = True
else: coinbase_trans = False
trx = make_random_transaction(ctr, coinbase_trans)
assert trx != False
block["tx"].append(trx)
total_tx += 1
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
print("blockchain height: " + str(blocks[-1]["height"]))
print("total transactions count: " + str(total_tx))
return blocks
def test_no_values_received():
"""
get all of the transaction values received by the wallet-holder
when the holder has no keys.
"""
wallet.wallet_state["keys"] = []
wallet.value_received(0)
assert wallet.wallet_state["received"] == []
@pytest.mark.parametrize("index", [
(0),
(1),
(11),
(55),
])
def test_values_received(index):
"""
test that each value received by the wallet owner pertains to a
public key owned by the wallet owner.
Note: At least 55 private-public keys must have been generated.
"""
wallet.initialize_wallet()
wallet.wallet_state["keys"] = []
my_key_pairs = keys[:]
for _ in range(index):
key_index = secrets.randbelow(len(my_key_pairs))
wallet.wallet_state["keys"].append(keys[key_index])
# remove any duplicates
wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
my_public_keys = []
for key_pair in wallet.wallet_state["keys"]:
my_public_keys.append(key_pair[1])
wallet.value_received(0)
for received in wallet.wallet_state["received"]:
assert received["public_key"] in my_public_keys
def test_one_received():
"""
test that for a given public key owned by the wallet-holder, at least one
transaction fragment exists in the wallet.
"""
wallet.initialize_wallet()
wallet.wallet_state["keys"] = []
key_index = secrets.randbelow(len(keys))
wallet.wallet_state["keys"].append(keys[key_index])
ctr = 0
wallet.value_received(0)
for received in wallet.wallet_state["received"]:
if received["public_key"] == wallet.wallet_state["keys"][0][1]:
ctr += 1
assert ctr >= 1
@pytest.mark.parametrize("index", [
(0),
(1),
(13),
(49),
(55)
])
def test_values_spent(index):
"""
test that values spent pertain to public keys owned by the wallet owner.
Note: At least 55 private-public keys must have been generated.
"""
wallet.initialize_wallet()
wallet.wallet_state["keys"] = []
my_key_pairs = keys[:]
for _ in range(index):
key_index = secrets.randbelow(len(my_key_pairs))
wallet.wallet_state["keys"].append(keys[key_index])
# remove any duplicates
wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
my_public_keys = []
for key_pair in wallet.wallet_state["keys"]:
my_public_keys.append(key_pair[1])
wallet.value_spent(0)
for spent in wallet.wallet_state["spent"]:
assert spent["public_key"] in my_public_keys
def test_received_and_spent():
"""
test that if a transaction fragment is spent then it has also been
received by the wallet owner.
"""
wallet.initialize_wallet()
wallet.wallet_state["keys"] = []
key_index = secrets.randbelow(len(keys))
wallet.wallet_state["keys"].append(keys[key_index])
wallet.value_received(0)
wallet.value_spent(0)
assert len(wallet.wallet_state["received"]) >= 1
for spent in wallet.wallet_state["spent"]:
ctr = 0
assert spent["public_key"] == wallet.wallet_state[keys][0][1]
ptr = spent["fragmentid"]
for received in wallet.wallet_state["received"]:
if received["fragmentid"] == ptr: ctr += 1
assert ctr == 1
ctr = 0
def test_wallet_persistence():
"""
test the persistence of a wallet to a file.
"""
wallet.initialize_wallet()
wallet.wallet_state["keys"] = []
my_key_pairs = keys[:]
for _ in range(25):
key_index = secrets.randbelow(len(my_key_pairs))
wallet.wallet_state["keys"].append(keys[key_index])
# remove any duplicates
wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
wallet.value_spent(0)
wallet.value_received(0)
wallet_str = json.dumps(wallet.wallet_state)
wallet_copy = json.loads(wallet_str)
assert wallet.save_wallet() == True
assert wallet.load_wallet() == True
assert wallet.wallet_state["received"] == wallet_copy["received"]
assert wallet.wallet_state["spent"] == wallet_copy["spent"]
assert wallet.wallet_state["received_last_block_scanned"] == \
wallet_copy["received_last_block_scanned"]
assert wallet.wallet_state["spent_last_block_scanned"] == \
wallet_copy["spent_last_block_scanned"]
max = len(wallet_copy["keys"])
ctr = 0
for ctr in range(max):
assert wallet.wallet_state["keys"][ctr][0] == wallet_copy["keys"]
[ctr][0]
assert wallet.wallet_state["keys"][ctr][1] == wallet_copy["keys"]
[ctr][1]

En este capítulo, hemos construido una wallet básica de línea de comandos a partir de una cadena de bloques. Esta wallet implementa todas las funciones básicas que se requieren en una wallet. La walletse puede mejorar cifrando el archivo de disco que conserva el estado de la wallet, envolver la billetera en una interfaz GUI y brindar otras características de conveniencia. En el próximo y último post, volveremos al tema de la creación de una red de prueba. para la red 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 )

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