Tworzenie kryptowaluty w kryształowym języku programowania

Opublikowany: 2022-03-11

Ten post jest moją próbą zrozumienia kluczowych aspektów łańcucha bloków poprzez badanie jego elementów wewnętrznych. Zacząłem od przeczytania oryginalnej białej księgi bitcoin, ale czułem, że jedynym sposobem, aby naprawdę zrozumieć blockchain, jest zbudowanie nowej kryptowaluty od zera.

Dlatego postanowiłem stworzyć kryptowalutę przy użyciu nowego języka programowania Crystal i nazwałem ją CrystalCoin . W tym artykule nie omówimy opcji algorytmu, trudności haszowania ani podobnych tematów. Zamiast tego skupimy się na szczegółowym opisie konkretnego przykładu, który powinien zapewnić głębsze, praktyczne zrozumienie mocnych stron i ograniczeń blockchainów.

Jeśli jeszcze tego nie czytałeś, aby uzyskać więcej informacji na temat algosów i haszowania, proponuję zajrzeć do artykułu Demira Selmanovica Cryptocurrency for Dummies: Bitcoin and Beyond.

Dlaczego wybrałem język programowania Crystal

Aby uzyskać lepszą demonstrację, chciałem użyć wydajnego języka, takiego jak Ruby, bez pogarszania wydajności. Kryptowaluta ma wiele czasochłonnych obliczeń (a mianowicie kopanie i haszowanie ), dlatego języki skompilowane, takie jak C++ i Java, są językami z wyboru do budowy „prawdziwych” kryptowalut. Biorąc to pod uwagę, chciałem użyć języka o czystszej składni, aby móc tworzyć zabawę i zapewnić lepszą czytelność. Wydajność kryształów i tak jest dobra.

Ilustracja Crystal Programming Language

Dlaczego więc zdecydowałem się na użycie języka programowania Crystal? Składnia Crystala jest mocno inspirowana Ruby, więc dla mnie czytanie i pisanie jest dla mnie naturalne. Ma dodatkową zaletę w postaci niższej krzywej uczenia się, szczególnie dla doświadczonych programistów Ruby.

Oto jak zespół Crystal lang umieszcza to na swojej oficjalnej stronie internetowej:

Szybki jak C, zręczny jak Ruby.

Jednak w przeciwieństwie do Ruby lub JavaScript, które są językami interpretowanymi, Crystal jest językiem skompilowanym, dzięki czemu jest znacznie szybszy i oferuje mniejsze zużycie pamięci. Pod maską używa LLVM do kompilacji do kodu natywnego.

Crystal ma również typ statyczny, co oznacza, że ​​kompilator pomoże Ci wyłapać błędy typu w czasie kompilacji.

Nie zamierzam wyjaśniać, dlaczego uważam język Crystal za niesamowity, ponieważ wykracza on poza zakres tego artykułu, ale jeśli mój optymizm nie jest dla ciebie przekonujący, zachęcam do zapoznania się z tym artykułem, aby uzyskać lepszy przegląd potencjału Crystal.

Uwaga: w tym artykule założono, że masz już podstawową wiedzę na temat programowania zorientowanego obiektowo (OOP).

Blockchain

Czym więc jest łańcuch bloków? Jest to lista (łańcuch) bloków połączonych i zabezpieczonych cyfrowymi odciskami palców (znanymi również jako skróty kryptograficzne).

Najłatwiej o tym myśleć jako o połączonej strukturze danych listy. Biorąc to pod uwagę, połączona lista wymagała jedynie odniesienia do poprzedniego elementu; blok musi mieć identyfikator zależny od identyfikatora poprzedniego bloku, co oznacza, że ​​nie można zastąpić bloku bez ponownego obliczenia każdego kolejnego bloku.

Na razie pomyśl o blockchain jako o serii bloków z niektórymi danymi połączonymi łańcuchem, który jest hashem poprzedniego bloku.

Cały łańcuch bloków istniałby na każdym węźle, który chce z nim wchodzić w interakcje, co oznacza, że ​​jest kopiowany na każdym z węzłów w sieci. Żaden pojedynczy serwer go nie obsługuje, ale korzystają z niego wszystkie firmy zajmujące się rozwojem blockchain, co sprawia, że ​​jest zdecentralizowany .

