Crystalプログラミング言語での暗号通貨の作成
公開: 2022-03-11この投稿は、内部を調査することによってブロックチェーンの重要な側面を理解するための私の試みです。 私は元のビットコインホワイトペーパーを読むことから始めましたが、ブロックチェーンを真に理解する唯一の方法は、新しい暗号通貨を最初から構築することであると感じました。
そのため、新しいCrystalプログラミング言語を使用して暗号通貨を作成することにし、 CrystalCoinと名付けました。 この記事では、アルゴリズムの選択、ハッシュの難易度、または同様のトピックについては説明しません。 代わりに、具体的な例の詳細に焦点を当てます。これにより、ブロックチェーンの長所と制限について、より深く実践的な理解が得られるはずです。
まだ読んでいない場合は、アルゴとハッシュの背景について、DemirSelmanovicの記事Cryptocurrencyfor Dummies:BitcoinandBeyondをご覧になることをお勧めします。
Crystalプログラミング言語を選んだ理由
より良いデモンストレーションのために、パフォーマンスを犠牲にすることなく、Rubyのような生産的な言語を使用したかったのです。 暗号通貨には多くの時間のかかる計算(つまり、マイニングとハッシュ)があります。そのため、C++やJavaなどのコンパイル言語が「実際の」暗号通貨を構築するための言語として選択されています。 そうは言っても、開発を楽しくし、読みやすさを向上させるために、よりクリーンな構文の言語を使用したかったのです。 とにかくクリスタルの性能は良い傾向があります。
では、なぜ私はCrystalプログラミング言語を使用することにしたのですか? Crystalの構文はRubyの構文に大きく影響を受けているため、私にとっては、読みやすく、書きやすいと感じています。 特に経験豊富なRuby開発者にとっては、学習曲線が低くなるという追加の利点があります。
これは、Crystallangチームが公式Webサイトに掲載する方法です。
Cのように速く、Rubyのように滑らかです。
ただし、インタプリタ言語であるRubyやJavaScriptとは異なり、Crystalはコンパイルされた言語であるため、はるかに高速になり、メモリフットプリントが少なくなります。 内部的には、ネイティブコードへのコンパイルにLLVMを使用します。
Crystalは静的に型付けされています。つまり、コンパイラーはコンパイル時に型エラーをキャッチするのに役立ちます。
この記事の範囲を超えているため、Crystal言語が素晴らしいと考える理由については説明しませんが、私の楽観的な見方がわからない場合は、この記事をチェックして、Crystalの可能性の概要を確認してください。
注:この記事は、オブジェクト指向プログラミング(OOP)の基本をすでに理解していることを前提としています。
ブロックチェーン
では、ブロックチェーンとは何ですか? これは、デジタル指紋(暗号化ハッシュとも呼ばれます)によってリンクおよび保護されたブロックのリスト(チェーン)です。
それを考える最も簡単な方法は、リンクリストのデータ構造として考えることです。 そうは言っても、リンクリストは前の要素への参照を持っている必要があるだけです。 ブロックには、前のブロックの識別子に応じた識別子が必要です。つまり、後続のすべてのブロックを再計算せずにブロックを置き換えることはできません。
今のところ、ブロックチェーンは、いくつかのデータがチェーンにリンクされた一連のブロックと考えてください。チェーンは前のブロックのハッシュです。
ブロックチェーン全体は、ブロックチェーンと対話する各ノードに存在します。つまり、ネットワーク内の各ノードにコピーされます。 単一のサーバーがそれをホストすることはありませんが、すべてのブロックチェーン開発会社がそれを使用しているため、分散化されています。
はい、これは従来の集中型システムと比較して奇妙です。 各ノードには、ブロックチェーン全体のコピーがあります(2017年12月までにビットコインブロックチェーンで> 149 Gb)。
ハッシュとデジタル署名
では、このハッシュ関数とは何ですか? ハッシュを関数と考えてください。これは、テキスト/オブジェクトを指定したときに一意のフィンガープリントを返します。 入力オブジェクトのわずかな変更でさえ、フィンガープリントを劇的に変更します。
さまざまなハッシュアルゴリズムがありますが、この記事では、 Bitcoin
で使用されているSHA256
ハッシュアルゴリズムを使用します。
SHA256
を使用すると、入力が256ビット未満または256ビットよりはるかに大きい場合でも、常に64の16進文字(256ビット)の長さになります。
入力 | ハッシュされた結果 |
---|---|
非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト非常に長いテキスト | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
トパル | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
トプタル。 | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
最後の例では、を追加するだけであることに注意してください.
(ドット)は、ハッシュに劇的な変化をもたらしました。
したがって、ブロックチェーンでは、次のブロックにリンクされるハッシュを生成するハッシュアルゴリズムにブロックデータを渡すことによってチェーンが構築され、以降、前のブロックのハッシュにリンクされた一連のブロックが形成されます。
クリスタルで暗号通貨を構築する
それでは、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リポジトリ、ライセンス、およびreadmeファイルを使用して、プロジェクトの基本構造を作成します。 また、テスト用のスタブと、プロジェクトを記述して依存関係を管理するための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
メソッドを新しいキーワードに置き換えます。
Rubyキーワード | クリスタルキーワード |
---|---|
attr_accessor | 財産 |
attr_reader | ゲッター |
attr_writer | セッター |
Crystalで気付いたかもしれないもう1つのことは、コードを通じて特定の型についてコンパイラーにヒントを与えたいということです。 Crystalは型を推測しますが、あいまいな場合はいつでも、型を明示的に宣言することもできます。 そのため、 current_hash
にString
型を追加しました。
次に、 block.cr
を2回実行して、 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
をデモンストレーションするために5つの新しいブロックを作成します。
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アルゴリズムを気にするのですか? ブロックごとに1つのハッシュを作成しないのはなぜですか? ハッシュは有効である必要があります。 この場合、ハッシュの最初の2文字が00
の場合、ハッシュは有効になります。 ハッシュが00......
で始まる場合、それは有効であると見なされます。 これは難易度と呼ばれます。 難易度が高いほど、有効なハッシュを取得するのに時間がかかります。
ただし、ハッシュが最初に有効でない場合は、使用するデータで何かを変更する必要があります。 同じデータを何度も使用すると、同じハッシュが何度も取得され、ハッシュが有効になることはありません。 ハッシュではnonce
と呼ばれるものを使用します(前の例ではy
です)。 これは、ハッシュが無効になるたびにインクリメントする単純な数値です。 データ(日付、メッセージ、前のハッシュ、インデックス)と1のナンスを取得します。これらで取得したハッシュが有効でない場合は、2のノンスで試行します。有効なハッシュを取得するまでナンスをインクリメントします。 。
ビットコインでは、プルーフオブワークアルゴリズムはハッシュキャッシュと呼ばれます。 ブロッククラスにプルーフオブワークを追加し、ナンスを見つけるためにマイニングを開始しましょう。 2つの先行ゼロ「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からの混乱を招く可能性があります。
クリスタルの整数型には、RubyのFixnum
と比較して、 Int8
、 Int16
、 Int32
、 Int64
、 UInt8
、 UInt16
、 UInt32
、またはUInt64
があることに気付いたかもしれません。 true
とfalse
は、RubyのクラスTrueClass
またはFalseClass
の値ではなく、 Bool
クラスの値です。
Crystalには、コア言語機能としてオプションの名前付きメソッド引数があり、引数を処理するための特別なコードを記述する必要はありません。これは非常に優れています。 Block#initialize(index = 0, data = "data", previous_hash = "hash")
をチェックしてから、 Block.new(data: data, previous_hash: "0")
のようなもので呼び出します。
CrystalとRubyプログラミング言語の違いの詳細なリストについては、CrystalforRubyistsをチェックしてください。
それでは、以下を使用して5つのトランザクションを作成してみましょう。
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
と証明が一致する難易度のハッシュ、つまり2つの先行ゼロ00
であることがわかりました。
作成した最初のブロックで、一致するラッキーナンバーが見つかるまで17のナンスを試しました。
ブロック | ループ/ハッシュ計算の数 |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
それでは、4つの先行ゼロの難易度を試してみましょう( difficulty="0000"
):
ブロック | ループ/ハッシュ計算の数 |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
最初のブロックでは、一致するラッキーナンバーが見つかるまで、26762ノンス(難易度「00」の17ノンスと比較)を試しました。
APIとしてのブロックチェーン
ここまでは順調ですね。 シンプルなブロックチェーンを作成しましたが、比較的簡単に作成できました。 ただし、ここでの問題は、 CrystalCoin
が1台のマシンでしか実行できないことです(分散/分散化されていません)。
これから、 CrystalCoin
のJSONデータの使用を開始します。 データはトランザクションになるため、各ブロックのデータフィールドはトランザクションのリストになります。
各トランザクションは、コインのsender
、コインのreceiver
者、および転送されるCrystalCoinのamount
を詳細に示すJSONオブジェクトになります。
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
新しいtransaction
形式(以前はdata
と呼ばれていました)をサポートするためのBlock
クラスへのいくつかの変更。 したがって、混乱を避けて一貫性を維持するために、今後は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
トランザクションはブロックにパックされるため、ブロックには1つまたは多数のトランザクションを含めることができます。 トランザクションを含むブロックは頻繁に生成され、ブロックチェーンに追加されます。

