20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Clojure でブロックチェーンを実装してみる

Last updated at Posted at 2018-05-08

Python でブロックチェーンを実装する記事を Qiita 上で発見しました。

ブロックチェーンを作ることで学ぶ 〜ブロックチェーンがどのように動いているのか学ぶ最速の方法は作ってみることだ〜

Swift で実装してみた記事 や Ruby で実装してみた記事があったので、右へ倣えということで Clojure で実装してみたいと思います。

基本的には元記事の章立てに合わせて実装していきます。また、引用記法の箇所は、元記事より引用した文となります。

最終的なソースはこちら

プロジェクト作成

後半に API としてブロックチェーンを利用するため、プロジェクトは compojure のテンプレートを使い作成しておきます。

$ lein new compojure clj-blockchain

作成したプロジェクトのディレクトリ構成は以下のようになっています。

$ tree

.
├── README.md
├── project.clj
├── resources
│   └── public
├── src
│   └── clj_blockchain
│       └── handler.clj
└── test
    └── clj_blockchain
        └── handler_test.clj

project.clj に lein-ring プラグインが設定されているため

$ lein ring server

でサーバを起動することができます。

ステップ1: ブロックチェーンを作る

src/clj_blockchain/core.clj ファイルを作成し、ブロックチェーンの実装をこのファイルに記述していきます。

ブロックチェーンを書く

元記事に沿って、主要関数の雛形を作成します。

core.clj
(ns clj-blockchain.core
  (:refer-clojure :exclude [hash]))

(defonce chain (atom []))
(defonce current-transactions (atom []))

(defn last-block
  "チェーンの最後のブロックを返す"
  [])

(defn hash
  "ブロックをハッシュ化する"
  [])

(defn create-transaction
  "新しいトランザクションをリストに加える"
  [])

(defn create-block
  "新しいブロックを作り、チェーンに加える"
  [])

(defn init
  "チェーン、トランザクションの初期化を行う"
  []
  (reset! chain [])
  (reset! current-transactions []))

元記事でクラス管理しているブロックチェーン、トランザクションの状態は atom で管理させます。

ブロックとはどのようなものなのか

それぞれのブロックは、インデックス、タイムスタンプ(UNIXタイム)、トランザクションのリスト、プルーフ(詳細は後ほど)そしてそれまでの全てのブロックから生成されるハッシュを持っている。

ブロックは以下のようなマップで表現されるデータとなります。

{:index 3,
 :timestamp 15060571251525039421764
 :transactions
 [{:sender "8527147fe1f5426f9dd545de4b27ee00"
   :recipient "a77f5cdfa2934df3954a5c7c7da5df1f"
   :amount 5}]
 :proof 324984774000
 :previous-hash "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"})

:previous-hash が1つ前のブロックから作られたハッシュです。ブロックがこの値を保持することで、ブロックが保持するデータの改ざんを検出できるようにしています。

必須でありませんが、上記のブロックに対するスペックを記述しておきます。

src/clj_blockchain/spec.clj
(ns clj-blockchain.spec
  (:require [clojure.spec.alpha :as s]))

;;; transaction
(s/def ::sender string?)
(s/def ::recipient string?)
(s/def ::amount pos-int?)
(s/def ::transaction (s/keys :req-un [::sender ::recipient ::amount]))

;;; block
(s/def ::index pos-int?)
(s/def ::timestamp pos-int?)
(s/def ::transactions (s/coll-of ::transaction))
(s/def ::proof nat-int?)
(s/def ::previous-hash string?)
(s/def ::block (s/keys :req-un [::index
                                ::timestamp
                                ::transactions
                                ::proof
                                ::previous-hash]))

トランザクションをブロックに加える

私たちにはトランザクションをブロックに加える方法が必要だ。new_transaction()メソッドがそれを司っており、非常に簡単だ。

同様の処理を行う create-transaction 関数を実装します。

core.clj
(defn count-chain
  []
  (count @chain))
  
