Criando uma criptomoeda na linguagem de programação Crystal

Publicados: 2022-03-11

Este post é minha tentativa de entender os principais aspectos do blockchain explorando os internos. Comecei lendo o whitepaper original do bitcoin, mas senti que a única maneira de realmente entender o blockchain é construindo uma nova criptomoeda do zero.

Por isso decidi criar uma criptomoeda usando a nova linguagem de programação Crystal, e a chamei de CrystalCoin . Este artigo não discutirá escolhas de algoritmos, dificuldade de hash ou tópicos semelhantes. Em vez disso, o foco será detalhar um exemplo concreto, que deve fornecer uma compreensão mais profunda e prática dos pontos fortes e limitações das blockchains.

Se você ainda não leu, para mais informações sobre algoritmos e hashing, sugiro que dê uma olhada no artigo de Demir Selmanovic Cryptocurrency for Dummies: Bitcoin and Beyond.

Por que eu escolhi a linguagem de programação Crystal

Para uma melhor demonstração, eu queria usar uma linguagem produtiva como Ruby sem comprometer o desempenho. A criptomoeda tem muitos cálculos demorados (nomeadamente mineração e hashing ), e é por isso que linguagens compiladas como C++ e Java são as linguagens de escolha para construir criptomoedas “reais”. Dito isso, eu queria usar uma linguagem com uma sintaxe mais limpa para manter o desenvolvimento divertido e permitir uma melhor legibilidade. O desempenho do cristal tende a ser bom de qualquer maneira.

Ilustração da linguagem de programação Crystal

Então, por que decidi usar a linguagem de programação Crystal? A sintaxe do Crystal é fortemente inspirada na do Ruby, então, para mim, parece natural de ler e fácil de escrever. Ele tem o benefício adicional de uma curva de aprendizado mais baixa, especialmente para desenvolvedores Ruby experientes.

É assim que a equipe do Crystal lang coloca em seu site oficial:

Rápido como C, liso como Ruby.

No entanto, ao contrário de Ruby ou JavaScript, que são linguagens interpretadas, Crystal é uma linguagem compilada, tornando-a muito mais rápida e oferecendo um consumo de memória menor. Sob o capô, ele usa LLVM para compilar para código nativo.

O Crystal também é tipado estaticamente, o que significa que o compilador o ajudará a detectar erros de tipo em tempo de compilação.

Não vou explicar por que considero a linguagem Crystal incrível porque está além do escopo deste artigo, mas se você não achar meu otimismo convincente, fique à vontade para conferir este artigo para obter uma visão geral melhor do potencial do Crystal.

Observação: este artigo pressupõe que você já tenha um conhecimento básico de programação orientada a objetos (OOP).

Blockchain

Então, o que é uma blockchain? É uma lista (cadeia) de blocos vinculados e protegidos por impressões digitais (também conhecidas como hashes de criptografia).

A maneira mais fácil de pensar nisso é como uma estrutura de dados de lista vinculada. Dito isto, uma lista encadeada só precisava ter uma referência ao elemento anterior; um bloco deve ter um identificador dependendo do identificador do bloco anterior, o que significa que você não pode substituir um bloco sem recalcular cada bloco que vem depois.

Por enquanto, pense no blockchain como uma série de blocos com alguns dados vinculados a uma cadeia, sendo a cadeia o hash do bloco anterior.

Todo o blockchain existiria em cada nó que deseja interagir com ele, ou seja, é copiado em cada um dos nós da rede. Nenhum servidor único o hospeda, mas todas as empresas de desenvolvimento de blockchain o usam, o que o torna descentralizado .

Sim, isso é estranho comparado aos sistemas centralizados convencionais. Cada um dos nós terá uma cópia de todo o blockchain (> 149 Gb em blockchain Bitcoin até dezembro de 2017).

Hash e assinatura digital

Então, o que é essa função hash? Pense no hash como uma função, que retorna uma impressão digital única quando damos a ele um texto/objeto. Mesmo a menor alteração no objeto de entrada alteraria drasticamente a impressão digital.

Existem diferentes algoritmos de hash e, neste artigo, usaremos o algoritmo de hash SHA256 , que é o usado no Bitcoin .