Tak, to dziwne w porównaniu z konwencjonalnymi systemami scentralizowanymi. Każdy z węzłów będzie miał kopię całego łańcucha bloków (> 149 GB w łańcuchu bloków Bitcoin do grudnia 2017 r.).

Haszowanie i podpis cyfrowy

Czym więc jest ta funkcja skrótu? Pomyśl o hashu jako o funkcji, która zwraca unikalny odcisk palca, gdy nadajemy mu tekst/obiekt. Nawet najmniejsza zmiana w obiekcie wejściowym radykalnie zmieniłaby odcisk palca.

Istnieją różne algorytmy haszujące, a w tym artykule użyjemy algorytmu haszującego SHA256 , który jest używany w Bitcoin .

Używając SHA256 , zawsze otrzymamy 64 znaki szesnastkowe (256 bitów), nawet jeśli dane wejściowe są mniejsze niż 256 bitów lub znacznie większe niż 256 bitów:

Wejście Wyniki haszowane
BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST BARDZO DŁUGI BARDZO DŁUGI TEKST BARDZO DŁUGI BARDZO DŁUGI TEKST BARDZO DŁUGI TEKST cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
Toptal 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
Toptal. 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

Zauważ z ostatnim przykładem, że wystarczy dodać . (kropka) spowodowało dramatyczną zmianę w haszu.

Dlatego w łańcuchu blokowym łańcuch jest budowany poprzez przekazanie danych bloku do algorytmu mieszającego, który wygeneruje hash, który jest połączony z następnym blokiem, odtąd tworząc serię bloków połączonych z hashami poprzednich bloków.

Budowanie kryptowaluty w Crystal

Teraz zacznijmy tworzyć nasz projekt Crystal i budować nasze szyfrowanie SHA256 .

Zakładając, że masz zainstalowany język programowania Crystal, stwórzmy szkielet bazy kodu CrystalCoin za pomocą wbudowanej 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/

To polecenie utworzy podstawową strukturę projektu, z już zainicjowanym repozytorium git, plikami licencyjnymi i readme. Zawiera również kody pośredniczące do testów oraz plik shard.yml do opisywania projektu i zarządzania zależnościami, znany również jako shards.

Dodajmy fragment openssl , który jest potrzebny do zbudowania algorytmu SHA256 :

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

Gdy to zrobisz, wróć do swojego terminala i uruchom crystal deps . Spowoduje to ściągnięcie openssl i jego zależności do wykorzystania.

Teraz mamy zainstalowaną wymaganą bibliotekę w naszym kodzie, zacznijmy od zdefiniowania klasy Block , a następnie zbudowania funkcji skrótu.

 # 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

Możesz teraz przetestować swoją aplikację, uruchamiając crystal run crystal src/crystal_coin/block.cr z terminala.

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

Projektowanie naszego łańcucha bloków

Każdy blok jest przechowywany ze timestamp i opcjonalnie index . W CrystalCoin będziemy przechowywać oba. Aby zapewnić integralność w całym łańcuchu bloków, każdy blok będzie miał samoidentyfikujący hash . Podobnie jak Bitcoin, hash każdego bloku będzie hashem kryptograficznym bloku ( index , timestamp , data , i hash hash poprzedniego bloku previous_hash ). Dane mogą być na razie dowolne.

 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

W języku Crystal zastępujemy metody attr_accessor , attr_getter i attr_setter w Ruby nowymi słowami kluczowymi:

Rubinowe słowo kluczowe Kryształowe słowo kluczowe
attr_accessor własność
attr_reader rębacz
attr_writer seter

Inną rzeczą, którą mogłeś zauważyć w Crystal, jest to, że chcemy podpowiedzieć kompilatorowi konkretne typy za pomocą naszego kodu. Crystal wnioskuje o typach, ale zawsze, gdy masz niejednoznaczność, możesz również jawnie zadeklarować typy. Dlatego dodaliśmy typy String dla current_hash .

Teraz block.cr dwa razy i zauważmy, że te same dane wygenerują różne skróty ze względu na różne timestamp :

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