(defn create-transaction
  "新しいトランザクションをリストに加える"
  [transaction]
  (swap! current-transactions #(conj % transaction))
  (inc (count-chain)))

current-transaction に引数で与えられた transaction を追加します。また、戻り値として追加したトランザクションが含まれるブロックを表す index 番号を返しますが、この時点ではまだ対象のブロックは作成されていません。

関数のスペックも記載しておきます。

spec.clj
(s/fdef core/create-transaction
  :args (s/cat :transaction ::transaction)
  :ret  ::index)

新しいブロックを作る

我々のBlockchainがインスタンス化されるとき、私たちはジェネシスブロック -先祖を持たないブロック- とともにシードする必要がある。それと同時に、ジェネシスブロックにプルーフ -マイニング(またはプルーフ・オブ・ワーク)の結果- も加える必要がある。マイニングについては後で取り上げる。

ブロックを作成する create-block 関数とその関数から利用する last-blockhash 関数を実装します。

core.clj
(defn last-block
  "チェーンの最後のブロックを返す"
  []
  (last @chain))

(defn hash
  "ブロックをハッシュ化する"
  [block]
  (-> (into (sorted-map) block)
      str
      digest/sha-256))

(defn create-block
  "新しいブロックを作り、チェーンに加える"
  ([proof]
   (create-block proof (hash (last-block))))
  ([proof previous-hash]
   (let [block {:index         (inc (count-chain))
                :timestamp     (System/currentTimeMillis)
                :transactions  @current-transactions
                :proof         proof
                :previous-hash previous-hash}]
     ;; 現在のトランザクションリストをリセット
     (reset! current-transactions [])

     (swap! chain #(conj % block))
     block)))

create-block 関数では後に説明するプルーフ・オブ・ワークアルゴリズムから得られる proof を引数として渡せるようにしておきます。また、ジェネシスブロック作成時用に previous-hash を直接与えられるようにしておきます。ジェネシスブロックの作成以外は1つ前のブロックが存在するのでそのブロックを元にハッシュを生成します。

hash 関数では clj-digest ライブラリの SHA-256 を利用しています。

[digest "1.4.8"]  ;; <-- project.clj の :dependencies に追加

ハッシュに一貫性を持たせるため、sorted-map でマップをソートしてから SHA-256 でハッシュを生成するようにします。

最後に作成した create-block 関数を使い、init 関数内にジェネシスブロックを作成する処理を追加しておきます。

core.clj
(defn init
  "チェーン、トランザクションの初期化を行う"
  []
  (reset! chain [])
  (reset! current-transactions [])

  ;; 追加
  ;; ジェネシスブロックを作る
  (let [proof 100
        previsous-hash "1"]
    (create-block proof previsous-hash)))

関数のスペックは以下

spec.clj
(s/fdef core/hash
  :args (s/cat :block ::block)
  :ret  string?)
  
(s/fdef core/create-block
  :args (s/cat :proof         ::proof
                :previous-hash (s/? ::previous-hash))
  :ret  ::block)

プルーフ・オブ・ワークを理解する

プルーフ・オブ・ワークアルゴリズム (PoW) とは、ブロックチェーン上でどのように新しいブロックが作られるか、または採掘されるかということを表している。PoWのゴールは、問題を解く番号を発見することだ。その番号はネットワーク上の誰からも見つけるのは難しく、確認するのは簡単 -コンピュータ的に言えば- なものでなければならない。これがプルーフ・オブ・ワークのコアとなるアイデアだ。

理解するために簡単な例を見てみよう。

下記は、 x = 5 に対して、x * y のハッシュ値の末尾が 0 で終わる y の値を求めています。

(let [x 5]
  (loop [y 0]
    (if (= \0 (-> (* x y) str digest/sha-256 last))
      y
      (recur (inc y)))))

実行すると 21 が返ってきます。

(-> (* 5 21) str digest/sha-256)
;; => "1253e9373e781b7500266caa55150e08e210bc8cd8cc70d89985e3600155e860" <-- 末尾が `0` であることを確認

ビットコインでは、プルーフ・オブ・ワークのアルゴリズムはハッシュキャッシュ  (Hashcash)と呼ばれている。そしてそれはこの基本的な例とそこまで違うものではない。ハッシュキャッシュは、採掘者が競い合って新しいブロックを作るために問題を解く、というものだ。一般的に、難易度は探す文字の数によって決まる。採掘者はその解に対して、報酬としてトランザクションの中でコインを受け取る。

ネットワークは簡単に採掘者の解が正しいかを確認することが出来る。

基本的なプルーフ・オブ・ワークを実装する

私たちのブロックチェーンのために似たアルゴリズムを実装しよう。ルールは上の例と似ている。前のブロックの解とともにハッシュを作ったときに、最初に4つの0が出てくるような番号pを探そう。

y = 21 を求める処理と同様の方法で、プルーフ・オブ・ワークの実装を行います。

core.clj
(defn valid-proof?
  "プルーフが正しいかを確認する: hash(last_proof, proof)の最初の4つが0となっているか?"
  [last-proof proof]
  (-> (str last-proof proof)
      digest/sha-256
      (subs 0 4)
      (= "0000")))

(defn proof-of-work
  "シンプルなプルーフ・オブ・ワークのアルゴリズム:
     - hash(pp') の最初の4つが0となるような p' を探す
     - p は1つ前のブロックのプルーフ、 p' は新しいブロックのプルーフ"
  [last-proof]
  (loop [proof 0]
    (if (valid-proof? last-proof proof)
      proof
      (recur (inc proof)))))

0 の数を増減させることで、アルゴリズムの難易度を調整することができます。

関数のスペックは以下

spec.clj
(s/def ::proof nat-int?)

(s/fdef core/valid-proof?
  :args (s/cat :last-proof ::proof
               :proof      ::proof)
  :ret  boolean?)


(s/fdef core/proof-of-work
  :args (s/cat :last-proof ::proof)
  :ret  ::proof)

ステップ2: APIとしての私たちのブロックチェーン

ここではPython Flaskフレームワークを使う。Flaskはマイクロフレームワークであり、簡単にエンドポイントをPythonのファンクションに対応させることが出来る。そして、私たちのブロックチェーンがHTTPリクエストを使ってWebで通信することが出来るようになる。

ここでは3つのメソッドを作る:

  • ブロックへの新しいトランザクションを作るための/transactions/new
  • サーバーに対して新しいブロックを採掘するように伝える/mine
  • フルブロックチェーンを返す/chain

Flask の代わりに Ring & Compojure で API の実装を行います。プロジェクト作成時に Compojure のテンプレートを利用したため、API に関する処理が記述された src/clj_blockchain/handler.clj が既に作成されています。

handler.clj
(ns clj-blockchain.handler
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (route/not-found "Not Found"))

(def app
  (wrap-defaults app-routes site-defaults))

このファイルを編集し、/transactions/new, /mine, /chain のエンドポイントを作成します。

handler.clj
(ns clj-blockchain.handler
  (:require [clj-blockchain.core :as core]
            [clojure.string :as cstr]
            [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
            [ring.middleware.json :refer [wrap-json-response wrap-json-params]]
            [ring.util.response :refer [response status]]))

;; このノードのグローバルにユニークなアドレスを作る
(defonce node-identifire (-> (str (java.util.UUID/randomUUID))
                             (cstr/replace "-" "")))

(defn- chain
  [req]
  (response {:chain  (core/get-chain)
             :length (core/count-chain)}))

(defn- mine
  [req]
  "新しいブロックを採掘します")

(defn- create-transaction
  [req]
  "新しいトランザクションを追加します")

(defroutes app-routes
  (GET  "/chain"             req (chain req))
  (GET  "/mine"              req (mine req))
  (POST "/transactions/new"  req (create-transaction req))
  
  (route/not-found "Not Found"))

(def app (-> app-routes
             wrap-json-response
             (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false))
             wrap-json-params))

(defn init
  []
  (core/init))
core.clj
(defn get-chain
  []
  @chain)

/chain に関しては、ブロックチェーンを返すだけなので中身の処理も実装しました。リクエストに対して、チェーン全体とその長さを含めた JSON を返しています。JSON でリクエスト&レスポンスを行うため、Ring の wrap-json-response, wrap-json-params ミドルウェアを利用しています。また、サーバ起動時にブロックチェーンの初期化を行う init 関数を追加しました。

これらを利用するための記述を project.clj に追加しておきます。

project.clj
(defproject clj-blockchain "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :min-lein-version "2.0.0"
  :dependencies [[org.clojure/clojure "1.9.0"]
                 [compojure "1.5.1"]
                 [ring/ring-defaults "0.2.1"]
                 [digest "1.4.8"]
                 [ring/ring-json "0.4.0"]] ;; <-- 追加
  :plugins [[lein-ring "0.9.7"]]
  :ring {:handler clj-blockchain.handler/app
         :init    clj-blockchain.handler/init} ;; <-- 追加
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring/ring-mock "0.3.0"]]}})

