Crearea unei criptomonede în limbajul de programare Crystal

Publicat: 2022-03-11

Această postare este încercarea mea de a înțelege aspectele cheie ale blockchain-ului prin explorarea elementelor interne. Am început prin a citi documentul original bitcoin, dar am simțit că singura modalitate de a înțelege cu adevărat blockchain-ul este construirea unei noi criptomonede de la zero.

De aceea am decis să creez o criptomonedă folosind noul limbaj de programare Crystal și l-am numit CrystalCoin . Acest articol nu va discuta despre alegerile algoritmilor, dificultatea hash sau subiecte similare. În schimb, accentul se va pune pe detalierea unui exemplu concret, care ar trebui să ofere o înțelegere mai profundă și practică a punctelor forte și limitărilor blockchain-urilor.

Dacă nu l-ați citit încă, pentru mai multe informații despre algos și hashing, vă sugerez să aruncați o privire la articolul lui Demir Selmanovic Cryptocurrency for Dummies: Bitcoin and Beyond.

De ce am ales limbajul de programare Crystal

Pentru o demonstrație mai bună, am vrut să folosesc un limbaj productiv precum Ruby, fără a compromite performanța. Criptomoneda are multe calcule consumatoare de timp (și anume mining și hashing ), și de aceea limbaje compilate precum C++ și Java sunt limbajele de alegere pentru construirea de criptomonede „reale”. Acestea fiind spuse, am vrut să folosesc un limbaj cu o sintaxă mai curată, astfel încât să pot păstra distracția dezvoltării și să permit o mai bună lizibilitate. Performanța cristalului tinde să fie oricum bună.

Ilustrație Crystal Programming Language

Deci, de ce am decis să folosesc limbajul de programare Crystal? Sintaxa lui Crystal este puternic inspirată de cea a lui Ruby, așa că pentru mine mi se pare natural să citesc și ușor de scris. Are avantajul suplimentar al unei curbe de învățare mai scăzute, în special pentru dezvoltatorii Ruby experimentați.

Iată cum spune echipa Crystal lang pe site-ul lor oficial:

Rapid ca C, elegant ca Ruby.

Cu toate acestea, spre deosebire de Ruby sau JavaScript, care sunt limbi interpretate, Crystal este un limbaj compilat, făcându-l mult mai rapid și oferind o amprentă de memorie mai mică. Sub capotă, folosește LLVM pentru compilarea în cod nativ.

Crystal este, de asemenea, tastat static, ceea ce înseamnă că compilatorul vă va ajuta să detectați erorile de tip în timpul compilării.

Nu voi explica de ce consider limbajul Crystal minunat, deoarece depășește scopul acestui articol, dar dacă optimismul meu nu este convingător, nu ezitați să consultați acest articol pentru o imagine de ansamblu mai bună a potențialului Crystal.

Notă: Acest articol presupune că aveți deja cunoștințe de bază despre programarea orientată pe obiecte (OOP).

Blockchain

Deci, ce este un blockchain? Este o listă (lanț) de blocuri legate și securizate de amprente digitale (cunoscute și sub denumirea de hash criptografic).

Cel mai simplu mod de a te gândi la aceasta este ca o structură de date cu listă legată. Acestea fiind spuse, o listă legată trebuie doar să aibă o referință la elementul anterior; un bloc trebuie să aibă un identificator în funcție de identificatorul blocului anterior, ceea ce înseamnă că nu puteți înlocui un bloc fără a recalcula fiecare bloc care urmează.

Deocamdată, gândiți-vă la blockchain ca la o serie de blocuri cu unele date legate de un lanț, lanțul fiind hash-ul blocului anterior.

Întregul blockchain ar exista pe fiecare nod care dorește să interacționeze cu acesta, adică este copiat pe fiecare dintre nodurile din rețea. Niciun server nu îl găzduiește, dar toate companiile de dezvoltare blockchain îl folosesc, ceea ce îl face descentralizat .

Da, acest lucru este ciudat în comparație cu sistemele centralizate convenționale. Fiecare dintre noduri va avea o copie a întregului blockchain (> 149 Gb în blockchain Bitcoin până în decembrie 2017).

Hashing și semnătură digitală

Deci, ce este această funcție hash? Gândiți-vă la hash ca la o funcție, care returnează o amprentă digitală unică atunci când îi dăm un text/obiect. Chiar și cea mai mică modificare a obiectului de intrare ar schimba dramatic amprenta.

Există diferiți algoritmi de hashing, iar în acest articol, vom folosi algoritmul de hash SHA256 , care este cel folosit în Bitcoin .