Teraz mamy naszą strukturę blokową, ale tworzymy blockchain. Musimy zacząć dodawać bloki, aby utworzyć rzeczywisty łańcuch. Jak wspomniałem wcześniej, każdy blok wymaga informacji z poprzedniego bloku. Ale w jaki sposób dostaje się tam pierwszy blok w łańcuchu bloków? Cóż, pierwszy blok, czyli blok genesis , to specjalny blok (blok bez poprzedników). W wielu przypadkach jest dodawany ręcznie lub ma unikalną logikę umożliwiającą jego dodanie.

Stworzymy nową funkcję, która zwraca blok genezy. Ten blok ma index=0 i ma dowolną wartość danych oraz dowolną wartość w parametrze previous_hash .

Zbudujmy lub zaklasyfikujmy metodę Block.first , która generuje blok genezy:

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

I przetestujmy to za pomocą 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">

Teraz, gdy jesteśmy w stanie stworzyć blok genezy , potrzebujemy funkcji, która wygeneruje kolejne bloki w łańcuchu bloków.

Ta funkcja przyjmie poprzedni blok w łańcuchu jako parametr, utworzy dane dla bloku, który ma zostać wygenerowany, i zwróci nowy blok z odpowiednimi danymi. Kiedy nowe bloki mieszają informacje z poprzednich bloków, integralność łańcucha bloków wzrasta z każdym nowym blokiem.

Ważną konsekwencją jest to, że blok nie może być modyfikowany bez zmiany skrótu każdego kolejnego bloku. Pokazuje to poniższy przykład. Jeżeli dane w bloku 44 zostaną zmienione z LOOP na EAST , wszystkie skróty kolejnych bloków muszą zostać zmienione. Dzieje się tak, ponieważ hash bloku zależy między innymi od wartości previous_hash .

Diagram hashowania kryształowej kryptowaluty

Gdybyśmy tego nie zrobili, łatwiej byłoby podmiotowi zewnętrznemu zmienić dane i zastąpić naszą sieć zupełnie nową własną. Ten łańcuch skrótów działa jako dowód kryptograficzny i pomaga zapewnić, że po dodaniu bloku do łańcucha bloków nie można go zastąpić ani usunąć. Stwórzmy metodę klasy 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

Aby wypróbować to wszystko razem, stworzymy prosty łańcuch bloków. Pierwszym elementem listy jest blok genezy. I oczywiście musimy dodać kolejne bloki. Stworzymy pięć nowych bloków, aby zademonstrować 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)">

Dowód pracy

Algorytm Proof of Work (PoW) to sposób tworzenia lub wydobywania nowych bloków w łańcuchu bloków. Celem PoW jest odkrycie liczby, która rozwiązuje problem. Numer musi być trudny do znalezienia, ale łatwy do zweryfikowania obliczeniowego przez każdego w sieci. To jest główna idea Proof of Work.

Zademonstrujmy na przykładzie, aby upewnić się, że wszystko jest jasne. Załóżmy, że hash pewnej liczby całkowitej x pomnożonej przez inny y musi zaczynać się od 00 . Więc:

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

W tym uproszczonym przykładzie naprawmy x=5 i zaimplementujmy to w 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)}"

Uruchommy kod:

 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

Jak widać liczba y=530 była trudna do znalezienia (brute-force), ale łatwa do zweryfikowania za pomocą funkcji haszującej.

Po co zawracać sobie głowę tym algorytmem PoW? Dlaczego po prostu nie utworzymy jednego skrótu na blok i to wszystko? Hash musi być poprawny . W naszym przypadku hash będzie ważny, jeśli pierwsze dwa znaki naszego hasha to 00 . Jeśli nasz hash zaczyna się od 00...... , jest uważany za prawidłowy. Nazywa się to trudnością . Im wyższy poziom trudności, tym dłużej trwa uzyskanie prawidłowego skrótu.