Usando SHA256 , sempre resultaremos em 64 caracteres hexadecimais (256 bits) de comprimento, mesmo que a entrada seja menor que 256 bits ou muito maior que 256 bits:

Entrada Resultados com hash
TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGO TEXTO MUITO LONGE TEXTO MUITO LONGO TEXTO MUITO LONGO cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
Total 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
Topo. 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

Observe com o último exemplo, que apenas adicionar um arquivo . (ponto) resultou em uma mudança dramática no hash.

Portanto, em uma blockchain, a cadeia é construída passando os dados do bloco para um algoritmo de hash que geraria um hash, que é vinculado ao próximo bloco, formando, a partir de então, uma série de blocos vinculados aos hashes dos blocos anteriores.

Construindo uma criptomoeda em Crystal

Agora vamos começar a criar nosso projeto Crystal e construir nossa criptografia SHA256 .

Supondo que você tenha a linguagem de programação Crystal instalada, vamos criar o esqueleto da base de código CrystalCoin usando o 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 irá criar a estrutura básica para o projeto, com um repositório git já inicializado, licença e arquivos leia-me. Ele também vem com stubs para testes e o arquivo shard.yml para descrever o projeto e gerenciar dependências, também conhecidos como shards.

Vamos adicionar o fragmento openssl , que é necessário para construir o algoritmo SHA256 :

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

Uma vez que está dentro, volte para o seu terminal e execute crystal deps . Fazer isso irá baixar o openssl e suas dependências para nós utilizarmos.

Agora que temos a biblioteca necessária instalada em nosso código, vamos começar definindo a classe Block e então construindo a função 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

Agora você pode testar seu aplicativo executando crystal run crystal src/crystal_coin/block.cr em seu terminal.

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

Projetando nosso Blockchain

Cada bloco é armazenado com um timestamp e, opcionalmente, um index . Em CrystalCoin , vamos armazenar ambos. Para ajudar a garantir a integridade em todo o blockchain, cada bloco terá um hash autoidentificado. Assim como o Bitcoin, o hash de cada bloco será um hash criptográfico do bloco ( index , timestamp , data e o hash do hash do bloco anterior previous_hash ). Os dados podem ser o que você quiser por enquanto.

 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

No Crystal lang, substituímos os métodos attr_accessor , attr_getter e attr_setter do Ruby por novas palavras-chave:

Palavra-chave Ruby Palavra-chave Cristal
attr_accessor propriedade
attr_reader pegador
attr_writer normatizador

Outra coisa que você deve ter notado no Crystal é que queremos dar dicas ao compilador sobre tipos específicos através do nosso código. O Crystal infere os tipos, mas sempre que houver ambiguidade, você também pode declarar tipos explicitamente. É por isso que adicionamos tipos String para current_hash .

Agora vamos executar block.cr duas vezes e observar que os mesmos dados irão gerar hashes diferentes por causa do timestamp diferente:

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

Agora temos nossa estrutura de blocos, mas estamos criando uma blockchain. Precisamos começar a adicionar blocos para formar uma corrente real. Como mencionei anteriormente, cada bloco requer informações do bloco anterior. Mas como o primeiro bloco no blockchain chega lá? Bem, o primeiro bloco, ou bloco de genesis , é um bloco especial (um bloco sem predecessores). Em muitos casos, é adicionado manualmente ou possui uma lógica exclusiva que permite que seja adicionado.

Vamos criar uma nova função que retorna um bloco de gênese. Este bloco é de index=0 e possui um valor de dados arbitrário e um valor arbitrário no parâmetro previous_hash .

Vamos construir ou classe o método Block.first que gera o bloco genesis:

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

E vamos testá-lo 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">

Agora que podemos criar um bloco de gênese , precisamos de uma função que gere blocos sucessivos no blockchain.

Esta função tomará como parâmetro o bloco anterior da cadeia, criará os dados para o bloco a ser gerado e retornará o novo bloco com os dados apropriados. Quando novos blocos fazem hash de informações de blocos anteriores, a integridade do blockchain aumenta a cada novo bloco.

Uma consequência importante é que um bloco não pode ser modificado sem alterar o hash de cada bloco consecutivo. Isso é demonstrado no exemplo abaixo. Se os dados no bloco 44 forem alterados de LOOP para EAST , todos os hashes dos blocos consecutivos devem ser alterados. Isso porque o hash do bloco depende do valor do previous_hash (entre outras coisas).

