Creazione di una criptovaluta nel linguaggio di programmazione Crystal

Pubblicato: 2022-03-11

Questo post è il mio tentativo di comprendere gli aspetti chiave della blockchain esplorando gli interni. Ho iniziato leggendo il whitepaper originale sui bitcoin, ma ho sentito che l'unico modo per capire veramente la blockchain è costruire una nuova criptovaluta da zero.

Ecco perché ho deciso di creare una criptovaluta utilizzando il nuovo linguaggio di programmazione Crystal, e l'ho soprannominata CrystalCoin . Questo articolo non discuterà le scelte dell'algoritmo, la difficoltà dell'hash o argomenti simili. Invece, l'attenzione si concentrerà sul dettaglio di un esempio concreto, che dovrebbe fornire una comprensione più profonda e pratica dei punti di forza e dei limiti delle blockchain.

Se non l'hai ancora letto, per ulteriori informazioni su algoritmi e hashing, ti suggerisco di dare un'occhiata all'articolo di Demir Selmanovic Cryptocurrency for Dummies: Bitcoin and Beyond.

Perché ho scelto il linguaggio di programmazione Crystal

Per una migliore dimostrazione, volevo usare un linguaggio produttivo come Ruby senza compromettere le prestazioni. La criptovaluta richiede molti calcoli dispendiosi in termini di tempo (vale a dire mining e hashing ), ed è per questo che linguaggi compilati come C++ e Java sono i linguaggi preferiti per la creazione di criptovalute "reali". Detto questo, volevo usare un linguaggio con una sintassi più pulita in modo da poter mantenere lo sviluppo divertente e consentire una migliore leggibilità. Le prestazioni dei cristalli tendono comunque ad essere buone.

Illustrazione del linguaggio di programmazione Crystal

Allora, perché ho deciso di utilizzare il linguaggio di programmazione Crystal? La sintassi di Crystal è fortemente ispirata a quella di Ruby, quindi per me è naturale da leggere e facile da scrivere. Ha l'ulteriore vantaggio di una curva di apprendimento inferiore, soprattutto per gli sviluppatori Ruby esperti.

Questo è il modo in cui il team di Crystal lang lo mette sul loro sito ufficiale:

Veloce come C, lucido come Ruby.

Tuttavia, a differenza di Ruby o JavaScript, che sono linguaggi interpretati, Crystal è un linguaggio compilato, che lo rende molto più veloce e offre un footprint di memoria inferiore. Sotto il cofano, utilizza LLVM per la compilazione in codice nativo.

Crystal è anche tipizzato staticamente, il che significa che il compilatore ti aiuterà a rilevare gli errori di tipo in fase di compilazione.

Non spiegherò perché considero fantastico il linguaggio Crystal perché va oltre lo scopo di questo articolo, ma se non trovi il mio ottimismo convincente, sentiti libero di dare un'occhiata a questo articolo per una migliore panoramica del potenziale di Crystal.

Nota: questo articolo presuppone che tu abbia già una conoscenza di base della programmazione orientata agli oggetti (OOP).

Blockchain

Allora, cos'è una blockchain? È un elenco (catena) di blocchi collegati e protetti da impronte digitali (note anche come hash crittografici).

Il modo più semplice per pensarlo è come una struttura di dati di elenchi collegati. Detto questo, un elenco collegato richiedeva solo un riferimento all'elemento precedente; un blocco deve avere un identificatore che dipende dall'identificatore del blocco precedente, il che significa che non puoi sostituire un blocco senza ricalcolare ogni singolo blocco successivo.

Per ora, pensa alla blockchain come a una serie di blocchi con alcuni dati collegati a una catena, essendo la catena l'hash del blocco precedente.

L'intera blockchain esisterebbe su ogni nodo che vuole interagire con essa, il che significa che viene copiata su ciascuno dei nodi della rete. Nessun singolo server lo ospita, eppure tutte le società di sviluppo blockchain lo utilizzano, il che lo rende decentralizzato .

Sì, questo è strano rispetto ai sistemi centralizzati convenzionali. Ciascuno dei nodi avrà una copia dell'intera blockchain (> 149 Gb in blockchain Bitcoin entro dicembre 2017).

Hashing e Firma Digitale

Allora, qual è questa funzione hash? Pensa all'hash come a una funzione, che restituisce un'impronta digitale univoca quando gli diamo un testo/oggetto. Anche il più piccolo cambiamento nell'oggetto di input cambierebbe drasticamente l'impronta digitale.

Esistono diversi algoritmi di hashing e in questo articolo utilizzeremo l'algoritmo di hash SHA256 , che è quello utilizzato in Bitcoin .

