Crystalプログラミング言語での暗号通貨の作成

公開: 2022-03-11

この投稿は、内部を調査することによってブロックチェーンの重要な側面を理解するための私の試みです。 私は元のビットコインホワイトペーパーを読むことから始めましたが、ブロックチェーンを真に理解する唯一の方法は、新しい暗号通貨を最初から構築することであると感じました。

そのため、新しいCrystalプログラミング言語を使用して暗号通貨を作成することにし、 CrystalCoinと名付けました。 この記事では、アルゴリズムの選択、ハッシュの難易度、または同様のトピックについては説明しません。 代わりに、具体的な例の詳細に焦点を当てます。これにより、ブロックチェーンの長所と制限について、より深く実践的な理解が得られるはずです。

まだ読んでいない場合は、アルゴとハッシュの背景について、DemirSelmanovicの記事Cryptocurrencyfor Dummies:BitcoinandBeyondをご覧になることをお勧めします。

Crystalプログラミング言語を選んだ理由

より良いデモンストレーションのために、パフォーマンスを犠牲にすることなく、Rubyのような生産的な言語を使用したかったのです。 暗号通貨には多くの時間のかかる計算(つまり、マイニングハッシュ)があります。そのため、C++やJavaなどのコンパイル言語が「実際の」暗号通貨を構築するための言語として選択されています。 そうは言っても、開発を楽しくし、読みやすさを向上させるために、よりクリーンな構文の言語を使用したかったのです。 とにかくクリスタルの性能は良い傾向があります。

Crystalプログラミング言語の図

では、なぜ私は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では、両方を保存します。 ブロックチェーン全体の整合性を確保するために、各ブロックには自己識別ハッシュがあります。 ビットコインと同様に、各ブロックのハッシュは、ブロックの暗号化ハッシュ( 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メソッドを新しいキーワードに置き換えます。

Rubyキーワードクリスタルキーワード
attr_accessor 財産
attr_reader ゲッター
attr_writer セッター

Crystalで気付いたかもしれないもう1つのことは、コードを通じて特定の型についてコンパイラーにヒントを与えたいということです。 Crystalは型を推測しますが、あいまいな場合はいつでも、型を明示的に宣言することもできます。 そのため、 current_hashString型を追加しました。

次に、 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と比較して、 Int8Int16Int32Int64UInt8UInt16UInt32 、またはUInt64があることに気付いたかもしれません。 truefalseは、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をチェックしてください。

サーバーは、ブロックチェーンネットワークで単一のノードを形成します。 まず、 Kemalshard.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/newPOSTリクエストを送信し、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 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.