Folosind SHA256 , vom avea întotdeauna 64 de caractere hexazecimale (256 de biți) în lungime, chiar dacă intrarea este mai mică de 256 de biți sau mult mai mare de 256 de biți:

Intrare Rezultate hashed
TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT FOARTE LUNG TEXT TEXT FOARTE LUNG TEXT FOARTE LUNG cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
Toptal 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
Toptal. 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

Rețineți, cu ultimul exemplu, că doar adăugarea unui . (punct) a dus la o schimbare dramatică a hashului.

Prin urmare, într-un blockchain, lanțul este construit prin trecerea datelor blocului într-un algoritm de hashing care ar genera un hash, care este legat de blocul următor, de acum înainte, formând o serie de blocuri legate cu hash-urile blocurilor anterioare.

Construirea unei criptomonede în Crystal

Acum să începem să creăm proiectul nostru Crystal și să construim criptarea SHA256 .

Presupunând că aveți limbajul de programare Crystal instalat, să creăm scheletul bazei de cod CrystalCoin utilizând 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/

Această comandă va crea structura de bază pentru proiect, cu un depozit git deja inițializat, licență și fișiere readme. De asemenea, vine cu stub-uri pentru teste și fișierul shard.yml pentru descrierea proiectului și gestionarea dependențelor, cunoscut și sub numele de shards.

Să adăugăm fragmentul openssl , care este necesar pentru a construi algoritmul SHA256 :

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

Odată ce ați intrat, întoarceți-vă în terminal și rulați crystal deps . Făcând acest lucru, openssl și dependențele sale vor fi eliminate pe care le vom utiliza.

Acum avem biblioteca necesară instalată în codul nostru, să începem prin a defini clasa Block și apoi construim funcția 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

Acum vă puteți testa aplicația rulând crystal run crystal src/crystal_coin/block.cr de pe terminalul dvs.

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

Proiectarea Blockchain-ului nostru

Fiecare bloc este stocat cu un marcaj de timestamp și, opțional, un index . În CrystalCoin , le vom stoca pe ambele. Pentru a ajuta la asigurarea integrității în întregul lanț de blocuri, fiecare bloc va avea un hash de auto-identificare. La fel ca Bitcoin, hash-ul fiecărui bloc va fi un hash criptografic al blocului ( index , timestamp , data și hash-ul hash-ului precedent_hash al blocului previous_hash ). Datele pot fi orice doriți pentru moment.

 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

În Crystal lang, înlocuim attr_accessor , attr_getter și attr_setter ale lui Ruby cu cuvinte cheie noi:

Cuvânt cheie Ruby Cuvânt cheie de cristal
attr_accessor proprietate
attr_reader getter
attr_writer setter

Un alt lucru pe care poate l-ați observat în Crystal este că vrem să sugerăm compilatorului anumite tipuri prin codul nostru. Crystal deduce tipurile, dar ori de câte ori aveți ambiguitate, puteți declara în mod explicit și tipurile. De aceea am adăugat tipuri String pentru current_hash .

Acum să rulăm block.cr de două ori și să observăm că aceleași date vor genera hash-uri diferite din cauza marcajului de timestamp diferit:

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

Acum avem structura noastră de bloc, dar creăm un blockchain. Trebuie să începem să adăugăm blocuri pentru a forma un lanț real. După cum am menționat mai devreme, fiecare bloc necesită informații din blocul anterior. Dar cum ajunge acolo primul bloc din blockchain? Ei bine, primul bloc, sau blocul genesis , este un bloc special (un bloc fără predecesori). În multe cazuri, este adăugat manual sau are o logică unică care permite să fie adăugat.

Vom crea o nouă funcție care returnează un bloc de geneză. Acest bloc este de index=0 și are o valoare de date arbitrară și o valoare arbitrară în parametrul previous_hash .

Să construim sau să clasem metoda Block.first care generează blocul geneză:

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

Și să-l testăm folosind 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">

Acum că suntem capabili să creăm un bloc de geneză , avem nevoie de o funcție care va genera blocuri succesive în blockchain.

Această funcție va lua ca parametru blocul anterior din lanț, va crea datele pentru blocul care urmează să fie generat și va returna noul bloc cu datele corespunzătoare. Când blocurile noi trimit informații din blocurile anterioare, integritatea blockchain-ului crește cu fiecare bloc nou.

O consecință importantă este că un bloc nu poate fi modificat fără modificarea hash-ului fiecărui bloc consecutiv. Acest lucru este demonstrat în exemplul de mai jos. Dacă datele din blocul 44 sunt modificate de la LOOP la EAST , toate hashurile blocurilor consecutive trebuie modificate. Acest lucru se datorează faptului că hash-ul blocului depinde de valoarea previous_hash (printre altele).