Usando SHA256 otterremo sempre 64 caratteri esadecimali (256 bit) anche se l'input è inferiore a 256 bit o molto più grande di 256 bit:

Ingresso Risultati hash
TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO TESTO MOLTO LUNGO cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
Toptale 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
Toptale. 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

Nota con l'ultimo esempio, che basta aggiungere un . (punto) ha comportato un cambiamento drammatico nell'hash.

Pertanto, in una blockchain, la catena viene costruita passando i dati del blocco in un algoritmo di hashing che genererebbe un hash, che viene collegato al blocco successivo, formando d'ora in poi una serie di blocchi collegati con gli hash dei blocchi precedenti.

Costruire una criptovaluta in cristallo

Ora iniziamo a creare il nostro progetto Crystal e costruiamo la nostra crittografia SHA256 .

Supponendo che tu abbia installato il linguaggio di programmazione Crystal, creiamo lo scheletro della base di codice CrystalCoin utilizzando l' 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/

Questo comando creerà la struttura di base per il progetto, con un repository git già inizializzato, licenza e file readme. Viene inoltre fornito con stub per i test e il file shard.yml per descrivere il progetto e gestire le dipendenze, noto anche come shard.

Aggiungiamo lo shard openssl , necessario per costruire l'algoritmo SHA256 :

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

Una volta che è dentro, torna nel tuo terminale ed esegui crystal deps . In questo modo si abbatterà openssl e le sue dipendenze da utilizzare.

Ora abbiamo la libreria richiesta installata nel nostro codice, iniziamo definendo la classe Block e poi costruendo la funzione 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

Ora puoi testare la tua applicazione eseguendo crystal run crystal src/crystal_coin/block.cr dal tuo terminale.

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

Progettare la nostra Blockchain

Ogni blocco viene archiviato con un timestamp e, facoltativamente, un index . In CrystalCoin memorizzeremo entrambi. Per garantire l'integrità in tutta la blockchain, ogni blocco avrà un hash autoidentificativo. Come Bitcoin, l'hash di ogni blocco sarà un hash crittografico del blocco ( index , timestamp , data e l'hash dell'hash del blocco precedente previous_hash ). I dati possono essere qualsiasi cosa tu voglia per ora.

 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

In Crystal lang, sostituiamo i attr_accessor , attr_getter e attr_setter di Ruby con nuove parole chiave:

Parola chiave rubino Parola chiave di cristallo
attr_accessor proprietà
attr_reader getter
attr_writer setter

Un'altra cosa che potresti aver notato in Crystal è che vogliamo suggerire al compilatore tipi specifici attraverso il nostro codice. Crystal deduce i tipi, ma ogni volta che hai ambiguità puoi anche dichiarare esplicitamente i tipi. Ecco perché abbiamo aggiunto i tipi String per current_hash .

Ora block.cr due volte e notiamo che gli stessi dati genereranno hash diversi a causa del diverso timestamp :

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

Ora abbiamo la nostra struttura a blocchi, ma stiamo creando una blockchain. Dobbiamo iniziare ad aggiungere blocchi per formare una catena vera e propria. Come accennato in precedenza, ogni blocco richiede informazioni dal blocco precedente. Ma come ci arriva il primo blocco della blockchain? Ebbene, il primo blocco, o blocco di genesis , è un blocco speciale (un blocco senza predecessori). In molti casi, viene aggiunto manualmente o ha una logica univoca che ne consente l'aggiunta.

Creeremo una nuova funzione che restituisce un blocco di genesi. Questo blocco è di index=0 e ha un valore di dati arbitrario e un valore arbitrario nel parametro previous_hash .

Costruiamo o classifichiamo il metodo Block.first che genera il blocco di genesi:

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

E proviamolo 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">

Ora che siamo in grado di creare un blocco di genesi , abbiamo bisogno di una funzione che generi blocchi successivi nella blockchain.

Questa funzione prenderà il blocco precedente nella catena come parametro, creerà i dati per il blocco da generare e restituirà il nuovo blocco con i dati appropriati. Quando i nuovi blocchi eseguono l'hashing delle informazioni dai blocchi precedenti, l'integrità della blockchain aumenta con ogni nuovo blocco.

Una conseguenza importante è che un blocco non può essere modificato senza cambiare l'hash di ogni blocco consecutivo. Ciò è dimostrato nell'esempio seguente. Se i dati nel blocco 44 vengono modificati da LOOP a EAST , tutti gli hash dei blocchi consecutivi devono essere modificati. Questo perché l'hash del blocco dipende dal valore dell'hash_precedente (tra previous_hash altre cose).

