使用 Crystal 編程語言創建加密貨幣
已發表: 2022-03-11這篇文章是我嘗試通過探索內部結構來了解區塊鏈的關鍵方面。 我從閱讀原始比特幣白皮書開始,但我覺得真正理解區塊鏈的唯一方法是從頭開始構建一種新的加密貨幣。
這就是為什麼我決定使用新的 Crystal 編程語言創建一種加密貨幣,並將其命名為CrystalCoin 。 本文不會討論算法選擇、哈希難度或類似主題。 相反,重點將放在一個具體的例子上,它應該提供對區塊鏈的優勢和局限性的更深入的、動手的理解。
如果你還沒有讀過,關於算法和散列的更多背景信息,我建議你看看 Demir Selmanovic 的文章 Cryptocurrency for Dummies: Bitcoin and Beyond。
為什麼我選擇 Crystal 編程語言
為了更好地演示,我想在不影響性能的情況下使用像 Ruby 這樣的高效語言。 加密貨幣有許多耗時的計算(即挖掘和散列),這就是為什麼編譯語言(如 C++ 和 Java)是構建“真實”加密貨幣的首選語言。 話雖如此,我想使用一種語法更簡潔的語言,這樣我就可以保持開發的樂趣並提高可讀性。 無論如何,水晶性能往往很好。
那麼,為什麼我決定使用 Crystal 編程語言呢? Crystal 的語法深受 Ruby 的啟發,所以對我來說,它讀起來很自然,寫起來也很容易。 它還具有學習曲線較低的額外好處,尤其是對於有經驗的 Ruby 開發人員。
Crystal lang 團隊在他們的官方網站上是這樣描述的:
像 C 一樣快,像 Ruby 一樣流暢。
但是,與解釋型語言的 Ruby 或 JavaScript 不同,Crystal 是一種編譯型語言,因此速度更快,內存佔用更少。 在底層,它使用 LLVM 編譯為本機代碼。
Crystal 也是靜態類型的,這意味著編譯器將幫助您在編譯時捕獲類型錯誤。
我不打算解釋為什麼我認為 Crystal 語言很棒,因為它超出了本文的範圍,但如果你不覺得我的樂觀令人信服,請隨時查看這篇文章,以更好地了解 Crystal 的潛力。
注意:本文假設您已經對面向對象編程 (OOP) 有基本的了解。
區塊鏈
那麼,什麼是區塊鏈? 它是由數字指紋(也稱為加密哈希)鏈接和保護的塊列表(鏈)。
將其視為鍊錶數據結構的最簡單方法。 話雖如此,鍊錶只需要引用前一個元素; 一個塊必須有一個標識符,具體取決於前一個塊的標識符,這意味著您不能在不重新計算後面的每個塊的情況下替換一個塊。
現在,將區塊鏈視為一系列塊,其中一些數據與鏈相連,鍊是前一個塊的哈希。
整個區塊鏈將存在於每個想要與之交互的節點上,這意味著它被複製到網絡中的每個節點上。 沒有單個服務器託管它,但所有區塊鏈開發公司都使用它,這使得它去中心化。
是的,與傳統的集中式系統相比,這很奇怪。 每個節點都將擁有整個區塊鏈的副本(到 2017 年 12 月,比特幣區塊鏈中 > 149 Gb)。
散列和數字簽名
那麼,這個哈希函數是什麼? 將哈希視為一個函數,當我們給它一個文本/對象時,它會返回一個唯一的指紋。 即使是輸入對象的最小變化也會顯著改變指紋。
有不同的散列算法,在本文中,我們將使用SHA256
散列算法,這是Bitcoin
中使用的算法。
使用SHA256
,即使輸入小於 256 位或遠大於 256 位,我們總是會產生 64 個十六進製字符(256 位)的長度:
輸入 | 哈希結果 |
---|---|
非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本 非常長的文本很長的文字 很長的文字 | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
托普塔爾 | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
頂部。 | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
請注意最後一個示例,只需添加一個.
(點)導致哈希發生巨大變化。
因此,在區塊鏈中,鍊是通過將塊數據傳遞給哈希算法來構建的,該算法將生成一個哈希,該哈希鏈接到下一個塊,從而形成一系列與前一個塊的哈希鏈接的塊。
在 Crystal 中構建加密貨幣
現在讓我們開始創建我們的 Crystal 項目並構建我們的SHA256
加密。
假設您安裝了 Crystal 編程語言,讓我們使用 Crystal 的內置項目工具crystal init app [name]
創建CrystalCoin
代碼庫的骨架:
% 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 存儲庫、許可證和自述文件。 它還帶有用於測試的存根,以及用於描述項目和管理依賴項(也稱為分片)的shard.yml
文件。
讓我們添加構建SHA256
算法所需的openssl
分片:
# 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
中,我們將存儲兩者。 為了幫助確保整個區塊鏈的完整性,每個塊都將有一個自我識別的哈希值。 與比特幣一樣,每個區塊的哈希將是區塊的加密哈希( 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 的attr_accessor
、 attr_getter
和attr_setter
方法替換為新的關鍵字:
紅寶石關鍵字 | 水晶關鍵詞 |
---|---|
attr_accessor | 財產 |
attr_reader | 吸氣劑 |
attr_writer | 二傳手 |
您可能在 Crystal 中註意到的另一件事是,我們希望通過代碼向編譯器提示特定類型。 Crystal 推斷類型,但只要您有歧義,您也可以顯式聲明類型。 這就是我們為current_hash
添加String
類型的原因。
現在讓我們運行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
對其進行測試:
#<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)">
工作證明
工作量證明算法 (PoW) 是在區塊鏈上創建或挖掘新塊的方式。 PoW 的目標是發現一個解決問題的數字。 該數字必須很難找到,但網絡上的任何人都可以通過計算輕鬆驗證。 這是工作量證明背後的核心思想。
讓我們用一個例子來演示,以確保一切都清楚。 我們假設某個整數 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
)。 它只是一個數字,每次哈希無效時我們都會增加一個數字。 我們得到我們的數據(日期、消息、先前的散列、索引)和 1 的隨機數。如果我們得到的散列無效,我們嘗試使用 2 的隨機數。我們增加隨機數直到我們得到一個有效的散列.
在比特幣中,工作量證明算法稱為 Hashcash。 讓我們在 Block 類中添加工作量證明並開始挖掘以找到 nonce。 我們將使用兩個前導零“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 要求每個私有方法都以 private 關鍵字為前綴,這可能會使來自 Ruby 的混淆。
您可能已經註意到 Crystal 的 Integer 類型與 Ruby 的Fixnum
相比有Int8
、 Int16
、 Int32
、 Int64
、 UInt8
、 UInt16
、 UInt32
或UInt64
。 true
和false
是Bool
類中的值,而不是 Ruby 中TrueClass
或FalseClass
類中的值。
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 是匹配難度的哈希,即兩個前導零00
。
注意我們創建的第一個塊,我們嘗試了 17 個隨機數,直到找到匹配的幸運數字:
堵塞 | 循環/哈希計算次數 |
---|---|
#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 個隨機數(比較 17 個隨機數,難度為“00”),直到找到匹配的幸運數字。
我們的區塊鏈作為 API
到目前為止,一切都很好。 我們創建了簡單的區塊鏈,並且相對容易做到。 但這裡的問題是CrystalCoin
只能在一台機器上運行(它不是分佈式/去中心化的)。
從現在開始,我們將開始為CrystalCoin
使用 JSON 數據。 數據將是交易,因此每個區塊的數據字段將是交易列表。
每筆交易都將是一個 JSON 對象,詳細說明硬幣的sender
、硬幣的receiver
以及正在轉移的 CrystalCoin amount
:
{ "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 服務器。
我們將創建四個端點:
- [POST]
/transactions/new
: 為區塊創建新交易 - [GET]
/mine
:告訴我們的服務器挖掘一個新塊。 - [GET]
/chain
:以JSON
格式返回完整的區塊鏈。 - [GET]
/pending
:返回待處理的事務(uncommitted_transactions
)。
我們將使用 Kemal Web 框架。 這是一個微型框架,可以輕鬆地將端點映射到 Crystal 函數。 Kemal 深受 Sinatra for Rubyists 的影響,並且以非常相似的方式工作。 如果您正在尋找 Ruby on Rails 等價物,請查看 Amber。
我們的服務器將在我們的區塊鍊網絡中形成單個節點。 讓我們首先將Kemal
作為 a 添加到shard.yml
文件中並安裝依賴項:
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
對象並使用Blockchain#add_transaction
將交易添加到uncommitted_transactions
數組中。
目前,事務最初存儲在uncommitted_transactions
池中。 將未經確認的交易放入一個區塊併計算工作證明(PoW)的過程稱為區塊挖掘。 一旦找到滿足我們約束的nonce
,我們就可以說已經開采了一個塊,並且將新塊放入了區塊鏈。
在CrystalCoin
中,我們將使用我們之前創建的簡單工作量證明算法。 要創建一個新區塊,礦工的計算機必須:
- 找到
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
然後讓我們通過向http://localhost:3000/transactions/new
發出POST
請求來創建兩個新事務,其中包含我們的事務結構:
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
中。
現在讓我們通過向http://0.0.0.0:3000/mine
發出GET
請求來挖掘這兩個交易:
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
:實現我們的共識算法,解決任何衝突——確保節點具有正確的鏈。
我們需要修改區塊鏈的構造函數並提供註冊節點的方法:
--- 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)
請注意,我們使用了具有String
類型的Set
數據結構來保存節點列表。 這是確保添加新節點是冪等的一種廉價方法,並且無論我們添加多少次特定節點,它都只出現一次。
現在讓我們向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?
驗證它們的方法。 方法。 如果找到一個有效鏈,其長度大於我們的,我們替換我們的。
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
完成! 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.