Python
Blockchain
SFC-RGDay 2

オレオレブロックチェーンを作る

この記事はSFC-RG Advent Calendar 2017の2日目です。

オレオレブロックチェーン

こんな記事(日本語版)があったので自分もブロックチェーンの勉強&実装トレーニングのためにオレオレブロックチェーンを作ることにした。ぼくのかんがえるさいきょうのぶろっくちぇーんを作ろう。今回のソースコードはここにある。と言いつつまだまだ実装途中だ。

要件

所謂「ブロックチェーン」と呼ばれる何かの定義は非常に曖昧なものである。例えば「ブロックのハッシュを次のブロックに含むデータ構造」のことをブロックチェーンと呼ぶし、「ブロックに直前のブロックのハッシュを含むデータ構造の台帳を、P2Pネットワーク内で一意に特定するシステム」全体のことをブロックチェーンと呼んだりする。ここでは、次のことを要件とした。

  • ブロックの中にはトランザクションと呼ばれる任意のデータを保持できる
  • ブロックのハッシュを次のブロックに含める
  • すべてのノードがgenesisからすべてのブロックを持ち、新たなブロックの検証が行える
  • ブロックの競合が起こった場合、ブロック内に含まれるscoreの値が高い方を優先する

トランザクションの中身を任意のデータにしたのは、トランザクションの検証を作るのがめんどくさい上記のことで所謂「改ざん困難な台帳」というものの基本的な構造を示せると思ったからである。ブロックのハッシュが正しく連鎖しなければ、改ざんされたことはわかる。scoreはBitcoinなどProof-of-Workベースのもので言うところのDifficultyだ。今回はPoWなどは未実装で任意のscoreを指定できるようになっているが、ここを一定の値を得るのが難しく、検証可能なものに置き換えれば最終的に1本のチェーンに収束するということを実現できるだろう。

開発環境

  • Python 3.6.0 :: Anaconda 4.3.1
  • OS X 10.11 El Capitan / Ubuntu 16.06 LTS

実装

実装順

自分は実装順として、

  • ブロックの連鎖構造
  • トランザクション
  • メッセージング
  • 異なるチェーンの解決

という順序で実装した。この順序が一番わかりやすいような気はする。オレオレブロックチェーンだ、好きに作ろう。

ブロック

ブロック生成

まずはブロックのハッシュ連鎖だろう、ということでそれを作る。Blockchainというクラスを作り、その中でブロックの生成及びチェーンへの追加可否のチェックなどを行うことにする。ブロックやトランザクションはわかりやすくJSONで扱うことにしよう。理解のためのオレオレブロックチェーンなのだから、自分が扱いやすいもので好きに作るパフォーマンスとかコストとかは二の次だ。下記はBlockを生成する関数である。

generate_block
    def generate_block(self, score):
        # Make new Block include all tx in txpool
        pool = []
        for txid in self.tx.txpool.keys():
            pool.append(self.tx.txpool[txid])
        blocknum = len(self.chain)
        previous = json.dumps(self.chain[-1])
        previoushash = hashlib.sha256(previous.encode('utf-8')).hexdigest()
        block = {"blocknum":blocknum, "tx":pool, "previous_hash":previoushash, "score":score}
        self.tx.txpool = {}

        print("-----------------------")
        print(json.dumps(block,indent=4,
                        ensure_ascii=False,
                        sort_keys=True))
        print("-----------------------")
        self.logger.log(20,"Generate New Block(%s)" % block["blocknum"])
        return block 

ここでやっていることは、自分の持つチェーンはlistとしたので、その長さをblocknum、最新のブロックのハッシュ値をprevious_hashにそれぞれ突っ込む、トランザクションプールにあるトランザクションをすべてブロックに突っ込み、トランザクションプールを空にする、ということだ。これでブロックらしきものはできた。

チェーンへの追加・検証

ブロックを作ったので自分のチェーンに追加する。ここでブロックが正しくチェーンに追加できるかどうかもチェックしなければならない。このチェックが最終的に一意のチェーンに定まるための重要部分であると言えるだろう。他のノードから受け取ったブロックの場合、自分が持つトランザクションを含んでいると考えられるので、チェックを通過し追加を行う際は、トランザクションプールからブロックの中にあるトランザクションを削除する処理が必要である。

また、後ほど出てくるが、自分の最新ブロックよりも数ブロック先、所謂orphanブロックだった場合、ハッシュが全く連鎖しないチェーンである場合などもしっかり検知しなければならない。ブロックの検証は以下のような感じだ。