Diagrama de hash de criptomoeda de cristal

Se não fizéssemos isso, seria mais fácil para uma parte externa alterar os dados e substituir nossa cadeia por uma inteiramente nova. Essa cadeia de hashes atua como prova criptográfica e ajuda a garantir que, uma vez que um bloco seja adicionado ao blockchain, ele não possa ser substituído ou removido. Vamos criar o método de classe 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 experimentar todos juntos, criaremos um blockchain simples. O primeiro elemento da lista é o bloco de gênese. E, claro, precisamos adicionar os blocos seguintes. Criaremos cinco novos blocos para demonstrar o 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)">

Prova de Trabalho

Um algoritmo de Prova de Trabalho (PoW) é como novos blocos são criados ou extraídos no blockchain. O objetivo do PoW é descobrir um número que resolva um problema. O número deve ser difícil de encontrar, mas fácil de verificar computacionalmente por qualquer pessoa na rede. Esta é a ideia central por trás da Prova de Trabalho.

Vamos demonstrar com um exemplo para garantir que tudo esteja claro. Vamos supor que o hash de algum inteiro x multiplicado por outro y deve começar com 00 . Assim:

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

E para este exemplo simplificado, vamos corrigir x=5 e implementar isso no 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)}"

Vamos executar o 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 você pode ver, esse número y=530 foi difícil de encontrar (força bruta), mas fácil de verificar usando a função hash.

Por que se preocupar com este algoritmo PoW? Por que não criamos apenas um hash por bloco e pronto? Um hash deve ser válido . No nosso caso, um hash será válido se os dois primeiros caracteres do nosso hash forem 00 . Se nosso hash começar com 00...... , ele é considerado válido. Isso é chamado de dificuldade . Quanto maior a dificuldade, mais tempo leva para obter um hash válido.

Mas, se o hash não for válido na primeira vez, algo deve mudar nos dados que usamos. Se usarmos os mesmos dados repetidamente, obteremos o mesmo hash repetidamente e nosso hash nunca será válido. Usamos algo chamado nonce em nosso hash (em nosso exemplo anterior é o y ). É simplesmente um número que incrementamos cada vez que o hash não é válido. Obtemos nossos dados (data, mensagem, hash anterior, índice) e um nonce de 1. Se o hash obtido com eles não for válido, tentamos com um nonce de 2. E incrementamos o nonce até obtermos um hash válido .

No Bitcoin, o algoritmo Proof of Work é chamado de Hashcash. Vamos adicionar uma prova de trabalho à nossa classe Block e começar a minerar para encontrar o nonce. Usaremos uma dificuldade codificada de dois zeros à esquerda '00':

Agora vamos redesenhar nossa classe Block para dar suporte a isso. Nosso bloco CrystalCoin conterá os seguintes 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 da imagem

Vou criar um módulo separado para fazer o hash e encontrar o nonce para mantermos nosso código limpo e modular. Vou chamá-lo de 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

Nossa classe Block seria algo como:

 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

Poucas coisas a serem observadas sobre o código Crystal e os exemplos da linguagem Crystal em geral. No Crystal, os métodos são públicos por padrão. O Crystal requer que cada método privado seja prefixado com a palavra-chave privada, o que pode ser confuso vindo do Ruby.

Você deve ter notado que os tipos Integer do Crystal são Int8 , Int16 , Int32 , Int64 , UInt8 , UInt16 , UInt32 ou UInt64 comparados ao Fixnum do Ruby. true e false são valores na classe Bool em vez de valores nas classes TrueClass ou FalseClass em Ruby.

O Crystal tem argumentos de método opcionais e nomeados como recursos principais da linguagem e não requer a escrita de código especial para lidar com os argumentos, o que é muito legal. Confira Block#initialize(index = 0, data = "data", previous_hash = "hash") e, em seguida, chame-o com algo como Block.new(data: data, previous_hash: "0") .

Para uma lista mais detalhada das diferenças entre as linguagens de programação Crystal e Ruby, confira Crystal for Rubyists.

Agora, vamos tentar criar cinco transações 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">

