使用 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中,我们将存储两者。 为了帮助确保整个区块链的完整性,每个块都将有一个自我识别的哈希值。 与比特币一样,每个区块的哈希将是区块的加密哈希( indextimestampdata和前一个区块的哈希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_accessorattr_getterattr_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相比有Int8Int16Int32Int64UInt8UInt16UInt32UInt64truefalseBool类中的值,而不是 Ruby 中TrueClassFalseClass类中的值。

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将具有chainuncommitted_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/newpending的端点开始:

 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?验证它们的方法。 方法。 如果找到一个有效链,其长度大于我们的,我们替换我们的。

现在让我们实现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

For parse_chain() we:

  • Issue a GET HTTP request using HTTP::Client.get to /chain end-point.
  • Parse the /chain JSON response using JSON.parse .
  • Extract an array of CrystalCoin::Block objects from the JSON blob that was returned using Array(CrystalCoin::Block).from_json(node_chain) .

There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type) functionality that gives us the following:

  • A way to create an instance of that class from a JSON string by running Class.from_json .
  • A way to serialize an instance of that class into a JSON string by running instance.to_json .
  • Getters and setters for keys defined in that class.

In our case, we had to define JSON.mapping in CrystalCoin::Block object, and we removed property usage in the class, like so:

 module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end

Now for Blockchain#valid_chain? , we iterate through all of the blocks, and for each we:

  • Recalculate the hash for the block using Block#recalculate_hash and check that the hash of the block is correct:
 module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
  • Check each of the blocks linked correctly with their previous hashes.
  • Check the block's hash is valid for the number of zeros ( difficulty in our case 00 ).

And finally we implement /nodes/resolve end-point:

 get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end

完成! You can find the final code on GitHub.

The structure of our project should look like this:

 crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr

Let's Try it Out

  • Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes: http://localhost:3000 and http://localhost:3001 .
  • Register the second node address to the first node using:
 crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
  • Let's add a transaction to the second node:
 crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
  • Let's mine transactions into a block at the second node:
 crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
  • At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
 crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
 crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
  • Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
 crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%

Let's check if the chain in the first node has updated:

 crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}% 

图片替代文字

万岁! 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.