Creación de una criptomoneda en el lenguaje de programación Crystal

Publicado: 2022-03-11

Esta publicación es mi intento de comprender los aspectos clave de la cadena de bloques al explorar los aspectos internos. Empecé leyendo el documento técnico original de bitcoin, pero sentí que la única forma de comprender verdaderamente la cadena de bloques es creando una nueva criptomoneda desde cero.

Es por eso que decidí crear una criptomoneda utilizando el nuevo lenguaje de programación Crystal, y lo denominé CrystalCoin . Este artículo no discutirá las opciones de algoritmos, la dificultad de hash o temas similares. En su lugar, la atención se centrará en detallar un ejemplo concreto, que debería proporcionar una comprensión más profunda y práctica de las fortalezas y limitaciones de las cadenas de bloques.

Si aún no lo ha leído, para obtener más información sobre algoritmos y hashing, le sugiero que eche un vistazo al artículo de Demir Selmanovic Criptomoneda para Dummies: Bitcoin y más allá.

Por qué elegí el lenguaje de programación Crystal

Para una mejor demostración, quería usar un lenguaje productivo como Ruby sin comprometer el rendimiento. Las criptomonedas tienen muchos cálculos que consumen mucho tiempo (a saber, minería y hashing ), y es por eso que los lenguajes compilados como C++ y Java son los lenguajes elegidos para crear criptomonedas "reales". Dicho esto, quería usar un lenguaje con una sintaxis más limpia para poder mantener el desarrollo divertido y permitir una mejor legibilidad. El rendimiento del cristal tiende a ser bueno de todos modos.

Ilustración de lenguaje de programación de cristal

Entonces, ¿por qué decidí usar el lenguaje de programación Crystal? La sintaxis de Crystal está fuertemente inspirada en la de Ruby, así que para mí, se siente natural de leer y fácil de escribir. Tiene el beneficio adicional de una curva de aprendizaje más baja, especialmente para los desarrolladores de Ruby experimentados.

Así es como lo expresa el equipo de Crystal lang en su sitio web oficial:

Rápido como C, hábil como Ruby.

Sin embargo, a diferencia de Ruby o JavaScript, que son lenguajes interpretados, Crystal es un lenguaje compilado, lo que lo hace mucho más rápido y ofrece una menor huella de memoria. Bajo el capó, utiliza LLVM para compilar en código nativo.

Crystal también tiene tipos estáticos, lo que significa que el compilador lo ayudará a detectar errores de tipo en tiempo de compilación.

No voy a explicar por qué considero que el lenguaje Crystal es increíble porque está más allá del alcance de este artículo, pero si no encuentras mi optimismo convincente, no dudes en consultar este artículo para obtener una mejor visión general del potencial de Crystal.

Nota: Este artículo asume que ya tiene una comprensión básica de la Programación Orientada a Objetos (POO).

cadena de bloques

Entonces, ¿qué es una cadena de bloques? Es una lista (cadena) de bloques vinculados y protegidos por huellas dactilares digitales (también conocidas como hashes criptográficos).

La forma más fácil de pensarlo es como una estructura de datos de lista enlazada. Dicho esto, una lista enlazada solo requiere tener una referencia al elemento anterior; un bloque debe tener un identificador que depende del identificador del bloque anterior, lo que significa que no puede reemplazar un bloque sin volver a calcular cada bloque que viene después.

Por ahora, piense en blockchain como una serie de bloques con algunos datos vinculados con una cadena, siendo la cadena el hash del bloque anterior.

Toda la cadena de bloques existiría en cada nodo que quiera interactuar con ella, es decir, se copia en cada uno de los nodos de la red. Ningún servidor lo aloja, pero todas las empresas de desarrollo de blockchain lo utilizan, lo que lo hace descentralizado .

Sí, esto es extraño en comparación con los sistemas centralizados convencionales. Cada uno de los nodos tendrá una copia de la cadena de bloques completa (> 149 Gb en la cadena de bloques de Bitcoin para diciembre de 2017).

Hashing y Firma Digital

Entonces, ¿qué es esta función hash? Piense en el hash como una función que devuelve una huella digital única cuando le damos un texto/objeto. Incluso el cambio más pequeño en el objeto de entrada cambiaría drásticamente la huella digital.