Diagrama de hashing a criptomonedei cristal

Dacă nu am face acest lucru, ar fi mai ușor pentru o parte din afara să schimbe datele și să înlocuiască lanțul nostru cu unul complet nou al lor. Acest lanț de hashe-uri acționează ca dovadă criptografică și ajută la asigurarea faptului că, odată ce un bloc este adăugat în blockchain, acesta nu poate fi înlocuit sau eliminat. Să creăm metoda de clasă 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

Pentru a încerca totul împreună, vom crea un blockchain simplu. Primul element al listei este blocul genezei. Și, desigur, trebuie să adăugăm blocurile următoare. Vom crea cinci blocuri noi pentru a demonstra 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)">

Dovada de lucru

Un algoritm Proof of Work (PoW) este modul în care blocurile noi sunt create sau extrase pe blockchain. Scopul PoW este de a descoperi un număr care rezolvă o problemă. Numărul trebuie să fie greu de găsit, dar ușor de verificat computațional de către oricine din rețea. Aceasta este ideea de bază din spatele Proof of Work.

Să demonstrăm cu un exemplu pentru a ne asigura că totul este clar. Vom presupune că hash-ul unui număr întreg x înmulțit cu un alt y trebuie să înceapă cu 00 . Asa de:

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

Și pentru acest exemplu simplificat, să reparăm x=5 și să implementăm acest lucru în 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)}"

Să rulăm codul:

 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

După cum puteți vedea, acest număr y=530 a fost greu de găsit (forță brută), dar ușor de verificat folosind funcția hash.

De ce să vă deranjați cu acest algoritm PoW? De ce nu creăm un hash pe bloc și gata? Un hash trebuie să fie valid . În cazul nostru, un hash va fi valid dacă primele două caractere ale hash-ului nostru sunt 00 . Dacă hash-ul nostru începe cu 00...... , este considerat valid. Aceasta se numește dificultate . Cu cât este mai mare dificultatea, cu atât este nevoie de mai mult pentru a obține un hash valid.

Dar, dacă hash-ul nu este valabil prima dată, trebuie să se schimbe ceva în datele pe care le folosim. Dacă folosim aceleași date din nou și din nou, vom obține același hash din nou și din nou și hash-ul nostru nu va fi niciodată valid. Folosim ceva numit nonce în hash (în exemplul nostru anterior este y ). Este pur și simplu un număr pe care îl incrementăm de fiecare dată când hash-ul nu este valid. Obținem datele noastre (data, mesaj, hash anterior, index) și un nonce de 1. Dacă hash-ul pe care îl obținem cu acestea nu este valid, încercăm cu un nonce de 2. Și incrementăm nonce până când obținem un hash valid .

În Bitcoin, algoritmul Proof of Work se numește Hashcash. Să adăugăm o dovadă de lucru la clasa noastră Block și să începem să minăm pentru a găsi nonce. Vom folosi o dificultate codificată de două zerouri la început „00”:

Acum să ne reproiectăm clasa Block pentru a sprijini asta. Blocul nostru CrystalCoin va conține următoarele atribute:

 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 

text alt imagine

Voi crea un modul separat pentru a face hashingul și a găsi nonce , astfel încât să ne păstrăm codul curat și modular. O voi numi 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

Clasa noastră Block ar arăta cam aș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

Puține lucruri de remarcat despre codul Crystal și exemplele de limbaj Crystal în general. În Crystal, metodele sunt publice în mod implicit. Crystal necesită ca fiecare metodă privată să fie prefixată cu cuvântul cheie privat care ar putea fi confuz provenind de la Ruby.

Poate ați observat că tipurile Integer ale Crystal există Int8 , Int16 , Int32 , Int64 , UInt8 , UInt16 , UInt32 sau UInt64 în comparație cu Fixnum lui Ruby. true și false sunt valori din clasa Bool , mai degrabă decât valori din clasele TrueClass sau FalseClass în Ruby.

Crystal are argumente opționale și numite metode ca caracteristici de bază ale limbajului și nu necesită scrierea unui cod special pentru gestionarea argumentelor, ceea ce este destul de grozav. Verificați Block#initialize(index = 0, data = "data", previous_hash = "hash") și apoi apelați-l cu ceva de genul Block.new(data: data, previous_hash: "0") .

Pentru o listă mai detaliată a diferențelor dintre limbajul de programare Crystal și Ruby, consultați Crystal for Rubyists.

Acum, să încercăm să creăm cinci tranzacții folosind:

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