トランザクションエンドポイント

これらトランザクションのリクエストの例だ。このようなものをユーザーはサーバーに送る:

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

/transactions/new への POST でトランザクションを登録する処理を実装します。

handler.clj
(defn- create-transaction
  [{:keys [params] :as req}]
  ;; POSTされたデータに必要なデータがあるかを確認
  (let [{:keys [sender recipient amount]} params]
    (if (some nil? [sender recipient amount])
      (-> (response "Missing values")
          (status 404))
      (let [transaction {:sender sender
                         :recipient recipient
                         :amount (Integer. amount)}
            ;; 新しいトランザクションを作る
            index (core/create-transaction transaction)]
        (response {:message (format "トランザクションはブロック%sに追加されました" index)})))))

既に core.cljcreate-transaction 関数を作成してあるので、受け取った値を transaction のマップに詰めて渡すだけです。index はそのトランザクションが追加されるブロック(この時点では作成されていない)の番号です。

採掘のエンドポイント

採掘のエンドポイントは魔法が起きるところだが、簡単だ。3つのことを行う必要がある。

  1. プルーフ・オブ・ワークを計算する
  2. 1コインを採掘者に与えるトランザクションを加えることで、採掘者(この場合は我々)に利益を与える
  3. チェーンに新しいブロックを加えることで、新しいブロックを採掘する