Hay diferentes algoritmos hash, y en este artículo usaremos el algoritmo hash SHA256 , que es el que se usa en Bitcoin .

Al utilizar SHA256 , siempre obtendremos 64 caracteres hexadecimales (256 bits) de longitud, incluso si la entrada es inferior a 256 bits o mucho mayor que 256 bits:

Aporte Resultados hash
TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO TEXTO MUY LARGO cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
Toptal 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
Toptal. 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

Tenga en cuenta con el último ejemplo, que solo agregando un archivo . (punto) resultó en un cambio dramático en el hash.

Por lo tanto, en una cadena de bloques, la cadena se construye pasando los datos del bloque a un algoritmo hash que generaría un hash, que se vincula al siguiente bloque, formando así una serie de bloques vinculados con los hash de los bloques anteriores.

Construyendo una Criptomoneda en Crystal

Ahora comencemos a crear nuestro proyecto Crystal y construyamos nuestro cifrado SHA256 .

Suponiendo que tiene instalado el lenguaje de programación Crystal, vamos a crear el esqueleto de la base de código de CrystalCoin mediante el uso de la crystal init app [name] :

 % crystal init app crystal_coin create crystal_coin/.gitignore create crystal_coin/.editorconfig create crystal_coin/LICENSE create crystal_coin/README.md create crystal_coin/.travis.yml create crystal_coin/shard.yml create crystal_coin/src/crystal_coin.cr create crystal_coin/src/crystal_coin/version.cr create crystal_coin/spec/spec_helper.cr create crystal_coin/spec/crystal_coin_spec.cr Initialized empty Git repository in /Users/eki/code/crystal_coin/.git/

Este comando creará la estructura básica para el proyecto, con un repositorio Git ya inicializado, licencia y archivos Léame. También viene con stubs para pruebas y el archivo shard.yml para describir el proyecto y administrar las dependencias, también conocidas como shards.

Agreguemos el fragmento openssl , que se necesita para construir el algoritmo SHA256 :

 # shard.yml dependencies: openssl: github: datanoise/openssl.cr

Una vez que esté dentro, regrese a su terminal y ejecute crystal deps . Al hacer esto, desplegará openssl y sus dependencias para que las utilicemos.

Ahora que tenemos la biblioteca requerida instalada en nuestro código, comencemos definiendo la clase Block y luego construyendo la función hash.

 # src/crystal_coin/block.cr require "openssl" module CrystalCoin class Block def initialize(data : String) @data = data end def hash hash = OpenSSL::Digest.new("SHA256") hash.update(@data) hash.hexdigest end end end puts CrystalCoin::Block.new("Hello, Cryptos!").hash

Ahora puede probar su aplicación ejecutando crystal run crystal src/crystal_coin/block.cr desde su terminal.

 crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5

Diseñando nuestra Blockchain

Cada bloque se almacena con una timestamp de tiempo y, opcionalmente, un index . En CrystalCoin , vamos a almacenar ambos. Para ayudar a garantizar la integridad en toda la cadena de bloques, cada bloque tendrá un hash de identificación propia. Al igual que Bitcoin, el hash de cada bloque será un hash criptográfico del bloque ( index , timestamp de tiempo, data y el hash del hash anterior_hash del bloque previous_hash ). Los datos pueden ser lo que quieras por ahora.

 module CrystalCoin class Block property current_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @current_hash = hash_block end private def hash_block hash = OpenSSL::Digest.new("SHA256") hash.update("#{@index}#{@timestamp}#{@data}#{@previous_hash}") hash.hexdigest end end end puts CrystalCoin::Block.new(data: "Same Data").current_hash

En Crystal lang, reemplazamos los attr_accessor , attr_getter y attr_setter de Ruby con nuevas palabras clave:

Palabra clave rubí Palabra clave de cristal
attr_accesorio propiedad
attr_lector adquiridor
attr_writer setter

Otra cosa que puede haber notado en Crystal es que queremos sugerirle al compilador tipos específicos a través de nuestro código. Crystal infiere los tipos, pero siempre que tenga ambigüedad, también puede declarar tipos explícitamente. Es por eso que agregamos tipos de String para current_hash .

