การสร้าง Cryptocurrency ในภาษาโปรแกรม Crystal
เผยแพร่แล้ว: 2022-03-11โพสต์นี้เป็นความพยายามของฉันในการทำความเข้าใจประเด็นสำคัญของบล็อคเชนโดยการสำรวจภายใน ฉันเริ่มต้นด้วยการอ่าน whitepaper ของ bitcoin ดั้งเดิม แต่ฉันรู้สึกว่าวิธีเดียวที่จะเข้าใจ blockchain อย่างแท้จริงคือการสร้าง cryptocurrency ใหม่ตั้งแต่ต้น
นั่นเป็นเหตุผลที่ฉันตัดสินใจสร้างสกุลเงินดิจิทัลโดยใช้ภาษาการเขียนโปรแกรม Crystal ใหม่ และฉันตั้งชื่อว่า CrystalCoin บทความนี้จะไม่กล่าวถึงตัวเลือกอัลกอริทึม ความยากของแฮช หรือหัวข้อที่คล้ายกัน แต่จะเน้นไปที่รายละเอียดตัวอย่างที่เป็นรูปธรรม ซึ่งควรให้ความเข้าใจที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับจุดแข็งและข้อจำกัดของบล็อกเชน
หากคุณยังไม่ได้อ่าน สำหรับพื้นหลังเพิ่มเติมเกี่ยวกับ algos และ hashing เราขอแนะนำให้คุณอ่านบทความเรื่อง Cryptocurrency for Dummies: Bitcoin and Beyond ของ Demir Selmanovic
ทำไมฉันถึงเลือกภาษาโปรแกรมคริสตัล
เพื่อการสาธิตที่ดีขึ้น ฉันต้องการใช้ภาษาที่มีประสิทธิภาพเช่น Ruby โดยไม่กระทบต่อประสิทธิภาพ Cryptocurrency มีการคำนวณที่ใช้เวลานานมาก (เช่น การขุด และ การแฮช ) และนั่นเป็นสาเหตุที่ภาษาที่คอมไพล์ เช่น C++ และ Java เป็นภาษาที่เลือกไว้สำหรับการสร้าง cryptocurrencies “ของจริง” ดังที่กล่าวไปแล้ว ฉันต้องการใช้ภาษาที่มีไวยากรณ์ที่สะอาดขึ้น เพื่อที่ฉันจะได้พัฒนาให้สนุกต่อการพัฒนาและช่วยให้อ่านง่ายขึ้น ประสิทธิภาพของคริสตัลมีแนวโน้มที่จะดีอยู่ดี
เหตุใดฉันจึงตัดสินใจใช้ภาษาโปรแกรม Crystal ไวยากรณ์ของ Crystal ได้รับแรงบันดาลใจอย่างมากจาก Ruby's ดังนั้นสำหรับฉัน มันจึงให้ความรู้สึกเป็นธรรมชาติในการอ่านและเขียนง่าย มีประโยชน์เพิ่มเติมจากช่วงการเรียนรู้ที่ต่ำกว่า โดยเฉพาะอย่างยิ่งสำหรับนักพัฒนา Ruby ที่มีประสบการณ์
นี่คือวิธีที่ทีม Crystal lang วางไว้ที่เว็บไซต์อย่างเป็นทางการ:
เร็วเท่า C เนียนเหมือน Ruby
อย่างไรก็ตาม ไม่เหมือนกับ Ruby หรือ JavaScript ซึ่งเป็นภาษาที่แปลแล้ว Crystal เป็นภาษาที่คอมไพล์ ทำให้เร็วขึ้นมากและให้พื้นที่หน่วยความจำที่ต่ำกว่า ภายใต้ประทุน จะใช้ LLVM เพื่อคอมไพล์เป็นโค้ดเนทีฟ
Crystal ยังพิมพ์แบบสแตติก ซึ่งหมายความว่าคอมไพเลอร์จะช่วยคุณตรวจจับข้อผิดพลาดประเภทในเวลาคอมไพล์
ฉันจะไม่อธิบายว่าทำไมฉันถึงคิดว่าภาษา Crystal นั้นยอดเยี่ยมเพราะมันอยู่นอกเหนือขอบเขตของบทความนี้ แต่ถ้าคุณไม่คิดว่าการมองโลกในแง่ดีของฉันน่าเชื่อถือ อย่าลังเลที่จะอ่านบทความนี้เพื่อดูภาพรวมที่ดีขึ้นเกี่ยวกับศักยภาพของ Crystal
หมายเหตุ: บทความนี้ถือว่าคุณมีความเข้าใจพื้นฐานเกี่ยวกับ Object Oriented Programming (OOP) แล้ว
บล็อกเชน
ดังนั้นบล็อคเชนคืออะไร? เป็นรายการ (ห่วงโซ่) ของบล็อกที่เชื่อมโยงและรักษาความปลอดภัยด้วยลายนิ้วมือดิจิทัล (หรือที่เรียกว่าแฮชเข้ารหัสลับ)
วิธีคิดที่ง่ายที่สุดคือเป็นโครงสร้างข้อมูลรายการลิงก์ ดังที่กล่าวไปแล้ว รายการเชื่อมโยงจำเป็นต้องมีการอ้างอิงถึงองค์ประกอบก่อนหน้าเท่านั้น บล็อกต้องมีตัวระบุขึ้นอยู่กับตัวระบุของบล็อกก่อนหน้า หมายความว่าคุณไม่สามารถแทนที่บล็อกโดยไม่ต้องคำนวณใหม่ทุกบล็อกที่ตามมา
สำหรับตอนนี้ ให้คิดว่าบล็อคเชนเป็นชุดของบล็อกที่มีข้อมูลบางส่วนเชื่อมโยงกับเชน โดยเชนนั้นเป็นแฮชของบล็อกก่อนหน้า
blockchain ทั้งหมดจะมีอยู่ในแต่ละโหนดที่ต้องการโต้ตอบกับมัน หมายความว่ามันถูกคัดลอกในแต่ละโหนดในเครือข่าย ไม่มีเซิร์ฟเวอร์ใดโฮสต์มัน แต่บริษัทพัฒนาบล็อคเชนทั้งหมดใช้มัน ซึ่งทำให้ กระจายอำนาจ
ใช่ มันแปลกเมื่อเทียบกับระบบรวมศูนย์ทั่วไป แต่ละโหนดจะมีสำเนาของ blockchain ทั้งหมด (> 149 Gb ใน Bitcoin blockchain ภายในเดือนธันวาคม 2017)
การแฮชและลายเซ็นดิจิทัล
แล้วฟังก์ชันแฮชนี้คืออะไร? คิดว่าแฮชเป็นฟังก์ชัน ซึ่งจะส่งกลับลายนิ้วมือที่ไม่ซ้ำกันเมื่อเราให้ข้อความ/วัตถุแก่มัน แม้แต่การเปลี่ยนแปลงที่น้อยที่สุดในวัตถุอินพุตก็เปลี่ยนลายนิ้วมือได้อย่างมาก
มีอัลกอริธึมการแฮชที่แตกต่างกัน และในบทความนี้ เราจะใช้อัลกอริทึมแฮช SHA256
ซึ่งเป็นอัลกอริธึมที่ใช้ใน Bitcoin
การใช้ SHA256
เราจะให้ความยาว 64 อักขระฐานสิบหก (256 บิต) แม้ว่าอินพุตจะน้อยกว่า 256 บิตหรือใหญ่กว่า 256 บิตมาก:
ป้อนข้อมูล | ผลลัพธ์ที่แฮช |
---|---|
ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความที่ยาวมาก ข้อความยาวมาก ข้อความยาวมาก | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Toptal | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
ท็อปทัล. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
สังเกตด้วยตัวอย่างสุดท้ายว่าเพียงแค่เพิ่ม .
(จุด) ส่งผลให้เกิดการเปลี่ยนแปลงอย่างมากในแฮช
ดังนั้น ในบล็อกเชน เชนถูกสร้างขึ้นโดยส่งข้อมูลบล็อกไปยังอัลกอริธึมการแฮชซึ่งจะสร้างแฮช ซึ่งเชื่อมโยงกับบล็อกถัดไป ต่อจากนี้ไป สร้างชุดบล็อกที่เชื่อมโยงกับแฮชของบล็อกก่อนหน้า
การสร้าง Cryptocurreny ใน Crystal
ตอนนี้ มาเริ่มสร้างโครงการ Crystal ของเราและสร้างการเข้ารหัส SHA256
ของเรา
สมมติว่าคุณติดตั้งภาษาโปรแกรม Crystal แล้ว ให้สร้างโครงกระดูกของ Codebase ของ CrystalCoin
โดยใช้ 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 ของบล็อก 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 เราแทนที่ attr_accessor
, attr_getter
และ attr_setter
ของ Ruby ด้วยคีย์เวิร์ดใหม่:
ทับทิมคำสำคัญ | คีย์เวิร์ดคริสตัล |
---|---|
attr_accessor | คุณสมบัติ |
attr_reader | getter |
attr_writer | setter |
อีกสิ่งหนึ่งที่คุณอาจสังเกตเห็นใน 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
กำหนดเองในพารามิเตอร์ 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
:
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
ตอนนี้เราสามารถสร้างบล็อค กำเนิด ได้แล้ว เราต้องการฟังก์ชันที่จะสร้างบล็อคที่ประสบความสำเร็จในบล็อคเชน
ฟังก์ชันนี้จะนำบล็อกก่อนหน้าในสายโซ่เป็นพารามิเตอร์ สร้างข้อมูลสำหรับบล็อกที่จะสร้าง และส่งคืนบล็อกใหม่พร้อมข้อมูลที่เหมาะสม เมื่อบล็อคใหม่แฮชข้อมูลจากบล็อคก่อนหน้า ความสมบูรณ์ของบล็อคเชนจะเพิ่มขึ้นตามแต่ละบล็อคใหม่
ผลที่ตามมาที่สำคัญคือไม่สามารถแก้ไขบล็อกได้โดยไม่เปลี่ยนแฮชของทุกบล็อกที่ต่อเนื่องกัน สิ่งนี้แสดงให้เห็นในตัวอย่างด้านล่าง หากข้อมูลในบล็อก 44 เปลี่ยนจาก LOOP
เป็น EAST
แฮชทั้งหมดของบล็อกที่ต่อเนื่องกันจะต้องเปลี่ยน เนื่องจากแฮชของบล็อกจะขึ้นอยู่กับค่าของ previous_hash
(เหนือสิ่งอื่นใด)
หากเราไม่ทำเช่นนี้ มันจะง่ายกว่าสำหรับบุคคลภายนอกในการเปลี่ยนแปลงข้อมูลและแทนที่ห่วงโซ่ของเราด้วยข้อมูลใหม่ทั้งหมดของพวกเขาเอง ห่วงโซ่ของแฮชนี้ทำหน้าที่เป็นหลักฐานการเข้ารหัสและช่วยให้แน่ใจว่าเมื่อเพิ่มบล็อกในบล็อคเชนแล้ว จะไม่สามารถแทนที่หรือลบออกได้ มาสร้างวิธีการเรียนกัน 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
[#<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)">
หลักฐานการทำงาน
อัลกอริธึม Proof of Work (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 จนกว่าเราจะได้ hash ที่ถูกต้อง .
ใน Bitcoin อัลกอริธึม Proof of Work เรียกว่า Hashcash มาเพิ่มหลักฐานการทำงานให้กับคลาส Block ของเราและเริ่ม ขุด เพื่อค้นหา nonce เราจะใช้ความ ยาก แบบฮาร์ดโค้ดของเลขศูนย์สองตัวนำหน้า '00':
ตอนนี้เรามาออกแบบคลาส Block ใหม่เพื่อรองรับสิ่งนั้น CrystalCoin
Block ของเราจะมีคุณลักษณะดังต่อไปนี้:
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
ใน Ruby
Crystal มีอาร์กิวเมนต์เมธอดที่เป็นทางเลือกและตั้งชื่อเป็นคุณสมบัติหลักของภาษา และไม่ต้องเขียนโค้ดพิเศษเพื่อจัดการกับอาร์กิวเมนต์ซึ่งค่อนข้างเจ๋ง ลองดู Block#initialize(index = 0, data = "data", previous_hash = "hash")
แล้วเรียกใช้ด้วยบางอย่างเช่น Block.new(data: data, previous_hash: "0")
หากต้องการทราบรายละเอียดเพิ่มเติมเกี่ยวกับความแตกต่างระหว่างภาษาโปรแกรม Crystal และ Ruby โปรดดูที่ Crystal for Rubyists
ตอนนี้ มาลองสร้างธุรกรรมห้ารายการโดยใช้:
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">
ดูความแตกต่าง? ตอนนี้แฮชทั้งหมดเริ่มต้นด้วย 00
นั่นคือความมหัศจรรย์ของการพิสูจน์การทำงาน การใช้ ProofOfWork
เราพบว่า nonce
และ proof คือ hash ที่มีความยากในการจับคู่ นั่นคือ เลขศูนย์นำหน้าสองตัว 00
หมายเหตุ ด้วยบล็อกแรกที่เราสร้างขึ้น เราลอง 17 nonce จนกระทั่งพบเลขนำโชคที่ตรงกัน:
ปิดกั้น | ลูป / จำนวนการคำนวณแฮช |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
ตอนนี้ มาลองความยากของเลขศูนย์นำหน้าสี่ตัว ( difficulty="0000"
):
ปิดกั้น | ลูป / จำนวนการคำนวณแฮช |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
ในบล็อกแรกลอง 26762 nonce (เปรียบเทียบ 17 nonce ที่มีปัญหา '00') จนกว่าจะพบเลขนำโชคที่ตรงกัน
Blockchain ของเราเป็น API
จนถึงตอนนี้ดีมาก เราสร้างบล็อคเชนที่เรียบง่ายของเราและค่อนข้างง่ายที่จะทำ แต่ปัญหาที่นี่คือ 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
ธุรกรรมจะถูกบรรจุเป็นบล็อค ดังนั้นบล็อกสามารถมีได้เพียงหนึ่งหรือหลายธุรกรรม บล็อกที่มีธุรกรรมถูกสร้างขึ้นบ่อยครั้งและเพิ่มลงในบล็อคเชน
blockchain ควรจะเป็นชุดของบล็อก เราสามารถจัดเก็บบล็อคทั้งหมดในรายการ Crystal และนั่นคือเหตุผลที่เราแนะนำ Blockchain
คลาสใหม่:
Blockchain
จะมีอาร์เรย์ chain
และ uncommitted_transactions
chain
จะรวมบล็อคที่ขุดได้ทั้งหมดในบล็อคเชน และ uncommitted_transactions
จะมีธุรกรรมทั้งหมดที่ยังไม่ได้เพิ่มในบล็อคเชน (ยังไม่ขุด) เมื่อเราเริ่มต้น Blockchain
เราจะสร้างบล็อคแหล่งกำเนิดโดยใช้ Block.first
และเพิ่มไปยัง chain
array และเราเพิ่ม uncommitted_transactions
array ที่ว่างเปล่า
เราจะสร้างวิธี 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 อย่างง่าย
เราจะสร้างจุดปลายสี่จุด:
- [POST]
/transactions/new
: เพื่อสร้างธุรกรรมใหม่ให้กับ block - [GET]
/mine
: เพื่อบอกให้เซิร์ฟเวอร์ของเราขุดบล็อกใหม่ - [GET]
/chain
: เพื่อส่งคืน blockchain แบบเต็มในรูปแบบJSON
- [GET]
/pending
: เพื่อส่งคืนธุรกรรมที่รอดำเนินการ (uncommitted_transactions
)
เราจะใช้เฟรมเวิร์กเว็บ Kemal เป็นไมโครเฟรมเวิร์กที่ทำให้ง่ายต่อการแมปจุดปลายกับฟังก์ชัน Crystal Kemal ได้รับอิทธิพลอย่างมากจาก Sinatra สำหรับ Rubyists และทำงานในลักษณะที่คล้ายกันมาก หากคุณกำลังมองหา Ruby on Rails ที่เทียบเท่าลองดู Amber
เซิร์ฟเวอร์ของเราจะสร้างโหนดเดียวในเครือข่ายบล็อคเชนของเรา ก่อนอื่นให้เพิ่ม 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
และ end-points ที่ 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
กระบวนการของการวางธุรกรรมที่ไม่ได้รับการยืนยันในบล็อคและการคำนวณ Proof of Work (PoW) เรียกว่าการ ขุด บล็อค เมื่อพบ nonce
ที่ตรงตามข้อจำกัดของเราแล้ว เราสามารถพูดได้ว่าบล็อกถูกขุดแล้ว และบล็อกใหม่จะถูกใส่เข้าไปในบล็อกเชน
ใน CrystalCoin
เราจะใช้อัลกอริธึม Proof-of-Work อย่างง่ายที่เราสร้างไว้ก่อนหน้านี้ ในการสร้างบล็อกใหม่ คอมพิวเตอร์ของผู้ขุดจะต้อง:
- ค้นหาบล็อกสุดท้ายใน
chain
- ค้นหาธุรกรรมที่รอดำเนินการ (
uncommitted_transactions
) - สร้างบล็อกใหม่โดยใช้
Block.next
- เพิ่มบล็อกที่ขุดลงในอาร์เรย์
chain
- ล้างอาร์เรย์
uncommitted_transactions
ดังนั้นเพื่อใช้งาน /mine
end-point เรามาเริ่มใช้ขั้นตอนข้างต้นใน 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
end-point:
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
โต้ตอบกับ Blockchain ของเรา
เราจะใช้ 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 #<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%
ตอนนี้เรามาแสดงรายการธุรกรรมที่รอดำเนินการ (เช่น ธุรกรรมที่ยังไม่ได้เพิ่มในบล็อก):
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
เราจะต้องใช้อัลกอริธึมฉันทามติหากเราต้องการโหนดมากกว่าหนึ่งโหนดในเครือข่ายของเรา
การลงทะเบียนโหนดใหม่
ในการใช้อัลกอริธึมฉันทามติ เราต้องมีวิธีให้โหนดทราบเกี่ยวกับโหนดใกล้เคียงในเครือข่าย แต่ละโหนดในเครือข่ายของเราควรเก็บรีจิสตรีของโหนดอื่นในเครือข่าย ดังนั้น เราจึงต้องการจุดปลายเพิ่มเติม:
- [POST]
/nodes/register
: เพื่อยอมรับรายการโหนดใหม่ในรูปแบบของ URL - [GET]
/nodes/resolve
: เพื่อนำ Consensus Algorithm ไปใช้งาน ซึ่งจะแก้ไขข้อขัดแย้งต่างๆ—เพื่อให้แน่ใจว่าโหนดมีสายโซ่ที่ถูกต้อง
เราจำเป็นต้องแก้ไขตัวสร้างบล็อคเชนของเราและจัดเตรียมวิธีการลงทะเบียนโหนด:
--- 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
เป็นวิธีที่วนซ้ำผ่านโหนดใกล้เคียงทั้งหมดของเรา ดาวน์โหลด chain ของพวกมันและยืนยันโดยใช้ valid_chain?
กระบวนการ. If a valid chain is found, whose length is greater than ours, we replace ours.
Now let's implement parse_chain()
and valid_chain?()
private methods:
module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end
For parse_chain()
we:
- Issue a
GET
HTTP request usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(CrystalCoin::Block).from_json(node_chain)
.
There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type)
functionality that gives us the following:
- A way to create an instance of that class from a JSON string by running
Class.from_json
. - A way to serialize an instance of that class into a JSON string by running
instance.to_json
. - Getters and setters for keys defined in that class.
In our case, we had to define JSON.mapping
in CrystalCoin::Block
object, and we removed property
usage in the class, like so:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Now for Blockchain#valid_chain?
, we iterate through all of the blocks, and for each we:
- Recalculate the hash for the block using
Block#recalculate_hash
and check that the hash of the block is correct:
module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
- Check each of the blocks linked correctly with their previous hashes.
- Check the block's hash is valid for the number of zeros (
difficulty
in our case00
).
And finally we implement /nodes/resolve
end-point:
get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end
It's done! You can find the final code on GitHub.
The structure of our project should look like this:
crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr
Let's Try it Out
- Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes:
http://localhost:3000
andhttp://localhost:3001
. - Register the second node address to the first node using:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
- Let's add a transaction to the second node:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
- Let's mine transactions into a block at the second node:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
- At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
- Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Let's check if the chain in the first node has updated:
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
ไชโย! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
ห่อ
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.