verify_block
    def verify_block(self, block):
        # Verify Block
        previous = json.dumps(self.chain[-1])
        previoushash = hashlib.sha256(previous.encode('utf-8')).hexdigest()
        if block["blocknum"] == self.chain[-1]["blocknum"]+1:
            # new block
            if block["previous_hash"] == previoushash:
                msg = {"result":"Checked Block has been verified","code":0}
            else:
                msg = {"result":"Checked Block is from different chain","code":1}
        elif self.chain[-1]["blocknum"] < block["blocknum"]:
            # orphan block
            msg = {"result":"Checked Block is orphan","code":2}
        else:
            # old block
            jsonblock = json.dumps(block)
            myblock = json.dumps(self.chain[block["blocknum"]])
            if hashlib.sha256(jsonblock.encode('utf-8')).hexdigest() == hashlib.sha256(myblock.encode('utf-8')).hexdigest():
                msg = {"result":"Checked Block has been in my chain","code":3}
            else:
                msg = {"result":"Checked Block is old and come from different chain","code":4}

        self.logger.log(20, msg["result"])
        return msg

トランザクション

トランザクションは任意の今回データ構造なので適当。作ったトランザクションをトランザクションプールと呼ばれる箱にどんどん入れて行って、随時ブロックを作る/受け取る/消す時にそこから出し入れしていく。

メッセージング

今回はSocket通信を使った。この記事を読みながら作った。

ピア管理

今回はピアを追加する際にIPv4アドレスかどうかのチェックしか行っておらず、一方的にピアと認識するだけである。本来であれば追加するピアに対して疎通確認を行い、追加された側にも、ピアとして登録させる、といったことをやる必要がある気がする。また疎通性が失われた時の処理も未実装。

各メッセージ

現状メッセージはjson形式で送り、typeでどんなメッセージかを示している。実装済みのメッセージタイプは以下。

  • block:新規ブロックの送信
  • tx:新規トランザクションの送信
  • getblk:指定blocknumのブロックの送信要求

この3つで基本的なことはできるはずだ。新規ブロック/トランザクションを生成した際にメッセージングするようにする。

異なるチェーンの解決

ブロックを受信した際に、Blockchainのメソッドで自分のチェーンに追加できるかのバリデーションを行う。ここでorphanだった場合と、異なるチェーンの最新として生成されたブロックであった場合、所謂フォークが起こっている場合の処理を行わなければならない。この部分が最重要部分であると自分は考えている。

orphanであった場合は単純だ。自分の最新ブロックから、受信したブロックの間のblocknumのblockを受信したピアへgetblkで要求する。

フォークの場合は少し困った。フォークがどこから起こっているかを特定するために、受信ブロックからgenesisまで一つずつブロックをgetblkで要求し、要求blocknumの時点で自分のチェーンに繋がるかどうかをチェックする。check_new_block_for_chainというメソッドで自分のチェーンの一部に対して特定のブロックが繋げられるかどうかをチェックしている。繋がる場合は、自分の持っているチェーンと、ピアの持っているチェーンの分岐点だということになる。その時にブロックのscoreの比較を行い、高い方を採用する。もし自分のブロックを採用しない場合、フォーク時点のブロックまでチェーンをバラしていく作業が必要になる。

resolv_different_chain
    def resolv_different_chain(self, rcvblock, client_address):
        for blocknum in reversed(range(1,rcvblock["blocknum"])):
            res, resmsg = self.send({"type":"getblk", "body":{"blocknum":blocknum}}, client_address)
            resmsg = json.loads(resmsg.decode('utf-8'))
            block = resmsg["body"]
            if self.check_new_block_for_chain(self.bc.chain[0:block["blocknum"]], block):
                if block["score"] > self.bc.chain[block["blocknum"]]["score"]:
                    for rmblocknum in range(blocknum,len(self.bc.chain)):
                        self.bc.rm_last_block()
                    self.bc.add_new_block(block)
                break

また、このメソッドを実行した後は自分のチェーンは分岐が起こっていたところまでになるので、orphanの解決メソッドを用いて、最初に受信したブロックまでの間のブロックを取得する。

今後

Proof-of-hogehogeみたいなものを作って、しっかりと一本のチェーンに収束させるようにしたい。オレオレブロックチェーンと言いつつもそこそこ自分の中で考えるブロックチェーンらしきものは出来上がってきているので、ブロックチェーンの拡張などを思いついたらこれでPoCをやってみるのもいいと思っている。なにせオレオレブロックチェーンだ、ぼくのかんがえるさいきょうのぶろっくちぇーんにしよう。

少しづつブロックチェーンのようなものが出来上がっていくのは楽しい。そして概念的に理解していても作るとなると各要素を分解してしっかりと構造化しないと実装していくのは難しい。だがこれは確実に力になるので、興味がある人はぜひぼくのかんがえるさいきょうのぶろっくちぇーんを作ろう。