Ahora block.cr dos veces y tengamos en cuenta que los mismos datos generarán hashes diferentes debido a la timestamp de tiempo diferente:

 crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3

Ahora tenemos nuestra estructura de bloques, pero estamos creando una cadena de bloques. Necesitamos comenzar a agregar bloques para formar una cadena real. Como mencioné anteriormente, cada bloque requiere información del bloque anterior. Pero, ¿cómo llega allí el primer bloque de la cadena de bloques? Bueno, el primer bloque, o bloque genesis , es un bloque especial (un bloque sin predecesores). En muchos casos, se agrega manualmente o tiene una lógica única que permite agregarlo.

Crearemos una nueva función que devuelva un bloque de génesis. Este bloque es de index=0 y tiene un valor de datos arbitrario y un valor arbitrario en el parámetro previous_hash .

Construyamos o clasifiquemos el método Block.first que genera el bloque de génesis:

 module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end

Y probemos usando p CrystalCoin::Block.first :

 #<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">

Ahora que podemos crear un bloque de génesis , necesitamos una función que genere bloques sucesivos en la cadena de bloques.

Esta función tomará el bloque anterior de la cadena como parámetro, creará los datos para que se genere el bloque y devolverá el nuevo bloque con los datos apropiados. Cuando los bloques nuevos procesan información de bloques anteriores, la integridad de la cadena de bloques aumenta con cada bloque nuevo.

Una consecuencia importante es que no se puede modificar un bloque sin cambiar el hash de cada bloque consecutivo. Esto se demuestra en el siguiente ejemplo. Si los datos en el bloque 44 se cambian de LOOP a EAST , se deben cambiar todos los valores hash de los bloques consecutivos. Esto se debe a que el hash del bloque depende del valor de previous_hash (entre otras cosas).

Diagrama de hashing de criptomonedas de Crystal

Si no hiciéramos esto, sería más fácil para un tercero cambiar los datos y reemplazar nuestra cadena con una completamente nueva. Esta cadena de hashes actúa como prueba criptográfica y ayuda a garantizar que una vez que se agrega un bloque a la cadena de bloques, no se puede reemplazar ni eliminar. Vamos a crear el método de clase Block.next :

 module CrystalCoin class Block ... def self.next(previous_node, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_node.index + 1})", index: previous_node.index + 1, previous_hash: previous_hash.hash ) end ... end end