Diagramma di hashing della criptovaluta Crystal

Se non lo facessimo, sarebbe più facile per una parte esterna modificare i dati e sostituire la nostra catena con una propria completamente nuova. Questa catena di hash funge da prova crittografica e aiuta a garantire che una volta aggiunto un blocco alla blockchain non possa essere sostituito o rimosso. Creiamo il metodo di 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

Per provarlo tutti insieme creeremo una semplice blockchain. Il primo elemento dell'elenco è il blocco di genesi. E, naturalmente, dobbiamo aggiungere i blocchi successivi. Creeremo cinque nuovi blocchi per dimostrare 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 di lavoro

Un algoritmo Proof of Work (PoW) è il modo in cui vengono creati o estratti nuovi blocchi sulla blockchain. L'obiettivo di PoW è scoprire un numero che risolva un problema. Il numero deve essere difficile da trovare ma facile da verificare computazionalmente da chiunque sulla rete. Questa è l'idea centrale dietro Proof of Work.

Dimostriamo con un esempio per assicurarci che tutto sia chiaro. Assumiamo che l'hash di un numero intero x moltiplicato per un altro y debba iniziare con 00 . Così:

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

E per questo esempio semplificato, fissiamo x=5 e implementiamo questo in 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)}"

Eseguiamo il codice:

 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

Come puoi vedere, questo numero y=530 era difficile da trovare (forza bruta) ma facile da verificare usando la funzione hash.

Perché preoccuparsi di questo algoritmo PoW? Perché non creiamo un hash per blocco e basta? Un hash deve essere valido . Nel nostro caso, un hash sarà valido se i primi due caratteri del nostro hash sono 00 . Se il nostro hash inizia con 00...... , è considerato valido. Questa è chiamata difficoltà . Maggiore è la difficoltà, più tempo ci vorrà per ottenere un hash valido.

Ma, se l'hash non è valido la prima volta, qualcosa deve cambiare nei dati che utilizziamo. Se utilizziamo gli stessi dati più e più volte, otterremo lo stesso hash più e più volte e il nostro hash non sarà mai valido. Usiamo qualcosa chiamato nonce nel nostro hash (nel nostro esempio precedente è y ). È semplicemente un numero che incrementiamo ogni volta che l'hash non è valido. Otteniamo i nostri dati (data, messaggio, hash precedente, indice) e un nonce di 1. Se l'hash che otteniamo con questi non è valido, proviamo con un nonce di 2. E incrementiamo il nonce finché non otteniamo un hash valido .

In Bitcoin, l'algoritmo Proof of Work si chiama Hashcash. Aggiungiamo una prova di lavoro alla nostra classe Block e iniziamo a estrarre per trovare il nonce. Useremo una difficoltà hardcoded di due zeri iniziali '00':

Ora riprogettiamo la nostra classe Block per supportarla. Il nostro blocco CrystalCoin conterrà i seguenti attributi:

 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 

testo alternativo immagine

Creerò un modulo separato per eseguire l'hashing e trovare il nonce in modo da mantenere il nostro codice pulito e modulare. Lo chiamerò 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

La nostra classe Block sarebbe simile a:

 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

Poche cose da notare sul codice Crystal e sugli esempi di linguaggio Crystal in generale. In Crystal, i metodi sono pubblici per impostazione predefinita. Crystal richiede che ogni metodo privato sia preceduto dalla parola chiave private che potrebbe creare confusione proveniente da Ruby.

Potresti aver notato che i tipi Integer di Crystal sono Int8 , Int16 , Int32 , Int64 , UInt8 , UInt16 , UInt32 o UInt64 rispetto a Fixnum di Ruby. true e false sono valori nella classe Bool anziché valori nelle classi TrueClass o FalseClass in Ruby.

Crystal ha argomenti di metodo opzionali e denominati come funzionalità del linguaggio di base e non richiede la scrittura di codice speciale per la gestione degli argomenti, il che è piuttosto interessante. Dai un'occhiata a Block#initialize(index = 0, data = "data", previous_hash = "hash") e poi chiamalo con qualcosa come Block.new(data: data, previous_hash: "0") .

Per un elenco più dettagliato delle differenze tra il linguaggio di programmazione Crystal e Ruby, dai un'occhiata a Crystal for Rubyists.

Ora, proviamo a creare cinque transazioni utilizzando:

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