こちらも既に必要な処理は core.clj に記載されているため、それらを呼び出していきます。

handler.clj
(defn- mine
  [req]
  (let [;; 次のプルーフを見つけるためプルーフ・オブ・ワークアルゴリズムを使用する
        last-block (core/last-block)
        last-proof (:proof last-block)
        proof (core/proof-of-work last-proof)

        ;; プルーフを見つけたことに対する報酬を得る
        ;; 送信者は、採掘者が新しいコインを採掘したことを表すために"0"とする
        transaction {:sender "0"
                     :recipient node-identifire
                     :amount 1}
        _ (core/create-transaction transaction)

        ;; チェーンに新しいブロックを加えることで、新しいブロックを採掘する
        block (core/create-block proof)]
    (response {:message "新しいブロックを採掘しました"
               :index (:index block)
               :transactions (:transactions block)
               :proof (:proof block)
               :previous-hash (:previous-hash block)})))

トランザクションで報酬を得る対象の recipient には自信のノードアドレスを格納します。create-block の実行により、前回のブロック作成以降に作られた全トランザクションを保持するブロックが作成されます。

ステップ3: オリジナルブロックチェーンとのインタラクション

作成した API を叩いて動作確認を行います。

サーバーを起動する

$ lein ring server-headless
* 2018-05-03 07:13:54.444:INFO:oejs.Server:jetty-7.6.13.v20130916
* 2018-05-03 07:13:54.528:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000
* Started server on port 3000

採掘を行う

http://localhost:3000/mineGET リスクエストを送信します。

$ curl http://localhost:3000/mine | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   253  100   253    0     0    298      0 --:--:-- --:--:-- --:--:--   297
{
  "message": "新しいブロックを採掘しました",
  "index": 2,
  "transactions": [
    {
      "sender": "0",
      "recipient": "84e6cd5894fb4a48b7647d3d75f977d2",
      "amount": 1
    }
  ],
  "proof": 35293,
  "previous-hash": "02179c23f9c6d4c7430b1365074e67f410028982beb2517c7800a99848bd8e37"
}
  • 作成されたブロックの index 番号が 2 になっていることを確認。
    • index 番号の 1 は初期化時に作成されるジェネシスブロックの番号。
  • transactions には採掘を行ったノードへ報酬を与えるトランザクションのみ格納されていることを確認。

トランザクションを作成する

http://localhost:3000/transactions/new へ JSON でトランザクション情報を POST します。

curl -X POST -H "Content-Type: application/json" -d '{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}' "http://localhost:3000/transactions/new" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   180  100    78  100   102   3452   4514 --:--:-- --:--:-- --:--:--  4636
{
  "message": "トランザクションはブロック3に追加されました"
}
  • 新しく追加したトランザクションは次に作成されるブロックに格納されるため、ブロックの index 番号が 3 になっていることを確認。

ブロックチェーンを取得する

http://localhost:3000/chainGET リクエストを送信し、ブロックチェーンを取得してみます。

