个性化阅读
专注于IT技术分析

使用Crystal编程语言创建加密货币

这篇文章是我尝试通过探索内部知识来理解区块链的关键方面的尝试。我从阅读原始的比特币白皮书开始, 但是我觉得真正了解区块链的唯一方法是从头开始构建新的加密货币。

这就是为什么我决定使用新的Crystal编程语言创建一种加密货币的原因, 并将其命名为CrystalCoin。本文不会讨论算法选择, 哈希难度或类似主题。相反, 重点将放在详细说明一个具体示例上, 该示例应提供对区块链的优势和局限性的更深入, 动手的理解。

如果你还没有读过它, 请访问Demir Selmanovic的文章”傻瓜的加密货币:比特币及其他”, 以获得有关算法和哈希的更多背景知识。

我为什么选择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哈希算法, 这是比特币中使用的算法。

使用SHA256, 即使输入少于256位或远大于256位, 我们也总是会产生64个十六进制字符(256位)的长度:

输入 阴霾的结果
非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本非常长的文本很长的文本很长的文本 cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a
托塔尔 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21
托塔尔。 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0

请注意最后一个示例, 只需添加一个即可。 (点)导致哈希值发生了巨大变化。

因此, 在区块链中, 通过将块数据传递到哈希算法中来构建链, 该哈希算法将生成哈希, 该哈希将链接到下一个块, 从而形成一系列与前一个块的哈希链接的块。

在Crystal中建立一个加密货币

现在开始创建Crystal项目并构建SHA256加密。