Vezi diferenta? Acum toate hashe-urile încep cu 00 . Aceasta este magia dovezii de lucru. Folosind ProofOfWork am găsit nonce și proof este hash cu dificultatea de potrivire, adică cele două zerouri de început 00 .

Rețineți că, cu primul bloc pe care l-am creat, am încercat 17 nonces până să găsim numărul norocos potrivit:

bloc Bucle/Număr de calcule hash
#0 17
#1 24
#2 61
#3 149
#4 570
#5 475

Acum să încercăm o dificultate de patru zerouri inițiale ( difficulty="0000" ):

bloc Bucle/Număr de calcule hash
#1 26 762
#2 68 419
#3 23 416
#4 15 353

În primul bloc s-au încercat 26762 nonce (comparați 17 nonce cu dificultatea '00') până la găsirea numărului norocos potrivit.

Blockchain-ul nostru ca API

Până acum, bine. Am creat blockchain-ul nostru simplu și a fost relativ ușor de realizat. Dar problema aici este că CrystalCoin poate rula doar pe o singură mașină (nu este distribuită/descentralizată).

De acum încolo, vom începe să folosim date JSON pentru CrystalCoin . Datele vor fi tranzacții, deci câmpul de date al fiecărui bloc va fi o listă de tranzacții.

Fiecare tranzacție va fi un obiect JSON care detaliază sender monedei, receiver monedei și amount de CrystalCoin care este transferată:

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

Câteva modificări ale clasei noastre Block pentru a sprijini noul format de transaction (numit anterior data ). Deci, doar pentru a evita confuzia și pentru a menține consistența, vom folosi termenul de transaction pentru a ne referi la data postate în aplicația noastră exemplu de acum înainte.

Vom introduce o nouă clasă simplă 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

Tranzacțiile sunt împachetate în blocuri, astfel încât un bloc poate conține doar una sau mai multe tranzacții. Blocurile care conțin tranzacțiile sunt generate frecvent și adăugate la blockchain.

Blockchain-ul ar trebui să fie o colecție de blocuri. Putem stoca toate blocurile din lista Crystal și de aceea introducem noua clasă Blockchain :

Blockchain va avea matrice chain și uncommitted_transactions . chain va include toate blocurile minate din blockchain, iar uncommitted_transactions va avea toate tranzacțiile care nu au fost adăugate în blockchain (încă nu au fost extrase). Odată ce inițializam Blockchain , creăm blocul geneză folosind Block.first și îl adăugăm la matricea chain și adăugăm o matrice goală uncommitted_transactions .

Vom crea metoda Blockchain#add_transaction pentru a adăuga tranzacții la matricea uncommitted_transactions .

Să construim noua noastră clasă 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

În clasa Block vom începe să folosim transactions în loc 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

Acum că știm cum vor arăta tranzacțiile noastre, avem nevoie de o modalitate de a le adăuga la unul dintre computerele din rețeaua noastră blockchain, numit node . Pentru a face asta, vom crea un server HTTP simplu.

Vom crea patru puncte finale:

  • [POST] /transactions/new : pentru a crea o nouă tranzacție într-un bloc
  • [GET] /mine : pentru a spune serverului nostru să mine un bloc nou.
  • [GET] /chain : pentru a returna blockchain-ul complet în format JSON .
  • [GET] /pending : pentru a returna tranzacțiile în așteptare ( uncommitted_transactions ).

Vom folosi cadrul web Kemal. Este un micro-cadru care facilitează maparea punctelor finale la funcțiile Crystal. Kemal este puternic influențat de Sinatra pentru rubiști și funcționează într-un mod foarte similar. Dacă căutați echivalentul Ruby on Rails, verificați Amber.

Serverul nostru va forma un singur nod în rețeaua noastră blockchain. Să adăugăm mai întâi Kemal la fișierul shard.yml ca a și să instalăm dependența:

 dependencies: kemal: github: kemalcr/kemal

Acum să construim scheletul serverului nostru 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

Și rulați serverul:

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

Să ne asigurăm că serverul funcționează bine:

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

Până acum, bine. Acum, putem continua cu implementarea fiecăruia dintre punctele finale. Să începem prin a implementa /transactions/new și 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

Implementare simplă. Doar creăm un obiect CrystalCoin::Block::Transaction și adăugăm tranzacția la matricea uncommitted_transactions folosind Blockchain#add_transaction .

În acest moment, tranzacțiile sunt stocate inițial într-un grup de uncommitted_transactions . Procesul de introducere a tranzacțiilor neconfirmate într-un bloc și de calcul a dovezii de lucru (PoW) este cunoscut sub numele de extragerea blocurilor. Odată ce ne-a îndeplinit nonce , putem spune că un bloc a fost extras, iar noul bloc este introdus în blockchain.