Ale jeśli hash nie jest prawidłowy za pierwszym razem, coś musi się zmienić w używanych przez nas danych. Jeśli będziemy używać tych samych danych w kółko, otrzymamy ten sam hash w kółko, a nasz hash nigdy nie będzie ważny. Używamy czegoś o nazwie nonce w naszym hashu (w naszym poprzednim przykładzie jest to y ). Jest to po prostu liczba, którą zwiększamy za każdym razem, gdy hash jest niepoprawny. Otrzymujemy nasze dane (data, wiadomość, poprzedni hash, indeks) i wartość jednorazową wartości 1. Jeśli hash, który otrzymaliśmy z tymi wartościami, jest niepoprawny, próbujemy z wartością jednokrotną wynoszącą 2. I zwiększamy wartość jednokrotną, aż otrzymamy poprawny skrót .

W Bitcoin algorytm Proof of Work nazywa się Hashcash. Dodajmy dowód pracy do naszej klasy Block i zacznijmy wydobywać , aby znaleźć jednorazowy. Użyjemy zakodowanej na stałe trudności dwóch wiodących zer '00':

Teraz przeprojektujmy naszą klasę Block, aby to wspierać. Nasz blok CrystalCoin będzie zawierał następujące atrybuty:

 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 

tekst alternatywny obrazu

Stworzę osobny moduł do haszowania i znajdowania wartości nonce , dzięki czemu nasz kod będzie czysty i modułowy. Nazwę to 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

Nasza klasa Block wyglądałaby mniej więcej tak:

 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

Kilka rzeczy do zapamiętania na temat kodu Crystal i ogólnie przykładów języka Crystal. W Crystal metody są domyślnie publiczne. Crystal wymaga, aby każda prywatna metoda była poprzedzona słowem kluczowym private, co może być mylące w przypadku Rubiego.

Być może zauważyłeś, że typy liczb całkowitych Crystal to Int8 , Int16 , Int32 , Int64 , UInt8 , UInt16 , UInt32 lub UInt64 w porównaniu z Fixnum . true i false są wartościami w klasie Bool , a nie wartościami w klasach TrueClass lub FalseClass w Ruby.

Crystal ma opcjonalne i nazwane argumenty metod jako podstawowe funkcje języka i nie wymaga pisania specjalnego kodu do obsługi argumentów, co jest całkiem fajne. Sprawdź Block#initialize(index = 0, data = "data", previous_hash = "hash") , a następnie wywołaj go za pomocą czegoś takiego jak Block.new(data: data, previous_hash: "0") .

Aby uzyskać bardziej szczegółową listę różnic między językami programowania Crystal i Ruby, zapoznaj się z Crystal for Rubyists.

Teraz spróbujmy utworzyć pięć transakcji za pomocą:

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

Zobacz różnicę? Teraz wszystkie skróty zaczynają się od 00 . To magia dowodu pracy. Korzystając ProofOfWork , znaleźliśmy nonce wartość, a dowodem jest hash z trudnością dopasowania, czyli dwoma wiodącymi zerami 00 .

Zauważ, że w pierwszym utworzonym bloku próbowaliśmy 17 nonces, dopóki nie znaleźliśmy pasującej szczęśliwej liczby:

Blok Pętle / liczba obliczeń skrótu
#0 17
#1 24
#2 61
#3 149
#4 570
# 5 475

Wypróbujmy teraz poziom trudności czterech wiodących zer ( difficulty="0000" ):

Blok Pętle / liczba obliczeń skrótu
#1 26 762
#2 68 419
#3 23 416
#4 15 353

W pierwszym bloku wypróbowano 26762 nonce (porównaj 17 nonces z trudnością '00') aż do znalezienia pasującej szczęśliwej liczby.

Nasz Blockchain jako API

Na razie w porządku. Stworzyliśmy nasz prosty blockchain i było to stosunkowo łatwe do zrobienia. Ale problem polega na tym, że CrystalCoin może działać tylko na jednej maszynie (nie jest rozproszona/zdecentralizowana).

Od teraz zaczniemy używać danych JSON dla CrystalCoin . Dane będą transakcjami, więc pole danych każdego bloku będzie listą transakcji.

Każda transakcja będzie obiektem JSON wyszczególniającym sender monety, receiver monety i amount przesyłanych CrystalCoin:

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