Veja a diferença? Agora todos os hashes começam com 00 . Essa é a mágica da prova de trabalho. Usando ProofOfWork encontramos o nonce e a prova é o hash com a dificuldade correspondente, ou seja, os dois zeros à esquerda 00 .

Observe que com o primeiro bloco que criamos, tentamos 17 nonces até encontrar o número da sorte correspondente:

Bloquear Loops / Número de Cálculos de Hash
#0 17
#1 24
#2 61
#3 149
#4 570
#5 475

Agora vamos tentar uma dificuldade de quatro zeros à esquerda ( difficulty="0000" ):

Bloquear Loops / Número de Cálculos de Hash
#1 26 762
#2 68 419
#3 23 416
#4 15 353

No primeiro bloco tentei 26762 nonces (compare 17 nonces com dificuldade '00') até encontrar o número da sorte correspondente.

Nosso Blockchain como uma API

Até agora tudo bem. Criamos nosso blockchain simples e foi relativamente fácil de fazer. Mas o problema aqui é que o CrystalCoin só pode ser executado em uma única máquina (não é distribuído/descentralizado).

A partir de agora, começaremos a usar dados JSON para CrystalCoin . Os dados serão transações, então o campo de dados de cada bloco será uma lista de transações.

Cada transação será um objeto JSON detalhando o sender da moeda, o receiver da moeda e a amount de CrystalCoin que está sendo transferida:

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

Algumas modificações em nossa classe Block para suportar o novo formato de transaction (anteriormente chamado data ). Portanto, apenas para evitar confusão e manter a consistência, usaremos o termo transaction para nos referirmos aos data postados em nosso aplicativo de exemplo a partir de agora.

Apresentaremos uma nova classe simples Transaction :

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

As transações são empacotadas em blocos, de modo que um bloco pode conter apenas uma ou várias transações. Blocos contendo as transações são gerados com frequência e adicionados ao blockchain.

O blockchain deve ser uma coleção de blocos. Podemos armazenar todos os blocos na lista Crystal, e é por isso que apresentamos a nova classe Blockchain :

Blockchain terá arrays chain e uncommitted_transactions . A chain incluirá todos os blocos minerados no blockchain e uncommitted_transactions terá todas as transações que não foram adicionadas ao blockchain (ainda não mineradas). Uma vez que inicializamos o Blockchain , criamos o bloco genesis usando Block.first e o adicionamos ao array chain , e adicionamos um array uncommitted_transactions vazio.

Vamos criar o método Blockchain#add_transaction para adicionar transações ao array uncommitted_transactions .

Vamos construir nossa nova classe 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

Na classe Block , começaremos a usar transactions em vez 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

Agora que sabemos como serão nossas transações, precisamos de uma maneira de adicioná-las a um dos computadores em nossa rede blockchain, chamado node . Para fazer isso, vamos criar um servidor HTTP simples.

Criaremos quatro endpoints:

  • [POST] /transactions/new : para criar uma nova transação para um bloco
  • [GET] /mine : para dizer ao nosso servidor para minerar um novo bloco.
  • [GET] /chain : para retornar o blockchain completo no formato JSON .
  • [GET] /pending : para retornar as transações pendentes ( uncommitted_transactions ).

Vamos usar o framework web Kemal. É uma microestrutura que facilita o mapeamento de endpoints para funções Crystal. Kemal é fortemente influenciado pelo Sinatra for Rubyists e funciona de maneira muito semelhante. Se você está procurando o equivalente a Ruby on Rails, confira Amber.

Nosso servidor formará um único nó em nossa rede blockchain. Vamos primeiro adicionar Kemal ao arquivo shard.yml como a e instalar a dependência:

 dependencies: kemal: github: kemalcr/kemal

Agora vamos construir o esqueleto do nosso 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

E execute o servidor:

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

Vamos verificar se o servidor está funcionando bem:

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

Até agora tudo bem. Agora, podemos prosseguir com a implementação de cada um dos endpoints. Vamos começar implementando /transactions/new e 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

Implementação direta. Nós apenas criamos um objeto CrystalCoin::Block::Transaction e adicionamos a transação ao array uncommitted_transactions usando Blockchain#add_transaction .