Para probarlo todo junto, crearemos una cadena de bloques simple. El primer elemento de la lista es el bloque de génesis. Y, por supuesto, necesitamos agregar los bloques siguientes. Crearemos cinco nuevos bloques para demostrar CrystalCoin :

 blockchain = [ CrystalCoin::Block.first ] previous_block = blockchain[0] 5.times do new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block end p blockchain
 [#<CrystalCoin::Block:0x108c57c80 @current_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @index=0, @previous_hash="0", @timestamp=2018-06-04 12:13:21 +03:00, @data="Genesis Block>, #<CrystalCoin::Block:0x109c89740 @current_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @index=1, @previous_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @timestamp=2018-06-04 12:13:21 +03:00, @data="Transaction data number (1)">, #<CrystalCoin::Block:0x109cc8500 @current_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @index=2, @previous_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (2)">, #<CrystalCoin::Block:0x109ccbe40 @current_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @index=3, @previous_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (3)">, #<CrystalCoin::Block:0x109cd0980 @current_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @index=4, @previous_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (4)">, #<CrystalCoin::Block:0x109cd0100 @current_hash= "00a0c70e5412edd7389a0360b48c407ce4ddc8f14a0bcf16df277daf3c1a00c7", @index=5, @previous_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (5)">

Prueba de trabajo

Un algoritmo de prueba de trabajo (PoW) es cómo se crean o extraen nuevos bloques en la cadena de bloques. El objetivo de PoW es descubrir un número que resuelva un problema. El número debe ser difícil de encontrar pero fácil de verificar computacionalmente por cualquiera en la red. Esta es la idea central detrás de la Prueba de trabajo.

Demostremos con un ejemplo para asegurarnos de que todo está claro. Supondremos que el hash de algún entero x multiplicado por otro y debe empezar por 00 . Entonces:

 hash(x * y) = 00ac23dc...

Y para este ejemplo simplificado, arreglemos x=5 e implementemos esto en Crystal:

 x = 5 y = 0 while hash((x*y).to_s)[0..1] != "00" y += 1 end puts "The solution is y = #{y}" puts "Hash(#{x}*#{y}) = #{hash((x*y).to_s)}"

Ejecutemos el código:

 crystal_coin [master●●] % time crystal src/crystal_coin/pow.cr The solution is y = 530 Hash(5*530) = 00150bc11aeeaa3cdbdc1e27085b0f6c584c27e05f255e303898dcd12426f110 crystal src/crystal_coin/pow.cr 1.53s user 0.23s system 160% cpu 1.092 total

Como puede ver, este número y=530 fue difícil de encontrar (fuerza bruta) pero fácil de verificar usando la función hash.

¿Por qué molestarse con este algoritmo PoW? ¿Por qué no creamos un hash por bloque y listo? Un hash debe ser válido . En nuestro caso, un hash será válido si los dos primeros caracteres de nuestro hash son 00 . Si nuestro hash comienza con 00...... , se considera válido. Esto se llama la dificultad . Cuanto mayor sea la dificultad, más tiempo llevará obtener un hash válido.

Pero, si el hash no es válido la primera vez, algo debe cambiar en los datos que usamos. Si usamos los mismos datos una y otra vez, obtendremos el mismo hash una y otra vez y nuestro hash nunca será válido. Usamos algo llamado nonce en nuestro hash (en nuestro ejemplo anterior es la y ). Es simplemente un número que incrementamos cada vez que el hash no es válido. Obtenemos nuestros datos (fecha, mensaje, hash anterior, índice) y un nonce de 1. Si el hash que obtenemos con estos no es válido, probamos con un nonce de 2. E incrementamos el nonce hasta obtener un hash válido .

En Bitcoin, el algoritmo de Prueba de trabajo se llama Hashcash. Agreguemos una prueba de trabajo a nuestra clase Block y comencemos a minar para encontrar el nonce. Usaremos una dificultad codificada de dos ceros iniciales '00':

Ahora rediseñemos nuestra clase Block para admitir eso. Nuestro bloque CrystalCoin contendrá los siguientes atributos:

 1) index: indicates the index of the block ex: 0,1 2) timestamp: timestamp in epoch, number of seconds since 1 Jan 1970 3) data: the actual data that needs to be stored on the blockchain. 4) previous_hash: the hash of the previous block, this is the chain/link between the blocks 5) nonce: this is the number that is to be mined/found. 6) current_hash: The hash value of the current block, this is generated by combining all the above attributes and passing it to a hashing algorithm 

texto alternativo de la imagen

Crearé un módulo separado para hacer el hashing y encontrar el nonce para que mantengamos nuestro código limpio y modular. Lo llamaré proof_of_work.cr :

 require "openssl" module CrystalCoin module ProofOfWork private def proof_of_work(difficulty = "00") nonce = 0 loop do hash = calc_hash_with_nonce(nonce) if hash[0..1] == difficulty return nonce else nonce += 1 end end end private def calc_hash_with_nonce(nonce = 0) sha = OpenSSL::Digest.new("SHA256") sha.update("#{nonce}#{@index}#{@timestamp}#{@data}#{@previous_hash}") sha.hexdigest end end end

Nuestra clase Block se vería así:

 require "./proof_of_work" module CrystalCoin class Block include ProofOfWork property current_hash : String property index : Int32 property nonce : Int32 property previous_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end def self.first(data = "Genesis Block") Block.new(data: data, previous_hash: "0") end def self.next(previous_block, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_block.index + 1})", index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end

Algunas cosas a tener en cuenta sobre el código Crystal y los ejemplos del lenguaje Crystal en general. En Crystal, los métodos son públicos de forma predeterminada. Crystal requiere que cada método privado tenga el prefijo de la palabra clave privada, lo que podría resultar confuso si proviene de Ruby.

Es posible que haya notado que los tipos Integer de Crystal son Int8 , Int16 , Int32 , Int64 , UInt8 , UInt16 , UInt32 o UInt64 en comparación con el Fixnum de Ruby. true y false son valores en la clase Bool en lugar de valores en las clases TrueClass o FalseClass en Ruby.