Kilka modyfikacji naszej klasy Block w celu obsługi nowego formatu transaction (wcześniej nazywanego data ). Tak więc, aby uniknąć nieporozumień i zachować spójność, od teraz będziemy używać terminu transaction w odniesieniu do data opublikowanych w naszej przykładowej aplikacji.

Wprowadzimy nową prostą klasę 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

Transakcje są pakowane w bloki, więc blok może zawierać tylko jedną lub wiele transakcji. Bloki zawierające transakcje są często generowane i dodawane do łańcucha bloków.

Blockchain ma być zbiorem bloków. Możemy przechowywać wszystkie bloki na liście Crystal, dlatego wprowadzamy nową klasę Blockchain :

Blockchain będzie miał tablice chain i uncommitted_transactions . chain będzie zawierał wszystkie wydobyte bloki w łańcuchu bloków, a uncommitted_transactions będą zawierały wszystkie transakcje, które nie zostały dodane do łańcucha bloków (nadal nie zostały wydobyte). Po zainicjowaniu Blockchain tworzymy blok genesis za pomocą Block.first i dodajemy go do tablicy chain oraz dodajemy pustą tablicę uncommitted_transactions .

Stworzymy metodę Blockchain#add_transaction , aby dodać transakcje do tablicy uncommitted_transactions .

Zbudujmy naszą nową klasę 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

W klasie Block zaczniemy używać transactions zamiast 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

Teraz, gdy wiemy, jak będą wyglądać nasze transakcje, potrzebujemy sposobu na dodanie ich do jednego z komputerów w naszej sieci blockchain, zwanego node . W tym celu stworzymy prosty serwer HTTP.

Stworzymy cztery punkty końcowe:

  • [POST] /transactions/new : aby utworzyć nową transakcję w bloku
  • [GET] /mine : aby powiedzieć naszemu serwerowi, aby wydobył nowy blok.
  • [GET] /chain : zwraca pełny łańcuch bloków w formacie JSON .
  • [GET] /pending : aby zwrócić oczekujące transakcje ( uncommitted_transactions ).

Będziemy używać frameworka Kemal. Jest to mikrostruktura, która ułatwia mapowanie punktów końcowych do funkcji Crystal. Kemal jest pod silnym wpływem Sinatry dla Rubinistów i działa w bardzo podobny sposób. Jeśli szukasz odpowiednika Ruby on Rails, sprawdź Amber.

Nasz serwer utworzy pojedynczy węzeł w naszej sieci blockchain. Najpierw dodajmy Kemal do pliku shard.yml jako i zainstalujmy zależność:

 dependencies: kemal: github: kemalcr/kemal

Teraz zbudujmy szkielet naszego serwera 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 uruchom serwer:

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

Upewnijmy się, że serwer działa poprawnie:

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

Na razie w porządku. Teraz możemy przystąpić do implementacji każdego z punktów końcowych. Zacznijmy od implementacji /transactions/new i pending punktów końcowych:

 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

Proste wdrożenie. Po prostu tworzymy obiekt CrystalCoin::Block::Transaction i dodajemy transakcję do tablicy uncommitted_transactions za pomocą Blockchain#add_transaction .

W tej chwili transakcje są początkowo przechowywane w puli uncommitted_transactions . Proces umieszczania niepotwierdzonych transakcji w bloku i obliczania Proof of Work (PoW) jest znany jako wydobywanie bloków. Po ustaleniu, że nonce wartość spełnia nasze ograniczenia, możemy powiedzieć, że blok został wydobyty, a nowy blok jest umieszczany w łańcuchu bloków.

W CrystalCoin użyjemy prostego algorytmu Proof-of-Work, który stworzyliśmy wcześniej. Aby utworzyć nowy blok, komputer górnika będzie musiał:

  • Znajdź ostatni blok w chain .
  • Znajdź oczekujące transakcje ( uncommitted_transactions ).
  • Utwórz nowy blok za pomocą Block.next .
  • Dodaj wydobyty blok do tablicy chain .
  • Wyczyść tablicę uncommitted_transactions .

Aby zaimplementować /mine punkt końcowy, zaimplementujmy najpierw powyższe kroki w 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