No momento, as transações são inicialmente armazenadas em um pool de uncommitted_transactions . O processo de colocar as transações não confirmadas em um bloco e computar a Prova de Trabalho (PoW) é conhecido como mineração de blocos. Uma vez que o nonce que satisfaz nossas restrições é descoberto, podemos dizer que um bloco foi minerado e o novo bloco é colocado no blockchain.

No CrystalCoin , usaremos o algoritmo simples de prova de trabalho que criamos anteriormente. Para criar um novo bloco, o computador de um minerador terá que:

  • Encontre o último bloco da chain .
  • Encontre transações pendentes ( uncommitted_transactions ).
  • Crie um novo bloco usando Block.next .
  • Adicione o bloco minerado ao array chain .
  • Limpe a matriz uncommitted_transactions .

Então, para implementar o ponto final /mine , vamos primeiro implementar as etapas acima em 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

Garantimos primeiro que temos algumas transações pendentes para minerar. Em seguida, obtemos o último bloco usando @chain.last e as primeiras 25 transações a serem extraídas (estamos usando Array#shift(BLOCK_SIZE) para retornar uma matriz das primeiras 25 uncommitted_transactions e, em seguida, removemos os elementos começando no índice 0) .

Agora vamos implementar o ponto final /mine :

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

E para o ponto final /chain :

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

Interagindo com nosso Blockchain

Usaremos cURL para interagir com nossa API em uma rede.

Primeiro, vamos iniciar o servidor:

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

Então vamos criar duas novas transações fazendo uma requisição POST para http://localhost:3000/transactions/new com um corpo contendo nossa estrutura de transação:

 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%

Agora vamos listar as transações pendentes (ou seja, transações que ainda não foram adicionadas ao bloco):

 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, as duas transações que criamos anteriormente foram adicionadas a uncommitted_transactions .

Agora vamos minerar as duas transações fazendo uma solicitação GET para 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 extraímos com sucesso o primeiro bloco e o adicionamos à nossa chain . Vamos verificar novamente nossa chain e se ela inclui o bloco minerado:

 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 e Descentralização

Isso é legal. Conseguimos um blockchain básico que aceita transações e nos permite minerar novos blocos. Mas o código que implementamos até agora deve ser executado em um único computador, enquanto o objetivo das blockchains é que elas devem ser descentralizadas. Mas se eles são descentralizados, como garantimos que todos reflitam a mesma cadeia?

Este é o problema do Consensus .

Teremos que implementar um algoritmo de consenso se quisermos mais de um nó em nossa rede.

Registrando novos nós

Para implementar um algoritmo de consenso, precisamos de uma maneira de informar um nó sobre os nós vizinhos na rede. Cada nó em nossa rede deve manter um registro de outros nós na rede. Portanto, precisaremos de mais endpoints:

  • [POST] /nodes/register : para aceitar uma lista de novos nós na forma de URLs.
  • [GET] /nodes/resolve : para implementar nosso Algoritmo de Consenso, que resolve quaisquer conflitos—para garantir que um nó tenha a cadeia correta.

Precisamos modificar o construtor do nosso blockchain e fornecer um método para registrar nós:

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

Observe que usamos uma estrutura de dados Set com o tipo String para manter a lista de nós. Essa é uma maneira barata de garantir que a adição de novos nós seja idempotente e que, não importa quantas vezes adicionemos um nó específico, ele apareça exatamente uma vez.

Agora vamos adicionar um novo módulo ao Consensus e implementar o primeiro 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

A função register_node analisará a URL do nó e a formatará.

E aqui vamos criar /nodes/register end-point:

 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

Agora, com esta implementação, podemos enfrentar um problema com vários nós. A cópia das cadeias de alguns nós pode ser diferente. Nesse caso, precisamos concordar com alguma versão da cadeia para manter a integridade de todo o sistema. Precisamos chegar a um consenso.

Para resolver isso, vamos fazer a regra de que a cadeia válida mais longa é aquela a ser usada. Usando este algoritmo, chegamos a um consenso entre os nós em nossa rede. A razão por trás dessa abordagem é que a cadeia mais longa é uma boa estimativa da maior quantidade de trabalho realizado.

texto alternativo da imagem

 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

Tenha em mente que resolve é um método que percorre todos os nossos nós vizinhos, baixa suas cadeias e as verifica usando o 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 da imagem

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

Empacotando

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.