ブロックチェーンは、ブロックのコレクションであると想定されています。 すべてのブロックをCrystalリストに保存できるため、新しいクラスのBlockchain
を導入します。
Blockchain
チェーンには、 chain
配列とuncommitted_transactions
配列があります。 chain
には、ブロックチェーン内のすべてのマイニングされたブロックが含まれ、 uncommitted_transactions
には、ブロックチェーンに追加されていない(まだマイニングされていない)すべてのトランザクションが含まれます。 Blockchain
を初期化したら、 Block.first
を使用してジェネシスブロックを作成し、それをchain
配列に追加し、空のuncommitted_transactions
配列を追加します。
uncommitted_transactions
配列にトランザクションを追加するBlockchain#add_transaction
メソッドを作成します。
新しい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
クラスでは、 data
の代わりにtransactions
の使用を開始します。
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
と呼ばれるブロックチェーンネットワーク内のコンピューターの1つにトランザクションを追加する方法が必要です。 そのために、単純なHTTPサーバーを作成します。
4つのエンドポイントを作成します。
- [POST]
/transactions/new
:ブロックへの新しいトランザクションを作成します - [GET]
/mine
:新しいブロックをマイニングするようにサーバーに指示します。 - [GET]
/chain
:完全なブロックチェーンをJSON
形式で返します。 - [GET]
/pending
:保留中のトランザクション(uncommitted_transactions
)を返します。
KemalWebフレームワークを使用します。 これは、エンドポイントをCrystal関数に簡単にマッピングできるようにするマイクロフレームワークです。 KemalはRubyistsのSinatraの影響を強く受けており、非常によく似た方法で機能します。 同等のRubyonRailsを探している場合は、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
および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
ブロックチェーンとの相互作用
ネットワークを介してAPIとやり取りするためにcURL
を使用します。
まず、サーバーを起動しましょう。
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
リクエストを送信し、2つの新しいトランザクションを作成しましょう。
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 } ] }
ご覧のとおり、前に作成した2つのトランザクションがuncommitted_transactions
に追加されています。
次に、http: http://0.0.0.0:3000/mine
:3000 / mineにGET
リクエストを送信して、2つのトランザクションをマイニングしましょう。
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" } ] }
コンセンサスと地方分権
これはカッコいい。 トランザクションを受け入れ、新しいブロックをマイニングできる基本的なブロックチェーンを取得しました。 ただし、これまでに実装したコードは1台のコンピューターで実行することを目的としていますが、ブロックチェーンの要点は分散化する必要があるということです。 しかし、それらが分散化されている場合、それらがすべて同じチェーンを反映していることをどのように保証しますか?
これが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
データ構造を使用していることに注意してください。 これは、新しいノードの追加がべき等であり、特定のノードを何度追加しても、それが1回だけ表示されることを保証する安価な方法です。
次に、新しいモジュールを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 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.