Crystal tiene argumentos de método opcionales y con nombre como características principales del lenguaje, y no requiere escribir un código especial para manejar los argumentos, lo cual es bastante bueno. Consulte Block#initialize(index = 0, data = "data", previous_hash = "hash") y luego llámelo con algo como Block.new(data: data, previous_hash: "0") .

Para obtener una lista más detallada de las diferencias entre el lenguaje de programación Crystal y Ruby, consulte Crystal for Rubyists.

Ahora, intentemos crear cinco transacciones usando:

 blockchain = [ CrystalCoin::Block.first ] puts blockchain.inspect previous_block = blockchain[0] 5.times do |i| new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block puts new_block.inspect end
 [#<CrystalCoin::Block:0x108f8fea0 @current_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759", @index=0, @nonce=17, @timestamp=2018-05-14 17:20:46 +03:00, @data="Genesis Block", @previous_hash="0">] #<CrystalCoin::Block:0x108f8f660 @current_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0", @index=1, @nonce=24, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (1)", @previous_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759"> #<CrystalCoin::Block:0x109fc5ba0 @current_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5", @index=2, @nonce=61, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (2)", @previous_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0"> #<CrystalCoin::Block:0x109fdc300 @current_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97", @index=3, @nonce=149, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (3)", @previous_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5"> #<CrystalCoin::Block:0x109ff58a0 @current_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484", @index=4, @nonce=570, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (4)", @previous_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97"> #<CrystalCoin::Block:0x109fde120 @current_hash="00720bb6e562a25c19ecd2b277925057626edab8981ff08eb13773f9bb1cb842", @index=5, @nonce=475, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (5)", @previous_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484">

¿Ver la diferencia? Ahora todos los hashes comienzan con 00 . Esa es la magia de la prueba de trabajo. Usando ProofOfWork encontramos el nonce y la prueba es el hash con la dificultad de coincidencia, es decir, los dos ceros iniciales 00 .

Tenga en cuenta que con el primer bloque que creamos, probamos 17 nonces hasta encontrar el número de la suerte correspondiente:

Cuadra Bucles / Número de cálculos hash
#0 17
#1 24
#2 61
#3 149
#4 570
#5 475

Ahora intentemos una dificultad de cuatro ceros iniciales ( difficulty="0000" ):

Cuadra Bucles / Número de cálculos hash
#1 26 762
#2 68 419
#3 23 416
#4 15 353

En el primer bloque probó 26762 nonces (compare 17 nonces con dificultad '00') hasta encontrar el número de la suerte correspondiente.

Nuestra Blockchain como API

Hasta aquí todo bien. Creamos nuestra cadena de bloques simple y fue relativamente fácil de hacer. Pero el problema aquí es que CrystalCoin solo puede ejecutarse en una sola máquina (no está distribuida/descentralizada).

De ahora en adelante, comenzaremos a usar datos JSON para CrystalCoin . Los datos serán transacciones, por lo que el campo de datos de cada bloque será una lista de transacciones.

Cada transacción será un objeto JSON que detallará el sender de la moneda, el receiver de la moneda y la amount de CrystalCoin que se transfiere:

 { "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }

Algunas modificaciones a nuestra clase Block para admitir el nuevo formato de transaction (anteriormente denominado data ). Entonces, solo para evitar confusiones y mantener la consistencia, de ahora en adelante usaremos el término transaction para referirnos a data publicados en nuestra aplicación de ejemplo.

Presentaremos una nueva Transaction de clase simple:

 module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end

Las transacciones se empaquetan en bloques, por lo que un bloque puede contener solo una o varias transacciones. Los bloques que contienen las transacciones se generan con frecuencia y se agregan a la cadena de bloques.

Se supone que la cadena de bloques es una colección de bloques. Podemos almacenar todos los bloques en la lista Crystal, y es por eso que presentamos la nueva clase Blockchain :

Blockchain tendrá matrices de chain y uncommitted_transactions . La chain incluirá todos los bloques minados en la cadena de bloques, y uncommitted_transactions tendrá todas las transacciones que no se han agregado a la cadena de bloques (aún no minadas). Una vez que inicializamos Blockchain , creamos el bloque de génesis usando Block.first y lo agregamos a la matriz de chain , y agregamos una matriz vacía de uncommitted_transactions .

Crearemos el método Blockchain#add_transaction para agregar transacciones a la matriz uncommitted_transactions .

Construyamos nuestra nueva clase Blockchain :

 require "./block" require "./transaction" module CrystalCoin class Blockchain getter chain getter uncommitted_transactions def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction end def add_transaction(transaction) @uncommitted_transactions << transaction end end end

En la clase Block , comenzaremos a usar transactions en lugar de data :

 module CrystalCoin class Block include ProofOfWork def initialize(index = 0, transactions = [] of Transaction, previous_hash = "hash") @transactions = transactions ... end .... def self.next(previous_block, transactions = [] of Transaction) Block.new( transactions: transactions, index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end

Ahora que sabemos cómo se verán nuestras transacciones, necesitamos una forma de agregarlas a una de las computadoras en nuestra red blockchain, llamada node . Para hacer eso, crearemos un servidor HTTP simple.

Crearemos cuatro puntos finales:

  • [POST] /transactions/new : para crear una nueva transacción en un bloque
  • [GET] /mine : para decirle a nuestro servidor que extraiga un nuevo bloque.
  • [GET] /chain : para devolver la cadena de bloques completa en formato JSON .
  • [GET] /pending : para devolver las transacciones pendientes ( uncommitted_transactions ).

Vamos a utilizar el marco web de Kemal. Es un micromarco que facilita la asignación de puntos finales a las funciones de Crystal. Kemal está fuertemente influenciado por Sinatra para Rubyists y funciona de manera muy similar. Si está buscando el equivalente de Ruby on Rails, consulte Amber.

Nuestro servidor formará un solo nodo en nuestra red blockchain. Primero agreguemos Kemal al archivo shard.yml como e instalemos la dependencia:

 dependencies: kemal: github: kemalcr/kemal

Ahora construyamos el esqueleto de nuestro servidor HTTP:

 # src/server.cr require "kemal" require "./crystal_coin" # Generate a globally unique address for this node node_identifier = UUID.random.to_s # Create our Blockchain blockchain = Blockchain.new get "/chain" do "Send the blockchain as json objects" end get "/mine" do "We'll mine a new Block" end get "/pending" do "Send pending transactions as json objects" end post "/transactions/new" do "We'll add a new transaction" end Kemal.run

Y ejecuta el servidor:

 crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000

Asegurémonos de que el servidor funciona bien:

 % curl http://0.0.0.0:3000/chain Send the blockchain as json objects%

Hasta aquí todo bien. Ahora, podemos proceder con la implementación de cada uno de los puntos finales. Comencemos implementando /transactions/new y pending :

 get "/pending" do { transactions: blockchain.uncommitted_transactions }.to_json end post "/transactions/new" do |env| transaction = CrystalCoin::Block::Transaction.new( from: env.params.json["from"].as(String), to: env.params.json["to"].as(String), amount: env.params.json["amount"].as(Int64) ) blockchain.add_transaction(transaction) "Transaction #{transaction} has been added to the node" end

Implementación sencilla. Simplemente creamos un objeto CrystalCoin::Block::Transaction y agregamos la transacción a la matriz uncommitted_transactions usando Blockchain#add_transaction .

Por el momento, las transacciones se almacenan inicialmente en un grupo de transacciones uncommitted_transactions . El proceso de colocar las transacciones no confirmadas en un bloque y calcular la Prueba de trabajo (PoW) se conoce como minería de bloques. Una vez que se determina el nonce que satisface nuestras restricciones, podemos decir que se ha extraído un bloque y el nuevo bloque se coloca en la cadena de bloques.

En CrystalCoin , usaremos el algoritmo de prueba de trabajo simple que creamos anteriormente. Para crear un nuevo bloque, la computadora de un minero deberá:

  • Encuentra el último bloque de la chain .
  • Encuentra transacciones pendientes ( uncommitted_transactions ).
  • Crea un nuevo bloque usando Block.next .
  • Agregue el bloque extraído a la matriz de chain .
  • Limpie la matriz de uncommitted_transactions .

Entonces, para implementar el punto final de /mine , primero implementemos los pasos anteriores en Blockchain#mine :

 module CrystalCoin class Blockchain include Consensus BLOCK_SIZE = 25 ... def mine raise "No transactions to be mined" if @uncommitted_transactions.empty? new_block = Block.next( previous_block: @chain.last, transactions: @uncommitted_transactions.shift(BLOCK_SIZE) ) @chain << new_block end end end

Primero nos aseguramos de tener algunas transacciones pendientes para extraer. Luego obtenemos el último bloque usando @chain.last y las primeras 25 transacciones que se extraerán (estamos usando Array#shift(BLOCK_SIZE) para devolver una matriz de las primeras 25 transacciones uncommitted_transactions y luego eliminamos los elementos que comienzan en el índice 0) .

Ahora implementemos el punto final /mine :

 get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end

Y para el punto final de la /chain :

 get "/chain" do { chain: blockchain.chain }.to_json end

Interactuando con nuestra Blockchain

Usaremos cURL para interactuar con nuestra API a través de una red.

Primero, encendamos el servidor:

 crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000

Luego, creemos dos nuevas transacciones haciendo una solicitud POST a http://localhost:3000/transactions/new con un cuerpo que contenga nuestra estructura de transacción:

 crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"iron_man", "amount": 1000}' Transaction #<CrystalCoin::Block::Transaction:0x10c4159f0 @from="eki", @to="iron_man", @amount=1000_i64> has been added to the node% crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"hulk", "amount": 700}' Transaction #<CrystalCoin::Block::Transaction:0x10c415810 @from="eki", @to="hulk", @amount=700_i64> has been added to the node%

Ahora enumeremos las transacciones pendientes (es decir, las transacciones que aún no se han agregado al bloque):

 crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }

Como podemos ver, las dos transacciones que creamos anteriormente se agregaron a uncommitted_transactions .

Ahora extraigamos las dos transacciones haciendo una solicitud GET a http://0.0.0.0:3000/mine :

 crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.

Parece que extrajimos con éxito el primer bloque y lo agregamos a nuestra chain . Verifiquemos dos veces nuestra chain y si incluye el bloque minado:

 crystal_coin [master●] % curl http://0.0.0.0:3000/chain { "chain": [ { "index": 0, "current_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "nonce": 363, "previous_hash": "0", "transactions": [ ], "timestamp": "2018-05-23T01:59:52+0300" }, { "index": 1, "current_hash": "003c05da32d3672670ba1e25ecb067b5bc407e1d5f8733b5e33d1039de1a9bf1", "nonce": 320, "previous_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "transactions": [ { "from": "ekis", "to": "huslks", "amount": 7090 }, { "from": "ekis", "to": "huslks", "amount": 70900 } ], "timestamp": "2018-05-23T02:02:38+0300" } ] }

Consenso y Descentralización

esto es genial Obtuvimos una cadena de bloques básica que acepta transacciones y nos permite extraer nuevos bloques. Pero el código que hemos implementado hasta ahora está destinado a ejecutarse en una sola computadora, mientras que el objetivo de las cadenas de bloques es que deben estar descentralizadas. Pero si están descentralizados, ¿cómo nos aseguramos de que todos reflejen la misma cadena?

Este es el problema del Consensus .

Tendremos que implementar un algoritmo de consenso si queremos más de un nodo en nuestra red.

Registro de nuevos nodos

Para implementar un algoritmo de consenso, necesitamos una forma de informar a un nodo sobre los nodos vecinos en la red. Cada nodo de nuestra red debe mantener un registro de otros nodos de la red. Por lo tanto, necesitaremos más puntos finales:

  • [POST] /nodes/register : para aceptar una lista de nuevos nodos en forma de URL.
  • [GET] /nodes/resolve : para implementar nuestro algoritmo de consenso, que resuelve cualquier conflicto, para garantizar que un nodo tenga la cadena correcta.

Necesitamos modificar el constructor de nuestra cadena de bloques y proporcionar un método para registrar nodos:

 --- a/src/crystal_coin/blockchain.cr +++ b/src/crystal_coin/blockchain.cr @@ -7,10 +7,12 @@ module CrystalCoin getter chain getter uncommitted_transactions + getter nodes def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction + @nodes = Set(String).new [] of String end def add_transaction(transaction)

Tenga en cuenta que hemos utilizado una estructura de datos Set con tipo String para contener la lista de nodos. Esta es una forma barata de asegurar que la adición de nuevos nodos sea idempotente y que no importa cuántas veces agreguemos un nodo específico, aparecerá exactamente una vez.

Ahora agreguemos un nuevo módulo a Consensus e implementemos el primer método register_node(address) :

 require "uri" module CrystalCoin module Consensus def register_node(address : String) uri = URI.parse(address) node_address = "#{uri.scheme}:://#{uri.host}" node_address = "#{node_address}:#{uri.port}" unless uri.port.nil? @nodes.add(node_address) rescue raise "Invalid URL" end end end

La función register_node analizará la URL del nodo y la formateará.

Y aquí vamos a crear /nodes/register punto final:

 post "/nodes/register" do |env| nodes = env.params.json["nodes"].as(Array) raise "Empty array" if nodes.empty? nodes.each do |node| blockchain.register_node(node.to_s) end "New nodes have been added: #{blockchain.nodes}" end

Ahora, con esta implementación, podríamos enfrentar un problema con múltiples nodos. La copia de cadenas de algunos nodos puede diferir. En ese caso, debemos acordar alguna versión de la cadena para mantener la integridad de todo el sistema. Necesitamos llegar a un consenso.

Para resolver esto, estableceremos la regla de que la cadena válida más larga es la que se utilizará. Usando este algoritmo, llegamos a un consenso entre los nodos de nuestra red. La razón detrás de este enfoque es que la cadena más larga es una buena estimación de la mayor cantidad de trabajo realizado.

texto alternativo de la imagen

 module CrystalCoin module Consensus ... def resolve updated = false @nodes.each do |node| node_chain = parse_chain(node) return unless node_chain.size > @chain.size return unless valid_chain?(node_chain) @chain = node_chain updated = true rescue IO::Timeout puts "Timeout!" end updated end ... end end

Tenga en cuenta que resolve es un método que recorre todos nuestros nodos vecinos, descarga sus cadenas y las verifica usando valid_chain? método. If a valid chain is found, whose length is greater than ours, we replace ours.

Now let's implement parse_chain() and valid_chain?() private methods:

 module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end

For parse_chain() we:

  • Issue a GET HTTP request using HTTP::Client.get to /chain end-point.
  • Parse the /chain JSON response using JSON.parse .
  • Extract an array of CrystalCoin::Block objects from the JSON blob that was returned using Array(CrystalCoin::Block).from_json(node_chain) .

There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type) functionality that gives us the following:

  • A way to create an instance of that class from a JSON string by running Class.from_json .
  • A way to serialize an instance of that class into a JSON string by running instance.to_json .
  • Getters and setters for keys defined in that class.

In our case, we had to define JSON.mapping in CrystalCoin::Block object, and we removed property usage in the class, like so:

 module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end

Now for Blockchain#valid_chain? , we iterate through all of the blocks, and for each we:

  • Recalculate the hash for the block using Block#recalculate_hash and check that the hash of the block is correct:
 module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
  • Check each of the blocks linked correctly with their previous hashes.
  • Check the block's hash is valid for the number of zeros ( difficulty in our case 00 ).

And finally we implement /nodes/resolve end-point:

 get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end

It's done! You can find the final code on GitHub.

The structure of our project should look like this:

 crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr

Let's Try it Out

  • Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes: http://localhost:3000 and http://localhost:3001 .
  • Register the second node address to the first node using:
 crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
  • Let's add a transaction to the second node:
 crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
  • Let's mine transactions into a block at the second node:
 crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
  • At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
 crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
 crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
  • Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
 crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%

Let's check if the chain in the first node has updated:

 crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}% 

texto alternativo de la imagen

¡Hurra! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.

Terminando

This Crystal language tutorial covered the fundamentals of a public blockchain. If you followed along, you implemented a blockchain from scratch and built a simple application allowing users to share information on the blockchain.

We've made a fairly sized blockchain at this point. Now, CrystalCoin can be launched on multiple machines to create a network, and real CrystalCoins can be mined.

I hope that this has inspired you to create something new, or at the very least take a closer look at Crystal programming.

Note: The code in this tutorial is not ready for real-world use. Please refer to this as a general guide only.