$ curl http://localhost:3000/chain | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   335  100   335    0     0  20263      0 --:--:-- --:--:-- --:--:-- 20937
{
  "chain": [
    {
      "index": 1,
      "timestamp": 1525299681597,
      "transactions": [],
      "proof": 100,
      "previous-hash": "1"
    },
    {
      "index": 2,
      "timestamp": 1525299704639,
      "transactions": [
        {
          "sender": "0",
          "recipient": "84e6cd5894fb4a48b7647d3d75f977d2",
          "amount": 1
        }
      ],
      "proof": 35293,
      "previous-hash": "02179c23f9c6d4c7430b1365074e67f410028982beb2517c7800a99848bd8e37"
    }
  ],
  "length": 2
}
  • ジェネシスブロック、採掘で作成されたブロックが存在することを確認。
  • ブロック3はまだ、mine が行われていないため存在しないことを確認。

ステップ4: コンセンサス

これはクールだ。トランザクションを受け付けて、新しいブロックを採掘できるブロックチェーンを作ることが出来た。しかしブロックチェーンの重要なポイントは、非中央集権的であることだ。そしてもし非中央集権的であれば、我々はどのように地球上の全員が同じチェーンを反映していると確認することが出来るだろうか。これはコンセンサスの問題と呼ばれており、もし1つより多くのノードをネットワーク上に持ちたければ、コンセンサスのアルゴリズムを実装しなければならない。

新しいノードを登録する

コンセンサスアルゴリズムを実装する前に、ネットワーク上にある他のノードを知る方法を作ろう。それぞれのノードがネットワーク上の他のノードのリストを持っていなければならない。なのでいくつかのエンドポイントが追加で必要となる。

  1. URLの形での新しいノードのリストを受け取るための/nodes/register
  2. あらゆるコンフリクトを解消することで、ノードが正しいチェーンを持っていることを確認するための/nodes/resolve

エンドポイントを追加する前に、core.clj にノードを登録する関数を追加します。

core.clj

(ns clj-blockchain.core
  (:refer-clojure :exclude [hash])
  (:require [digest]
             [clojure.java.io :as io]))  ;; <-- 追加
            