În CrystalCoin , vom folosi algoritmul simplu Proof-of-Work pe care l-am creat mai devreme. Pentru a crea un bloc nou, computerul unui miner va trebui să:

  • Găsiți ultimul bloc din chain .
  • Găsiți tranzacții în așteptare ( uncommitted_transactions ).
  • Creați un bloc nou folosind Block.next .
  • Adăugați blocul extras la matricea chain .
  • Curățați matricea uncommitted_transactions .

Deci, pentru a implementa punctul final /mine , să implementăm mai întâi pașii de mai sus în 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

Ne asigurăm mai întâi că avem câteva tranzacții în așteptare pentru mine. Apoi obținem ultimul bloc folosind @chain.last și primele 25 de tranzacții care urmează să fie extrase (folosim Array#shift(BLOCK_SIZE) pentru a returna o matrice cu primele 25 de tranzacții uncommitted_transactions și apoi eliminăm elementele începând cu indexul 0) .

Acum să implementăm punctul final /mine :

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

Și pentru punctul final /chain :

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

Interacționează cu Blockchain-ul nostru

Vom folosi cURL pentru a interacționa cu API-ul nostru printr-o rețea.

Mai întâi, să pornim serverul:

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

Apoi, să creăm două tranzacții noi făcând o solicitare POST către http://localhost:3000/transactions/new cu un corp care conține structura noastră de tranzacție:

 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%

Acum să enumerăm tranzacțiile în așteptare (adică tranzacțiile care nu au fost încă adăugate la bloc):

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

După cum putem vedea, cele două tranzacții pe care le-am creat mai devreme au fost adăugate la uncommitted_transactions .

Acum, să analizăm cele două tranzacții făcând o solicitare GET către http://0.0.0.0:3000/mine :

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

Se pare că am extras cu succes primul bloc și l-am adăugat în chain nostru. Să verificăm chain nostru și dacă include blocul minat:

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

Consens și descentralizare

Acest lucru este cool. Ne-am obținut un blockchain de bază care acceptă tranzacții și ne permite să extragem noi blocuri. Dar codul pe care l-am implementat până acum este menit să ruleze pe un singur computer, în timp ce scopul principal al blockchain-urilor este că ar trebui să fie descentralizate. Dar dacă sunt descentralizate, cum ne asigurăm că toate reflectă același lanț?

Aceasta este problema Consensus .

Va trebui să implementăm un algoritm de consens dacă dorim mai mult de un nod în rețeaua noastră.

Înregistrarea nodurilor noi

Pentru a implementa un algoritm de consens, avem nevoie de o modalitate de a informa un nod despre nodurile vecine din rețea. Fiecare nod din rețeaua noastră ar trebui să păstreze un registru al altor noduri din rețea. Prin urmare, vom avea nevoie de mai multe puncte finale:

  • [POST] /nodes/register : pentru a accepta o listă de noduri noi sub formă de URL-uri.
  • [GET] /nodes/resolve : pentru a implementa algoritmul nostru de consens, care rezolvă orice conflicte - pentru a ne asigura că un nod are lanțul corect.

Trebuie să modificăm constructorul blockchain-ului și să furnizăm o metodă de înregistrare a nodurilor:

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

Rețineți că am folosit o structură de date Set cu tip String pentru a păstra lista de noduri. Aceasta este o modalitate ieftină de a ne asigura că adăugarea de noi noduri este idempotentă și că, indiferent de câte ori adăugăm un anumit nod, acesta apare exact o dată.

Acum să adăugăm un nou modul la Consensus și să implementăm prima metodă 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

Funcția register_node va analiza adresa URL a nodului și o va formata.

Și aici să creăm punctul final /nodes/register :

 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

Acum, cu această implementare, ne-am putea confrunta cu o problemă cu mai multe noduri. Copia lanțurilor a câtorva noduri poate diferi. În acest caz, trebuie să cădem de acord asupra unei versiuni a lanțului pentru a menține integritatea întregului sistem. Trebuie să ajungem la un consens.

Pentru a rezolva acest lucru, vom face regula că cel mai lung lanț valid este cel care trebuie utilizat. Folosind acest algoritm, ajungem la un consens între nodurile din rețeaua noastră. Motivul din spatele acestei abordări este că cel mai lung lanț este o estimare bună a celei mai mari cantități de muncă realizată.

text alt imagine

 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

Rețineți că resolve este o metodă care trece prin toate nodurile noastre vecine, le descarcă lanțurile și le verifică folosind valid_chain? metodă. 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"}]}% 

text alt imagine

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

Încheierea

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.