Тази публикация е опитът ми да разбера ключовите аспекти на блокчейна, като изследвам вътрешните органи. Започнах с четене на оригинален документ за биткойн , но почувствах, че единственият начин да разбера истински блокчейна е чрез изграждането на нова криптовалута от нулата.
Ето защо реших да създам криптовалута, използвайки новия език за програмиране Crystal, и я нарекох CrystalCoin . Тази статия няма да обсъжда избора на алгоритъм, хеш трудността или подобни теми. Вместо това фокусът ще бъде върху детайлизирането на конкретен пример, който трябва да осигури по-задълбочено, практическо разбиране на силните страни и ограниченията на блокчейните.
Ако още не сте го прочели, за повече информация относно алгоритмите и хеширането, ви предлагам да разгледате статията на Демир Селманович Криптовалута за манекени: Биткойн и след това .
За по-добра демонстрация исках да използвам продуктивен език като Руби без да се нарушава производителността. Криптовалутата има много отнемащи време изчисления (а именно минен и хеширане ) и затова компилираните езици като C ++ и Java са избраните езици за изграждане на „истински“ криптовалути. Като се има предвид това, исках да използвам език с по-изчистен синтаксис, за да мога да запазя развитието забавно и да позволя по-добра четливост. Кристалните характеристики и без това са добри.
И така, защо реших да използвам Кристален език за програмиране ? Синтаксисът на Crystal е силно вдъхновен от Ruby’s, така че за мен е естествено да чета и да пиша лесно. Той има допълнителната полза от по-ниската крива на обучение, особено за опитни разработчици на Ruby.
Ето как екипът на Crystal lang го поставя на официалния си уебсайт:
Бърз като C, хлъзгав като Ruby.
Въпреки това, за разлика от Ruby или JavaScript, които са интерпретирани езици, Crystal е компилиран език, което го прави много по-бърз и предлага по-малък отпечатък на паметта. Под капака използва LLVM за компилиране в собствен код.
Crystal също се набира статично, което означава, че компилаторът ще ви помогне да улавяте грешки в типа по време на компилация.
Няма да обяснявам защо считам Crystal езика за страхотен, защото е извън обхвата на тази статия, но ако не смятате, че оптимизмът ми е убедителен, не се колебайте да проверите тази статия за по-добър преглед на потенциала на Crystal.
как да направим език за кодиране
Забележка: Тази статия предполага, че вече имате основни познания за обектно-ориентираното програмиране (OOP).
И така, какво е блокчейн? Това е списък (верига) от блокове, свързани и защитени с цифрови пръстови отпечатъци (известни също като крипто хешове).
Най-лесният начин да мислите за него е като свързана структура от данни на списъка. Като се има предвид това, свързан списък се изисква само да има препратка към предишния елемент; блокът трябва да има идентификатор в зависимост от идентификатора на предишния блок, което означава, че не можете да замените блок, без да преизчислявате всеки отделен блок, който идва след това.
Засега мислете за блокчейн като поредица от блокове с някои данни, свързани с верига, като веригата е хеш на предишния блок.
Цялата блокчейн би съществувала на всеки възел, който иска да взаимодейства с него, което означава, че се копира на всеки един от възлите в мрежата. Нито един сървър не го хоства, но всички компании за разработка на блокчейн използвайте го, което го прави децентрализиран .
Да, това е странно в сравнение с конвенционалните централизирани системи. Всеки от възлите ще има копие на цялата блокчейн (> 149 Gb в биткойн блокчейн от Декември 2017 ).
И така, каква е тази хеш функция? Мислете за хеш като за функция, която връща уникален пръстов отпечатък, когато му дадем текст / обект. Дори най-малката промяна във входния обект би променила драстично пръстовия отпечатък.
Има различни алгоритми за хеширане и в тази статия ще използваме SHA256
хеш алгоритъм, който се използва в Bitcoin
.
Използване на SHA256
винаги ще имаме 64 шестнадесетични символа (256 бита) с дължина, дори ако входът е по-малък от 256 бита или много по-голям от 256 бита:
Вход | Хеширани резултати |
---|---|
Много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго TEX много дълго ТЕКСТ много дълго много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ много дълго ТЕКСТ МНОГО ДЪЛГ ТЕКСТ МНОГО ДЪЛГ ТЕКСТ | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Маймунско бягство | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
Маймунско бягство. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
Забележете с последния пример, че просто добавяне на .
(точка) доведе до драстична промяна в хеша.
Следователно в блокчейн веригата се изгражда чрез предаване на блоковите данни в алгоритъм за хеширане, който би генерирал хеш, който е свързан със следващия блок, оттук нататък, образувайки поредица от блокове, свързани с хешовете на предишните блокове.
Сега нека започнем да създаваме нашия Crystal проект и да изградим нашите SHA256
криптиране.
Ако приемем, че имате Инсталиран е език за програмиране Crystal , нека създадем скелета на CrystalCoin
codebase с помощта на вградения инструмент за проектиране на Crystal 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/
Тази команда ще създаде основната структура за проекта, с вече инициализирано git хранилище, файлове за лиценз и readme. Той също така се предлага с заглушки за тестове и shard.yml
файл за описание на проекта и управление на зависимости, известен също като парчета.
Нека добавим openssl
парче, което е необходимо за изграждане SHA256
алгоритъм:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
След като влезете, върнете се обратно в терминала си и стартирайте crystal deps
. Правейки това ще изтегли openssl
и неговите зависимости, които да използваме.
Сега имаме инсталирана необходимата библиотека в нашия код, нека започнем с дефиниране Block
клас и след това изграждане на хеш функцията.
# 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
Вече можете да тествате приложението си, като стартирате crystal run crystal src/crystal_coin/block.cr
от вашия терминал.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
Всеки блок се съхранява с timestamp
и, по желание, index
. В CrystalCoin
ще съхраним и двете. За да се гарантира целостта на цялата верига блокчета, всеки блок ще има самоидентифициране хеш . Подобно на Bitcoin, хешът на всеки блок ще бъде криптографски хеш на блока (index
, timestamp
, data
и хешът на хеш на предишния блок previous_hash
). Засега данните могат да бъдат всичко, което искате.
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
В Crystal lang заместваме Ruby’s attr_accessor
, attr_getter
и attr_setter
методи с нови ключови думи:
Ключова дума Ruby | Кристална ключова дума |
---|---|
attr_accessor | Имот |
attr_reader | гетер |
attr_writer | сетер |
Друго нещо, което може би сте забелязали в Crystal е, че искаме да намекнем на компилатора за конкретни типове чрез нашия код. Crystal извежда типовете, но винаги, когато имате неяснота, можете изрично да декларирате и типове. Ето защо добавихме String
типове за current_hash
.
Сега нека пуснем block.cr
два пъти и имайте предвид, че едни и същи данни ще генерират различни хешове поради различните timestamp
:
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
Сега имаме наша блокова структура, но ние създаваме блокчейн. Трябва да започнем да добавяме блокове, за да образуваме действителна верига. Както споменах по-рано, всеки блок изисква информация от предишния блок. Но как първият блок в блокчейна стига до там? Е, първият блок или genesis
блок, е специален блок (блок без предшественици). В много случаи се добавя ръчно или има уникална логика, позволяваща добавянето му.
Ще създадем нова функция, която връща блок за генезис. Този блок е от index=0
и има произволна стойност на данните и произволна стойност в previous_hash
параметър.
Нека да изградим или метод на клас Block.first
който генерира генезисния блок:
module CrystalCoin class Block ... def self.first(data='Genesis Block') Block.new(data: data, previous_hash: '0') end ... end end
И нека тестваме, използвайки p CrystalCoin::Block.first
:
#
Сега, когато успяхме да създадем генезис блок, имаме нужда от функция, която ще генерира следващи блокове в блокчейна.
Тази функция ще вземе предишния блок във веригата като параметър, ще създаде данните за генерирания блок и ще върне новия блок със съответните данни. Когато новите блокове хешират информация от предишни блокове, целостта на блокчейна се увеличава с всеки нов блок.
Важно последствие е, че блок не може да бъде модифициран, без да се променя хешът на всеки последователен блок. Това е показано в примера по-долу. Ако данните в блок 44 се променят от LOOP
до EAST
, всички хешове на последователните блокове трябва да бъдат променени. Това е така, защото хешът на блока зависи от стойността на previous_hash
(наред с други неща).
как да получите терминал на bloomberg
Ако не направихме това, би било по-лесно за външна страна да промени данните и да замени нашата верига с изцяло нова собствена. Тази верига от хешове действа като криптографско доказателство и помага да се гарантира, че след като блокът бъде добавен към блокчейна, той не може да бъде заменен или премахнат. Нека създадем метода на класа 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
За да изпробваме всичко заедно, ще създадем проста блокчейн. Първият елемент от списъка е блокът на генезиса. И разбира се, трябва да добавим следващите блокове. Ще създадем пет нови блока, за да демонстрираме 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
[#, #, #, #, #, #
Алгоритъм за доказателство за работа (PoW) е начинът, по който се създават или блокират нови блокове добит на блокчейна. Целта на PoW е да открие число, което решава проблем. Номерът трябва да е труден за намиране, но лесен за изчислителна проверка от всеки в мрежата. Това е основната идея зад Proof of Work.
Нека демонстрираме с пример, за да сме сигурни, че всичко е ясно. Ще приемем, че хешът на някакво цяло число x, умножено по друго y, трябва да започва с 00
. Така:
hash(x * y) = 00ac23dc...
И за този опростен пример, нека поправим x=5
и внедрете това в 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)}'
Нека пуснем кода:
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
Както можете да видите това число y=530
беше трудно да се намери (груба сила), но беше лесно да се провери с помощта на хеш функцията.
Защо да се занимавам с този алгоритъм на PoW? Защо просто не създадем по един хеш на блок и това е всичко? Хеш трябва да бъде валиден . В нашия случай хеш ще бъде валиден, ако първите два знака от нашия хеш са 00
. Ако нашият хеш започва с 00......
, той се счита за валиден. Това се нарича трудност . Колкото по-голяма е трудността, толкова по-дълго е необходимо да се получи валиден хеш.
Но ако хешът не е валиден за първи път, нещо трябва да се промени в данните, които използваме. Ако използваме едни и същи данни отново и отново, ще получаваме един и същ хеш отново и отново и нашият хеш никога няма да бъде валиден. Използваме нещо, наречено nonce
в нашия хеш (в нашия предишен пример това е y
). Това е просто число, което увеличаваме всеки път, когато хешът не е валиден. Получаваме нашите данни (дата, съобщение, предишен хеш, индекс) и nonce от 1. Ако хешът, който получаваме с тях, не е валиден, опитваме с nonce от 2. И увеличаваме nonce, докато получим валиден хеш .
В биткойн се извиква алгоритъмът Proof of Work Hashcash . Нека добавим доказателство за работа към нашия клас Block и да започнем минен за да намерим нонса. Ще използваме твърдо кодиран трудност от две водещи нули „00“:
Сега нека преработим нашия клас Block, за да го подкрепим. Нашите CrystalCoin
Блокът ще съдържа следните атрибути:
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
Ще създам отделен модул, за да направя хеширането и да намеря nonce
така че поддържаме нашия код чист и модулен. Ще го нарека 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
Нашите Block
клас би изглеждал по следния начин:
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
Малко неща, които трябва да се отбележат за кода на Crystal и примери за език на Crystal като цяло. В Crystal методите са публични по подразбиране. Crystal изисква всеки частен метод да бъде с префикс с частната ключова дума, която може да е объркваща, идваща от Ruby.
Може би сте забелязали, че Integer типовете на Crystal има Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
, или UInt64
в сравнение с Ruby’s Fixnum
. true
и false
са стойности в Bool
клас, а не стойности в класове TrueClass
или FalseClass
в Руби.
Crystal има незадължителни и именувани аргументи на метода като основни функции на езика и не изисква писане на специален код за обработка на аргументите, което е доста готино. Вижте Block#initialize(index = 0, data = 'data', previous_hash = 'hash')
и след това да го извикате с нещо като Block.new(data: data, previous_hash: '0')
.
За по-подробен списък на разликите между езика за програмиране Crystal и Ruby вижте Кристал за рубисти .
Сега, нека се опитаме да създадем пет транзакции, като използваме:
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
[#] # # # # #
Виждате ли разликата? Сега всички хешове започват с 00
. Това е магията на доказателството за работа. Използване на ProofOfWork
намерихме nonce
а доказателство е хешът със съвпадение на трудността, тоест двете водещи нули 00
.
Забележете с първия блок, който създадохме, опитахме 17 нонса, докато намерим съответстващото щастливо число:
Блок | Цикли / брой изчисления на хеш |
---|---|
# 0 | 17 |
#one | 24 |
# 2 | 61 |
# 3 | 149 |
# 4 | 570 |
# 5 | 475 |
Сега нека опитаме трудност от четири водещи нули (difficulty='0000'
):
Блок | Цикли / брой изчисления на хеш |
---|---|
#one | 26 762 |
# 2 | 68 419 |
# 3 | 23 416 |
# 4 | 15 353 |
В първия блок се опитаха 26762 нонса (сравнете 17 нонса с трудност ‘00’), докато се намери съответстващото щастливо число.
Дотук добре. Създадохме нашата проста блокчейн и беше относително лесно да се направи. Но проблемът тук е, че CrystalCoin
може да работи само на една машина (не е разпределена / децентрализирана).
Отсега нататък ще започнем да използваме JSON данни за CrystalCoin
. Данните ще бъдат транзакции, така че полето за данни на всеки блок ще бъде списък с транзакции.
Всяка транзакция ще бъде JSON обект с подробности за sender
на монетата, receiver
на монетата, а amount
на CrystalCoin, който се прехвърля:
{ 'from': '71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij', 'to': '93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo', 'amount': 3 }
Няколко модификации на нашите Block
клас за поддръжка на новия transaction
формат (наречен по-рано data
). Така че, за да избегнем объркване и да запазим последователност, ще използваме термина transaction
да се позовава на data
публикувано в нашето примерно приложение оттук нататък.
Ще представим нов прост клас 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
Транзакциите са пакетирани в блокове, така че блок може да съдържа само една или множество транзакции. Блоковете, съдържащи транзакциите, се генерират често и се добавят към блокчейна.
Предполага се, че блокчейнът е колекция от блокове. Можем да съхраняваме всички блокове в списъка Crystal и затова представяме новия клас Blockchain
:
Blockchain
ще има chain
и uncommitted_transactions
масиви. chain
ще включва всички извлечени блокове в блокчейна и uncommitted_transactions
ще има всички транзакции, които не са добавени към блокчейна (все още не са минирани). След като инициализираме Blockchain
, ние създаваме генезисния блок, използвайки Block.first
и го добавете към chain
масив и добавяме празен uncommitted_transactions
масив.
Ще създадем Blockchain#add_transaction
метод за добавяне на транзакции към uncommitted_transactions
масив.
Нека да изградим нашия нов 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
В Block
клас ще започнем да използваме transactions
вместо 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
Сега, когато знаем как ще изглеждат транзакциите ни, имаме нужда от начин да ги добавим към един от компютрите в нашата блокчейн мрежа, наречен | node
За целта ще създадем прост HTTP сървър.
Ще създадем четири крайни точки:
/transactions/new
: за създаване на нова транзакция към блок/mine
: да кажем на нашия сървър да копае нов блок./chain
: за връщане на пълната блокчейн в JSON
формат./pending
: за връщане на чакащите транзакции (uncommitted_transactions
).Ще използваме Зрелост уеб рамка. Това е микро-рамка, която улеснява картографирането на крайните точки към функциите на Crystal. Кемал е силно повлиян от Синатра за рубистите и работи по много подобен начин. Ако търсите Рубин на релси еквивалент, след това проверете Амбър .
Нашият сървър ще формира един възел в нашата блокчейн мрежа. Нека първо добавим Kemal
към shard.yml
файл като a и инсталирайте зависимостта:
dependencies: kemal: github: kemalcr/kemal
Сега нека изградим скелета на нашия 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
И стартирайте сървъра:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Нека се уверим, че сървърът работи нормално:
списък на фондове за частни капиталови инвестиции
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
Дотук добре. Сега можем да продължим с прилагането на всяка от крайните точки. Нека започнем с внедряване /transactions/new
и 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
Правоприлагане. Ние просто създаваме CrystalCoin::Block::Transaction
обект и добавете транзакцията към uncommitted_transactions
масив, използващ Blockchain#add_transaction
.
В момента транзакциите първоначално се съхраняват в пул от uncommitted_transactions
. Процесът на поставяне на непотвърдените транзакции в блок и изчисляване на доказателство за работа (PoW) е известен като минен на блокове. След като nonce
удовлетворяването на нашите ограничения е разбрано, можем да кажем, че блок е добит и новият блок е поставен в блокчейна.
В CrystalCoin
ще използваме простия алгоритъм Proof-of-Work, който създадохме по-рано. За да създаде нов блок, компютърът на миньор трябва да:
chain
.uncommitted_transactions
).Block.next
.chain
масив.uncommitted_transactions
масив.Така че да се приложи /mine
крайна точка, нека първо приложим горните стъпки в 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
Първо се уверяваме, че имаме някои изчакващи транзакции за копаене. След това получаваме последния блок, използвайки @chain.last
, и първия 25
транзакции, които трябва да бъдат извлечени (използваме Array#shift(BLOCK_SIZE)
, за да върнем масив от първите 25 uncommitted_transactions
и след това да премахнем елементите, започващи от индекс 0).
Сега нека приложим /mine
крайна точка:
get '/mine' do blockchain.mine 'Block with index=#{blockchain.chain.last.index} is mined.' end
И за /chain
крайна точка:
get '/chain' do { chain: blockchain.chain }.to_json end
Ще използваме cURL
за взаимодействие с нашия API през мрежа.
Първо, нека запалим сървъра:
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
След това нека създадем две нови транзакции, като направим POST
искане до http://localhost:3000/transactions/new
с тяло, съдържащо нашата структура на транзакциите:
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 # 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 # has been added to the node%
Сега нека изброим чакащите транзакции (т.е. транзакции, които все още не са добавени към блока):
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { 'transactions':[ { 'from':'ekis', 'to':'huslks', 'amount':7090 }, { 'from':'ekis', 'to':'huslks', 'amount':70900 } ] }
Както виждаме, двете транзакции, които създадохме по-рано, бяха добавени към uncommitted_transactions
.
Сега нека моята двете транзакции чрез извършване на GET
заявка до http://0.0.0.0:3000/mine
:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
Изглежда, че успешно копахме първия блок и го добавихме към нашите chain
. Нека проверим отново нашите chain
и ако включва минирания блок:
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' } ] }
Това е готино. Набавихме си основна блокчейн, която приема транзакции и ни позволява да копаем нови блокове. Но кодът, който сме прилагали досега, е предназначен да работи на един компютър, докато целият смисъл на блокчейн е, че те трябва да бъдат децентрализирани. Но ако те са децентрализирани, как гарантираме, че всички те отразяват една и съща верига?
Това е проблемът на Consensus
.
Ще трябва да внедрим консенсус алгоритъм, ако искаме повече от един възел в нашата мрежа.
За да приложим консенсусен алгоритъм, ни е необходим начин да информираме възел за съседните възли в мрежата. Всеки възел в нашата мрежа трябва да поддържа регистър на други възли в мрежата. Следователно ще ни трябват повече крайни точки:
/nodes/register
: за приемане на списък с нови възли под формата на URL адреси./nodes/resolve
: за прилагане на нашия консенсусен алгоритъм, който разрешава всички конфликти - за да се гарантира, че възелът има правилната верига.Трябва да модифицираме конструктора на нашия блокчейн и да предоставим метод за регистриране на възли:
--- 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)
Имайте предвид, че сме използвали Set
структура на данните с String
въведете, за да задържите списъка с възли. Това е евтин начин да се гарантира, че добавянето на нови възли е идемпотентно и че независимо колко пъти добавяме конкретен възел, той се появява точно веднъж.
Сега нека добавим нов модул към Consensus
и внедрете първия метод 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
register_node
функция, ще анализира URL адреса на възела и ще го форматира.
И тук нека създадем /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
Сега с тази реализация може да се сблъскаме с проблем с множество възли. Копието на вериги от няколко възли може да се различава. В този случай трябва да се споразумеем за някаква версия на веригата, за да поддържаме целостта на цялата система. Трябва да постигнем консенсус.
За да разрешим това, ще направим правилото, че най-дългата валидна верига е тази, която ще се използва. Използвайки този алгоритъм, ние постигаме консенсус между възлите в нашата мрежа. Причината зад този подход е, че най-дългата верига е добра оценка за най-много свършена работа.
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
Имайте предвид, че resolve
е метод, който циклично преминава през всички наши съседни възли, изтегля техните вериги и ги проверява с помощта на valid_chain?
метод. Ако бъде намерена валидна верига, чиято дължина е по-голяма от нашата, ние заместваме нашата.
Сега нека приложим parse_chain()
и valid_chain?()
частни методи:
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
За parse_chain()
ние:
GET
HTTP заявка с използване на HTTP::Client.get
до /chain
крайна точка./chain
JSON отговор, използващ JSON.parse
.CrystalCoin::Block
обекти от JBON петно, което беше върнато с помощта на Array(CrystalCoin::Block).from_json(node_chain)
.Има повече от един начин за анализ на JSON в Crystal. Предпочитаният метод е да се използва супер-удобният Crystal's JSON.mapping(key_name: Type)
функционалност, която ни дава следното:
Class.from_json
.instance.to_json
.В нашия случай трябваше да определим JSON.mapping
в CrystalCoin::Block
обект и премахнахме property
използване в класа, така:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Сега за Blockchain#valid_chain?
итерираме през всички блокове и за всеки:
Block#recalculate_hash
и проверете дали хешът на блока е правилен:module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
difficulty
в нашия случай 00
).И накрая изпълняваме /nodes/resolve
крайна точка:
get '/nodes/resolve' do if blockchain.resolve 'Successfully updated the chain' else 'Current chain is up-to-date' end end
Готово е! Можете да намерите окончателен код на GitHub.
Структурата на нашия проект трябва да изглежда така:
защо Гърция има толкова дългове
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
http://localhost:3000
и http://localhost:3001
.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'}%
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 # has been added to the node%
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
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'}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Нека проверим дали веригата в първия възел е актуализирана:
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'}]}%
Ура! Нашият пример за език на Crystal работи като чар и се надявам, че този дълъг урок е кристално ясен, извинете за каламбура.
Този урок за Crystal език обхваща основите на публичната блокчейн. Ако последвахте, внедрихте блокчейн от нулата и създадохте просто приложение, което позволява на потребителите да споделят информация за блокчейна.
На този етап направихме доста голяма блокчейн. Сега, CrystalCoin
може да се стартира на множество машини за създаване на мрежа и реално CrystalCoins
може да се добива.
Надявам се, че това ви е вдъхновило да създадете нещо ново или поне да разгледате по-отблизо програмирането на Crystal.
Забележка: Кодът в този урок не е готов за реална употреба. Моля, вижте това като само общо ръководство.
Crystal е родов език. Можете да правите всичко, което можете да правите в Ruby, използвайки Crystal, с по-добра производителност и по-малко използване на паметта. Една от точките за продажба на Crystal е лекотата, с която можете да взаимодействате с C библиотеки, тъй като Crystal ви позволява да се свързвате със съществуващи C библиотеки, без да пишете нито един ред в C.
Crystal е език за програмиране, който може лесно да се разбере от хората и се компилира в бързи програми. Това е статично написан, компилиран език, който постига производителност, близка до c / c ++, като същевременно има синтаксис, толкова четим, колкото Ruby.
Crystal има подобен на Ruby синтаксис, но това е различен език, а не Ruby изпълнение. Тъй като това е компилиран, статически типизиран език, езикът има някои разлики в сравнение с Ruby. Идвайки от Ruby, Crystal има ниска крива на обучение.
Ако обичате пълнотата на Rails, ще се почувствате като у дома си с рамката Amber. Ако простотата и лесното персонализиране на Sinatra са по-вашите неща, ще откриете тази простота с Kemal.
Това е един от най-обещаващите езици за програмиране днес. Изглежда, че е разрушил бариерата между синтактично сладко интерпретирани / динамични езици като Ruby / Python, които са обичани заради четливостта и лекотата на използване, и суровите конски сили на C / C ++ и системните езици на ниско ниво.