(defonce nodes (atom #{}))

(defn register-node
  "ノードリストに新しいノードを加える"
  [address]
  (let [url (io/as-url address)
        node (str (.getHost url) ":" (.getPort url))]
    (swap! nodes #(conj % node))))

(defn init
  "チェーン、トランザクションの初期化を行う"
  []
  (reset! chain [])
  (reset! current-transactions [])
  (reset! nodes #{})    ;; <-- 追加

  ;; ジェネシスブロックを作る
  (let [proof 100
        previsous-hash "1"]
    (create-block proof previsous-hash)))

渡されたアドレスからホスト名とポート番号のみを取り出し nodes に格納しておきます。また、nodes は重複させないため、set にしておきます。

spec.clj
(s/def ::address string?) 
;; ノードのアドレスは http://192.168.0.5:5000 といった URL で渡されるため、
;; string? ではなく正規表現でスペックを書いたほうが良い

(s/fdef core/register-node
  :args (s/cat :address ::address))

コンセンサスアルゴリズムを実装する

以前言及したとおり、コンフリクトはあるノードが他のノードと異なったチェーンを持っているときに発生する。これを解決するために、最も長いチェーンが信頼できるというルールを作る。別の言葉で言うと、ネットワーク上で最も長いチェーンは事実上正しいものといえる。このアルゴリズムを使って、ネットワーク上のノード間でコンセンサスに到達する。

登録されているノード達が持つブロックチェーンを調べていき、一番長いブロックチェーンで自身のチェーンを置き換える処理を実装します。

core.clj
(defn get-nodes
  []
  (vec @nodes))

(defn valid-chain?
  "ブロックチェーンが正しいかを確認する"
  [chain]
  (loop [last-block (first chain)
         current-index 1]
    (if-let [block (get chain current-index)]
      (if (and (= (:previous-hash block) (hash last-block))
               (valid-proof? (:proof last-block) (:proof block)))
        (recur block (inc current-index))
        false)
      true)))

(defn fetch-chain
  [node]
  (let [url (str "http://" node "/chain")
        response (client/get url {:as :json})]
    (select-keys response [:status :body])))

(defn fetch-longest-chain
  [neighbours new-chain max-length]
  (if-let [node (first neighbours)]
    (let [response (request-chain node)]
      (if (= 200 (:status response))
        (let [length (get-in response [:body :length])
              chain (get-in response [:body :chain])]
          ;; そのチェーンがより長いか、有効かを確認
          (if (and (> length max-length)
                   (valid-chain? chain))
            (recur (rest neighbours) chain length)
            (recur (rest neighbours) new-chain max-length)))
        (recur (rest neighbours) new-chain max-length)))
    new-chain))

(defn resolve-conflicts
  []
  "これがコンセンサスアルゴリズムだ。ネットワーク上の最も長いチェーンで自らのチェーンを
   置き換えることでコンフリクトを解消する。"
  (if-let [new-chain (fetch-longest-chain (get-nodes) nil (count-chain))]
    (do
      ;; もし自らのチェーンより長く、かつ有効なチェーンを見つけた場合それで置き換える
      (reset! chain new-chain)
      true)
    false))

spec.clj
(s/def ::node string?)
(s/def ::nodes (s/coll-of ::node))
(s/def ::chain (s/coll-of ::block))
(s/def ::status (s/and pos-int? #(>= % 100) #(<= % 500)))
(s/def ::length pos-int?)
(s/def ::body (s/keys :req-un [::chain ::length]))
(s/def ::response (s/keys :req-un [::status ::body]))

(s/fdef core/valid-chain?
  :args (s/cat :chain ::chain)
  :ret  boolean?)

(s/fdef core/fetch-chain
  :args (s/cat :node ::node)
  :ret  ::response)
  
(s/fdef core/fetch-longest-chain
  :args (s/cat :neighbours ::nodes
                :new-chain  (s/nilable ::chain)
                :max-length pos-int?)
  :ret (s/nilable ::chain))  

(s/fdef core/resolve-conflicts
  :ret boolean?)

valid-chain? 関数ではチェーンの先頭ブロックから順番に、ハッシュに問題がないか?有効なプルーフであるか? を確認し、ブロックチェーンに改ざんが行われていないことを調べます。fetch-longest-chain 関数では保持するノードの "/chain" にリクエストを順に投げていき、有効かつ一番長いチェーンを探します。resolve-conflicts 関数がエンドポイントから呼び出す関数になり、fetch-longest-chainで自身のもつチェーンより長いチェーンが得られた場合、取得したチェーンで置き換えを行います。

ノードへのリクエストでは clj-http ライブラリを利用しています。

[clj-http "3.8.0"] ;; <-- project.clj の :dependencies に追加

handler.clj にエンドポイントを追加し、実装したノードを追加するための処理コンフリクトを解消する処理を API として実行できるようにします。

handler.clj
(defn- register-node
  [{:keys [params] :as req}]
  (if-let [nodes (:nodes params)]
    (do
      (if (string? nodes)
        (core/register-node nodes)
        (doseq [node nodes]
          (core/register-node node)))
      (response {:message "新しいノードが追加されました"
                 :total-nodes (core/get-nodes)}))
    (-> (response "Error: 有効ではないノードのリストです")
        (status 400))))

(defn- consensus
  [req]
  (if-let [replaced (core/resolve-conflicts)]
    (response {:message "チェーンが置き換えられました"
               :new-chain (core/get-chain)})
    (response {:message "正当なチェーンです"
               :new-chain (core/get-chain)})))

(defroutes app-routes
  (GET  "/chain"             req (chain req))
  (GET  "/mine"              req (mine req))
  (POST "/transactions/new"  req (create-transaction req))
  (POST "/nodes/register"    req (register-node req))        ;; <-- 追加
  (GET  "/nodes/resolve"     req (consensus req))            ;; <-- 追加
  
  (route/not-found "Not Found"))

追加した、2つの API を含めて動作確認を行います。

サーバーを起動する

コンフリクト解消のテストをするため、複数のノード(サーバー)を起動しておきます。

以下、

ノード3000: http://localhost:3000
ノード3001: http://localhost:3001

と呼びます。

$ lein ring server-headless 3000
2018-05-03 11:52:25.190:INFO:oejs.Server:jetty-7.6.13.v20130916
2018-05-03 11:52:25.261:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000
Started server on port 3000
$ lein ring server-headless 3001
2018-05-03 11:53:14.843:INFO:oejs.Server:jetty-7.6.13.v20130916
2018-05-03 11:53:14.915:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3001
Started server on port 3001

ノードを登録する

ノード3000にノード3001を登録します。

$ curl -X POST -H "Content-Type: application/json" -d '{
    "nodes": ["http://localhost:3001"]
}' "http://localhost:3000/nodes/register" | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   131  100    89  100    42    338    159 --:--:-- --:--:-- --:--:--   338
{
  "message": "新しいノードが追加されました",
  "total-nodes": [
    "localhost:3001"
  ]
}
  • taotal-nodes にノード3001が追加されていることを確認

採掘を行う

ノード3001で採掘を行います。

$ curl http://localhost:3001/mine | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   253  100   253    0     0    254      0 --:--:-- --:--:-- --:--:--   254
{
  "message": "新しいブロックを採掘しました",
  "index": 2,
  "transactions": [
    {
      "sender": "0",
      "recipient": "a766dec9d04f47edb6d60074161ff904",
      "amount": 1
    }
  ],
  "proof": 35293,
  "previous-hash": "f33abbca0381d0358521839fb858070ccfc92901c016752334e0f1cef87e18b5"
}

ノード3001のチェーンを確認しておきます。

$ curl http://localhost:3001/chain | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   335  100   335    0     0  22517      0 --:--:-- --:--:-- --:--:-- 23928
{
  "chain": [
    {
      "index": 1,
      "timestamp": 1525315994788,
      "transactions": [],
      "proof": 100,
      "previous-hash": "1"
    },
    {
      "index": 2,
      "timestamp": 1525316222823,
      "transactions": [
        {
          "sender": "0",
          "recipient": "a766dec9d04f47edb6d60074161ff904",
          "amount": 1
        }
      ],
      "proof": 35293,
      "previous-hash": "f33abbca0381d0358521839fb858070ccfc92901c016752334e0f1cef87e18b5"
    }
  ],
  "length": 2
}
  • mine を行ったので chain の長さが 2 になっていることを確認

ノード3000のチェーンを確認しておきます。

$ curl http://localhost:3000/chain | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   110  100   110    0     0   2511      0 --:--:-- --:--:-- --:--:--  2558
{
  "chain": [
    {
      "index": 1,
      "timestamp": 1525315945138,
      "transactions": [],
      "proof": 100,
      "previous-hash": "1"
    }
  ],
  "length": 1
}
  • mine を行っていないため、ジェネシスブロックのみであることを確認

コンフリクトを解消する

ノード3000でコンフリクトの解消を行います。

$ curl http://localhost:3000/nodes/resolve | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   383  100   383    0     0   1423      0 --:--:-- --:--:-- --:--:--  1423
{
  "message": "チェーンが置き換えられました",
  "new-chain": [
    {
      "index": 1,
      "timestamp": 1525315994788,
      "transactions": [],
      "proof": 100,
      "previous-hash": "1"
    },
    {
      "index": 2,
      "timestamp": 1525316222823,
      "transactions": [
        {
          "sender": "0",
          "recipient": "a766dec9d04f47edb6d60074161ff904",
          "amount": 1
        }
      ],
      "proof": 35293,
      "previous-hash": "f33abbca0381d0358521839fb858070ccfc92901c016752334e0f1cef87e18b5"
    }
  ]
}
  • 登録されているノード3001のブロックチェーンの方が長いので置き換えが実施されることを確認。

念の為、chain を確認しておきます。

$ curl http://localhost:3000/chain | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   335  100   335    0     0  25842      0 --:--:-- --:--:-- --:--:-- 27916
{
  "chain": [
    {
      "index": 1,
      "timestamp": 1525315994788,
      "transactions": [],
      "proof": 100,
      "previous-hash": "1"
    },
    {
      "index": 2,
      "timestamp": 1525316222823,
      "transactions": [
        {
          "sender": "0",
          "recipient": "a766dec9d04f47edb6d60074161ff904",
          "amount": 1
        }
      ],
      "proof": 35293,
      "previous-hash": "f33abbca0381d0358521839fb858070ccfc92901c016752334e0f1cef87e18b5"
    }
  ],
  "length": 2
}
  • ノード3001のチェーン取得と同様の値が返ってくることを確認。

おわりに

元記事のソースコードが300行程度で、簡単に実装できそうだなと思い、Clojure でブロックチェーンを実装してみました。自分の書いたコードは確認したところ core.clj, handler.clj を合わせて230行程度でしたので、どの言語でも200〜300行程度のコードで実装できるのではないでしょうか。Web 上に仮想通貨やブロックチェーン関連の記事がたくさんあふれていていますが、エンジニアの方は自分で実装した方が理解が早いと思います。ぜひ、お好きな言語でチャレンジしてみてください。

20
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?