Vedi la differenza? Ora tutti gli hash iniziano con 00 . Questa è la magia del proof-of-work. Usando ProofOfWork abbiamo trovato il nonce e la prova è l'hash con la difficoltà di corrispondenza, cioè i due zeri iniziali 00 .

Nota con il primo blocco che abbiamo creato, abbiamo provato 17 nonce fino a trovare il numero fortunato corrispondente:

Bloccare Cicli/Numero di calcoli hash
#0 17
# 1 24
#2 61
#3 149
#4 570
#5 475

Ora proviamo una difficoltà di quattro zeri iniziali ( difficulty="0000" ):

Bloccare Cicli/Numero di calcoli hash
# 1 26 762
#2 68 419
#3 23 416
#4 15 353

Nel primo blocco ho provato 26762 nonce (confronta 17 nonce con difficoltà '00') fino a trovare il numero fortunato corrispondente.

La nostra Blockchain come API

Fin qui tutto bene. Abbiamo creato la nostra semplice blockchain ed è stato relativamente facile da fare. Ma il problema qui è che CrystalCoin può essere eseguito solo su una singola macchina (non è distribuito/decentralizzato).

D'ora in poi, inizieremo a utilizzare i dati JSON per CrystalCoin . I dati saranno transazioni, quindi il campo dati di ogni blocco sarà un elenco di transazioni.

Ogni transazione sarà un oggetto JSON che dettaglia il sender della moneta, il receiver della moneta e l' amount di CrystalCoin che viene trasferito:

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

Alcune modifiche alla nostra classe Block per supportare il nuovo formato di transaction (precedentemente chiamato data ). Quindi, solo per evitare confusione e mantenere la coerenza, utilizzeremo il termine transaction per fare riferimento ai data pubblicati nella nostra applicazione di esempio d'ora in poi.

Introdurremo una nuova classe semplice 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

Le transazioni sono raggruppate in blocchi, quindi un blocco può contenere solo una o più transazioni. I blocchi contenenti le transazioni vengono generati frequentemente e aggiunti alla blockchain.

La blockchain dovrebbe essere una raccolta di blocchi. Possiamo memorizzare tutti i blocchi nell'elenco Crystal, ed è per questo che introduciamo la nuova classe Blockchain :

Blockchain avrà array chain e uncommitted_transactions . La chain includerà tutti i blocchi estratti nella blockchain e uncommitted_transactions avrà tutte le transazioni che non sono state aggiunte alla blockchain (ancora non estratte). Dopo aver inizializzato Blockchain , creiamo il blocco di genesi usando Block.first e lo aggiungiamo all'array della chain e aggiungiamo un array uncommitted_transactions vuoto.

Creeremo il metodo Blockchain#add_transaction per aggiungere transazioni all'array uncommitted_transactions .

Costruiamo la nostra nuova 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

Nella classe Block inizieremo a utilizzare transactions anziché i 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

Ora che sappiamo come saranno le nostre transazioni, abbiamo bisogno di un modo per aggiungerle a uno dei computer nella nostra rete blockchain, chiamato node . Per farlo, creeremo un semplice server HTTP.

Creeremo quattro endpoint:

  • [POST] /transactions/new : per creare una nuova transazione in un blocco
  • [GET] /mine : per dire al nostro server di estrarre un nuovo blocco.
  • [GET] /chain : per restituire la blockchain completa in formato JSON .
  • [GET] /pending : per restituire le transazioni in sospeso ( uncommitted_transactions ).

Utilizzeremo il framework web Kemal. È un micro-framework che semplifica il mapping degli endpoint alle funzioni Crystal. Kemal è fortemente influenzato da Sinatra per Rubyists e funziona in modo molto simile. Se stai cercando l'equivalente di Ruby on Rails, dai un'occhiata ad Amber.

Il nostro server formerà un singolo nodo nella nostra rete blockchain. Aggiungiamo prima Kemal al file shard.yml come a e installiamo la dipendenza:

 dependencies: kemal: github: kemalcr/kemal

Ora costruiamo lo scheletro del nostro server 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

Ed esegui il server:

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

Assicuriamoci che il server funzioni correttamente:

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

Fin qui tutto bene. Ora possiamo procedere con l'implementazione di ciascuno degli endpoint. Iniziamo implementando /transactions/new e punti finali 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

Implementazione semplice. Creiamo semplicemente un oggetto CrystalCoin::Block::Transaction e aggiungiamo la transazione all'array uncommitted_transactions usando Blockchain#add_transaction .

