Creazione di una criptovaluta nel linguaggio di programmazione Crystal
Pubblicato: 2022-03-11Questo 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.
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).
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
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 formatoJSON
. - [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.
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 usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(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 case00
).
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
andhttp://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"}]}%
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.