Upewniamy się, że najpierw mamy kilka oczekujących transakcji do wydobycia. Następnie otrzymujemy ostatni blok za pomocą @chain.last i pierwszych 25 transakcji do wydobycia (używamy Array#shift(BLOCK_SIZE) , aby zwrócić tablicę pierwszych 25 uncommitted_transactions , a następnie usuwamy elementy zaczynające się od indeksu 0) .

Teraz zaimplementujmy /mine end-point:

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

A dla punktu końcowego /chain :

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

Interakcja z naszym łańcuchem bloków

Będziemy używać cURL do interakcji z naszym API przez sieć.

Najpierw odpalmy serwer:

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

Następnie utwórzmy dwie nowe transakcje, wysyłając żądanie POST na http://localhost:3000/transactions/new z treścią zawierającą naszą strukturę transakcji:

 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%

Wypiszmy teraz transakcje oczekujące (tj. transakcje, które nie zostały jeszcze dodane do bloku):

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

Jak widać, dwie utworzone wcześniej transakcje zostały dodane do uncommitted_transactions .

Teraz wykopmy dwie transakcje, wysyłając żądanie GET do http://0.0.0.0:3000/mine :

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

Wygląda na to, że udało nam się wydobyć pierwszy blok i dodać go do naszego chain . Sprawdźmy dokładnie nasz chain i czy zawiera wykopany blok:

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

Konsensus i decentralizacja

To jest fajne. Dostaliśmy sobie podstawowy blockchain, który akceptuje transakcje i pozwala nam wydobywać nowe bloki. Ale kod, który zaimplementowaliśmy do tej pory, ma działać na jednym komputerze, podczas gdy celem blockchainów jest to, że powinny być zdecentralizowane. Ale jeśli są zdecentralizowane, w jaki sposób zapewniamy, że wszystkie odzwierciedlają ten sam łańcuch?

To jest problem Consensus .

Będziemy musieli zaimplementować algorytm konsensusu, jeśli chcemy mieć więcej niż jeden węzeł w naszej sieci.

Rejestracja nowych węzłów

Aby zaimplementować algorytm konsensusu, potrzebujemy sposobu na poinformowanie węzła o sąsiednich węzłach w sieci. Każdy węzeł w naszej sieci powinien prowadzić rejestr innych węzłów w sieci. Dlatego potrzebujemy więcej punktów końcowych:

  • [POST] /nodes/register : aby zaakceptować listę nowych węzłów w postaci adresów URL.
  • [GET] /nodes/resolve : aby zaimplementować nasz algorytm konsensusu, który rozwiązuje wszelkie konflikty — aby upewnić się, że węzeł ma poprawny łańcuch.

Musimy zmodyfikować konstruktor naszego blockchaina i udostępnić metodę rejestracji węzłów:

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

Zauważ, że użyliśmy struktury danych Set z typem String do przechowywania listy węzłów. Jest to tani sposób na zapewnienie, że dodawanie nowych węzłów jest idempotentne i bez względu na to, ile razy dodamy konkretny węzeł, pojawi się dokładnie raz.

Teraz dodajmy nowy moduł do Consensus i zaimplementujmy pierwszą 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

Funkcja register_node przeanalizuje adres URL węzła i sformatuje go.

A tutaj stwórzmy /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

Teraz przy tej implementacji możemy napotkać problem z wieloma węzłami. Kopia łańcuchów kilku węzłów może się różnić. W takim przypadku musimy uzgodnić jakąś wersję łańcucha, aby zachować integralność całego systemu. Musimy osiągnąć konsensus.

Aby rozwiązać ten problem, ustanowimy regułę, że należy użyć najdłuższego prawidłowego łańcucha. Używając tego algorytmu, osiągamy konsensus wśród węzłów w naszej sieci. Powodem takiego podejścia jest to, że najdłuższy łańcuch jest dobrym oszacowaniem największej ilości wykonanej pracy.

tekst alternatywny obrazu

 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

Pamiętaj, że resolve to metoda, która przechodzi przez wszystkie sąsiednie węzły, pobiera ich łańcuchy i weryfikuje je za pomocą valid_chain? metoda. 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"}]}% 

tekst alternatywny obrazu

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

Zawijanie

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.