Al momento, le transazioni sono inizialmente archiviate in un pool di uncommitted_transactions . Il processo di inserimento delle transazioni non confermate in un blocco e calcolo della Proof of Work (PoW) è noto come mining di blocchi. Una volta individuato il nonce che soddisfa i nostri vincoli, possiamo dire che un blocco è stato estratto e il nuovo blocco è stato inserito nella blockchain.

In CrystalCoin , utilizzeremo il semplice algoritmo Proof-of-Work creato in precedenza. Per creare un nuovo blocco, il computer di un minatore dovrà:

  • Trova l'ultimo blocco della chain .
  • Trova le transazioni in sospeso ( uncommitted_transactions ).
  • Crea un nuovo blocco usando Block.next .
  • Aggiungi il blocco estratto all'array della chain .
  • Pulisci l'array uncommitted_transactions .

Quindi, per implementare /mine end-point, implementiamo prima i passaggi precedenti in 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

Per prima cosa ci assicuriamo di avere alcune transazioni in sospeso da estrarre. Quindi otteniamo l'ultimo blocco usando @chain.last e le prime 25 transazioni da estrarre (stiamo usando Array#shift(BLOCK_SIZE) per restituire un array delle prime 25 uncommitted_transactions e quindi rimuovere gli elementi a partire dall'indice 0) .

Ora implementiamo /mine end-point:

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

E per /chain end-point:

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

Interagire con la nostra Blockchain

Useremo cURL per interagire con la nostra API su una rete.

Per prima cosa, accendiamo il server:

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

Quindi creiamo due nuove transazioni effettuando una richiesta POST a http://localhost:3000/transactions/new con un corpo contenente la nostra struttura di transazione:

 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%

Ora elenchiamo le transazioni in sospeso (cioè le transazioni che non sono state ancora aggiunte al blocco):

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

Come possiamo vedere, le due transazioni che abbiamo creato in precedenza sono state aggiunte a uncommitted_transactions .

Ora estraiamo le due transazioni effettuando una richiesta 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.

Sembra che abbiamo estratto con successo il primo blocco e lo abbiamo aggiunto alla nostra chain . Controlliamo due volte la nostra chain e se include il blocco minato:

 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 decentramento

Questo è bello. Ci siamo procurati una blockchain di base che accetta transazioni e ci consente di estrarre nuovi blocchi. Ma il codice che abbiamo implementato fino ad ora è pensato per funzionare su un singolo computer, mentre il punto centrale delle blockchain è che dovrebbero essere decentralizzate. Ma se sono decentralizzati, come ci assicuriamo che riflettano tutti la stessa catena?

Questo è il problema del Consensus .

Dovremo implementare un algoritmo di consenso se vogliamo più di un nodo nella nostra rete.

Registrazione di nuovi nodi

Per implementare un algoritmo di consenso, abbiamo bisogno di un modo per far conoscere a un nodo i nodi vicini sulla rete. Ogni nodo sulla nostra rete dovrebbe mantenere un registro di altri nodi sulla rete. Pertanto, avremo bisogno di più endpoint:

  • [POST] /nodes/register : per accettare un elenco di nuovi nodi sotto forma di URL.
  • [GET] /nodes/resolve : per implementare il nostro algoritmo di consenso, che risolve tutti i conflitti, per garantire che un nodo abbia la catena corretta.

Dobbiamo modificare il costruttore della nostra blockchain e fornire un metodo per la registrazione dei nodi:

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

Si noti che abbiamo utilizzato una struttura dati Set con tipo String per contenere l'elenco dei nodi. Questo è un modo economico per garantire che l'aggiunta di nuovi nodi sia idempotente e che non importa quante volte aggiungiamo un nodo specifico, appaia esattamente una volta.

Ora aggiungiamo un nuovo modulo a Consensus e implementiamo il primo metodo 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 funzione register_node analizzerà l'URL del nodo e lo formatterà.

E qui creiamo /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

Ora, con questa implementazione, potremmo affrontare un problema con più nodi. La copia di catene di pochi nodi può differire. In tal caso, dobbiamo concordare una versione della catena per mantenere l'integrità dell'intero sistema. Dobbiamo raggiungere il consenso.

Per risolvere questo problema, faremo la regola che la catena valida più lunga è quella da utilizzare. Utilizzando questo algoritmo, raggiungiamo il consenso tra i nodi della nostra rete. Il motivo alla base di questo approccio è che la catena più lunga è una buona stima della maggior quantità di lavoro svolto.

testo alternativo immagine

 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

Tieni presente che la resolve è un metodo che scorre tutti i nostri nodi vicini, scarica le loro catene e le verifica usando valid_chain? metodo. 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"}]}% 

testo alternativo immagine

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

Avvolgendo

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.