假设你已安装Crystal编程语言, 让我们使用Crystal的内置项目工具crystal init应用程序[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文件。

让我们添加openssl分片, 它是构建SHA256算法所必需的:

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

进入后, 回到终端并运行水晶部门。这样做将拉下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

设计我们的区块链

每个块都存储有一个时间戳和一个索引(可选)。在CrystalCoin中, 我们将同时存储两者。为了帮助确保整个区块链的完整性, 每个块都将具有一个自识别哈希。像比特币一样, 每个区块的哈希将是该区块的加密哈希(索引, 时间戳, 数据和前一个区块的哈希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方法:

Ruby关键字 水晶关键字
attr_accessor 属性
attr_reader 吸气剂
attr_writer 看跌期权

你可能在Crystal中注意到的另一件事是, 我们希望通过代码向编译器提示特定类型。 Crystal会推断类型, 但是只要你有歧义, 就可以显式声明类型。因此, 我们为current_hash添加了字符串类型。

现在, 让我们运行block.cr两次, 并注意由于时间戳不同, 同一数据将产生不同的哈希值:

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

现在我们有了块结构, 但是我们正在创建一个区块链。我们需要开始添加块以形成实际的链。如前所述, 每个块都需要来自上一个块的信息。但是, 区块链中的第一个块如何到达那里?好吧, 第一个区块, 即创世区块, 是一个特殊的区块(没有前任的区块)。在许多情况下, 它是手动添加的, 或者具有独特的逻辑允许添加。

我们将创建一个新函数, 该函数返回一个创世纪块。该块的索引为= 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类中添加工作量证明, 然后开始挖掘以发现现时数。我们将使用两个前导零” 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
图片替代文字

我将创建一个单独的模块来进行哈希处理并找到随机数, 以便我们保持代码的简洁和模块化。我称它为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。

你可能已经注意到, 与Ruby的Fixnum相比, Crystal的Integer类型有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, 我们发现现时和证明是具有匹配难度的哈希, 即两个前导零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个随机数(比较难度为” 00″的17个随机数), 直到找到匹配的幸运数字。

我们的区块链作为API

到现在为止还挺好。我们创建了简单的区块链, 并且相对容易做到。但是这里的问题是, CrystalCoin只能在一台计算机上运行(它不是分布式/分散式)。

从现在开始, 我们将开始将JSON数据用于CrystalCoin。数据将是交易, 因此每个区块的数据字段将是交易列表。

每笔交易将是一个JSON对象, 详细说明硬币的发送者, 硬币的接收者以及所转移的CrystalCoin的数量:

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

对我们的Block类进行了一些修改, 以支持新的事务格式(以前称为数据)。因此, 为避免混淆并保持一致性, 从现在开始, 我们将使用交易一词来引用示例应用程序中发布的数据。

我们将介绍一个新的简单类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的原因:

区块链将具有chain和uncommitted_transactions数组。该链将包括区块链中所有已开采的区块, uncommitted_transactions将具有尚未添加到区块链(仍未开采)的所有交易。初始化Blockchain后, 我们使用Block.first创建创世块并将其添加到链数组中, 然后添加一个空的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类中, 我们将开始使用事务而不是数据:

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

现在我们知道我们的交易会是什么样子, 我们需要一种将交易添加到我们的区块链网络中称为节点的计算机中的一种方法。为此, 我们将创建一个简单的HTTP服务器。

我们将创建四个端点:

  • [POST] / transactions / new:为区块创建新交易
  • [GET] / mine:告诉我们的服务器挖掘一个新块。
  • [GET] / chain:以JSON格式返回完整的区块链。
  • [GET] / pending:返回待处理的事务(uncommitted_transactions)。

我们将使用凯末尔网络框架。这是一个微框架, 可轻松将端点映射到Crystal函数。凯末尔(Kemal)深受Sinatra对Rubyists的影响, 并且工作方式非常相似。如果你正在寻找等效的Ruby on Rails, 请查看Amber。

我们的服务器将在我们的区块链网络中形成一个单一节点。首先, 将Kemal作为, 添加到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和未决的端点开始:

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)的过程称为区块挖掘。一旦找出满足我们约束条件的随机数, 就可以说已经开采出一块, 并将新块放入区块链中。

在CrystalCoin中, 我们将使用我们之前创建的简单的工作量证明算法。要创建新区块, 矿工的计算机将必须:

  • 找到链中的最后一个块。
  • 查找待处理的事务(uncommitted_transactions)。
  • 使用Block.next创建一个新块。
  • 将已开采的区块添加到链阵列。
  • 清理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.

看来我们成功地开采了第一个区块并将其添加到我们的链中。让我们仔细检查一下我们的连锁店, 看看它是否包含采矿区:

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

共识与权力下放

这很酷。我们获得了一个基本的区块链, 可以接受交易并允许我们挖掘新的区块。但是, 到目前为止, 我们已经实现的代码只能在单台计算机上运行, ​​而整个区块链的重点是应该将它们去中心化。但是, 如果它们分散, 我们如何确保它们都反映相同的链条?

这是共识问题。

如果我们要在网络中使用多个节点, 则必须实施共识算法。

注册新节点

为了实现共识算法, 我们需要一种让节点知道网络上相邻节点的方法。我们网络上的每个节点都应保留网络上其他节点的注册表。因此, 我们需要更多的端点:

  • [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数据结构来保存节点列表。这是一种廉价的方法, 可确保添加新节点是幂等的, 并且无论我们添加特定节点多少次, 它都会出现一次。

现在, 我们为共识添加一个新模块, 并实现第一个方法register_node(address):

require "uri"

module CrystalCoin
  module Consensus
    def register_node(address : String)
      uri = URI.parse(address)
      node_address = "#{uri.scheme}:://#{uri.host}"
      node_address = "#{node_address}:#{uri.port}" unless uri.port.nil?
      @nodes.add(node_address)
    rescue
      raise "Invalid URL"
    end
  end
end

register_node函数将解析节点的URL并将其格式化。

在这里, 我们创建/ nodes / register端点:

post "/nodes/register" do |env|
  nodes = env.params.json["nodes"].as(Array)

  raise "Empty array" if nodes.empty?

  nodes.each do |node|
    blockchain.register_node(node.to_s)
  end

  "New nodes have been added: #{blockchain.nodes}"
end

现在, 使用此实现, 我们可能会遇到多个节点的问题。几个节点的链的副本可以不同。在这种情况下, 我们需要就链的某些版本达成一致, 以维护整个系统的完整性。我们需要达成共识。

为解决此问题, 我们将规则设定为最长的有效链是要使用的链。使用此算法, 我们可以在网络中的节点之间达成共识。这种方法背后的原因是最长的链是对完成的工作量的最佳估计。

图片替代文字
module CrystalCoin
  module Consensus
    ...
    
    def resolve
      updated = false

      @nodes.each do |node|
        node_chain = parse_chain(node)
        return unless node_chain.size > @chain.size
        return unless valid_chain?(node_chain)
        @chain = node_chain
        updated = true
      rescue IO::Timeout
        puts "Timeout!"
      end

      updated
    end
    
  ...
  end
end

请记住, resolve是一种遍历我们所有相邻节点, 下载其链并使用valid_chain进行验证的方法?方法。如果找到有效链, 其长度大于我们的长度, 我们将替换我们的长度。

现在, 我们实现parse_chain()和valid_chain?()私有方法:

module CrystalCoin
  module Consensus
    ...
    
    private def parse_chain(node : String)
      node_url = URI.parse("#{node}/chain")
      node_chain = HTTP::Client.get(node_url)
      node_chain = JSON.parse(node_chain.body)["chain"].to_json

      Array(CrystalCoin::Block).from_json(node_chain)
    end

    private def valid_chain?(node_chain)
      previous_hash = "0"

      node_chain.each do |block|
        current_block_hash = block.current_hash
        block.recalculate_hash

        return false if current_block_hash != block.current_hash
        return false if previous_hash != block.previous_hash
        return false if current_block_hash[0..1] != "00"
        previous_hash = block.current_hash
      end

      return true
    end
  end
end

对于parse_chain(), 我们:

  • 使用HTTP :: Client.get到/ chain端点发出GET HTTP请求。
  • 使用JSON.parse解析/ chain JSON响应。
  • 从使用Array(CrystalCoin :: Block).from_json(node_chain)返回的JSON Blob中提取CrystalCoin :: Block对象数组。

在Crystal中, 解析JSON的方法不止一种。首选方法是使用Crystal的超方便JSON.mapping(key_name:Type)功能, 该功能可为我们提供以下功能:

  • 一种通过运行Class.from_json从JSON字符串创建该类的实例的方法。
  • 一种通过运行instance.to_json将此类的实例序列化为JSON字符串的方法。
  • 该类中定义的键的获取器和设置器。

在我们的例子中, 我们必须在CrystalCoin :: Block对象中定义JSON.mapping, 然后删除类中的属性用法, 如下所示:

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

现在, 对于Blockchain#valid_chain ?, 我们遍历所有块, 对于每个块, 我们:

  • 使用Block#recalculate_hash重新计算该块的哈希, 并检查该块的哈希是否正确:
module CrystalCoin
  class Block
    ...
    
    def recalculate_hash
      @nonce = proof_of_work
      @current_hash = calc_hash_with_nonce(@nonce)
    end
  end
end
  
  • 检查与它们之前的哈希正确链接的每个块。
  • 检查该区块的哈希值是否对零有效(在我们的情况下为00)。

最后, 我们实现/ nodes / resolve端点:

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

完成!你可以在GitHub上找到最终代码。

我们项目的结构应如下所示:

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

让我们尝试一下

  • 抓住另一台计算机, 并在网络上运行不同的节点。或使用同一台计算机上的不同端口启动进程。就我而言, 我在机器上的两个端口上创建了两个节点, 以具有两个节点:http:// localhost:3000和http:// localhost:3001。
  • 使用以下命令将第二个节点地址注册到第一个节点:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}'
New nodes have been added: Set{"http://0.0.0.0:3001"}%
  • 让我们向第二个节点添加一个事务:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}'
Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
  • 让我们将交易挖掘到第二个节点的一个区块中:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine
Block with index=1 is mined.%
  • 此时, 第一个节点只有一个块(创世块), 第二个节点有两个节点(创世块和开采的块):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain
{"chain":[{"index":0, "current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062", "nonce":157, "previous_hash":"0", "transactions":[], "timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain
{"chain":[{"index":0, "current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e", "nonce":147, "previous_hash":"0", "transactions":[], "timestamp":"2018-05-24T00:21:38+0300"}, {"index":1, "current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d", "nonce":92, "previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e", "transactions":[{"from":"eqbal", "to":"spiderman", "amount":100}], "timestamp":"2018-05-24T00:23:57+0300"}]}%
  • 我们的目标是更新第一个节点中的链, 以在第二个节点中包含新生成的块。因此, 让我们解析第一个节点:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve
Successfully updated the chain%

让我们检查第一个节点中的链是否已更新:

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

万岁!我们的Crystal语言示例的工作原理很吸引人, 我希望你发现这段冗长的教程非常清晰, 对不起。

包起来

该Crystal语言教程涵盖了公共区块链的基础知识。如果你遵循此步骤, 那么你将从头开始实现一个区块链, 并构建一个简单的应用程序, 允许用户在区块链上共享信息。

至此, 我们已经制作了一个大小合适的区块链。现在, 可以在多台计算机上启动CrystalCoin以创建网络, 并且可以开采真正的CrystalCoins。

我希望这能激发你创建新的东西, 或者至少使你更深入地了解Crystal编程。

注意:本教程中的代码尚不适合实际使用。请仅将此作为一般指南。

赞(0)
未经允许不得转载:srcmini » 使用Crystal编程语言创建加密货币

评论 抢沙发

评论前必须登录!