"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

マイブロックチェーンの骨格だけPythonで実装してみる/ブロックチェーンの勉強一回目

ブロックチェーン技術の勉強に、pythonで「マイブロックチェーン」を実装してみようという試みです。

f:id:arakan_no_boku:20200130223854p:plain

 

はじめに

 

ブロックチェーンについて勉強したことの整理を兼ねて、Pythonで実装してみます。

ブロックチェーンは要素がいろいろあるので、一度にあれもこれもするとポイントがぼけてしまうので、3回くらいにわけて、一歩一歩って感じでやります。

今のところの予定としては。

一回目:ブロックチェーンの骨格を作る

二回目:保存や改ざんチェックなどの機能を付け加えてみる

三回目:マイニングにあたる部分を付け加えてみる

という感じで考えてます。

 

ブロックチェーンの概念をざっくりおさらい

 

ブロックチェーンは意味合い的には「ブロックにまとめたデータを数珠つなぎにしてチェーンを形成したもの」であり、

  • 分散型
  • 非中央集権的
  • 新規書き込み専用・改ざん困難

という特徴に特化したデータ管理の仕組です。

これらの特徴がマッチして、ビットコインなどの暗号通貨の中核機能になりました。

改ざんを難しくするために、数珠つなぎになった前のブロックのハッシュを含むデータで新しいハッシュを計算するようなアプローチをとってます。

P2Pネットワークでつながった利用者のPCには、数珠繋ぎになった全てのデータがダウンロードされる必要があり、ブロックチェイン全体のサイズが大きくなりすぎないように、ひとつひとつのブロックで扱えるのは、ごく小さなデータになります。

それでも、チェインされるブロックの数が増えると結構なサイズになり、例えば、ビットコインコアの場合だと、2020年1月時点で「約210GBのデータが最初にダウンロードされて、毎月5-10GBデータが増加していく。」そうです。

bitcoincore.org

 

ざっくり概要としては、こんな感じです。 

 

マイブロックチェーンのブロック仕様

 

今回は、ブロックチェーンの骨格だけ「MyBlockChain」クラスとして実装します。

改ざん検知やマイニングに関連する部分などは次回以降にまわします。

さて。

MyBlockChainクラスの仕様を決めます。

最初に。

何をブロックのトランザクションとして管理するか・・を決めます。

とりあえず、暗号通貨にならって、入力と出力のペアをトランザクションにします。

例えば。

入力:XXX

出力:VVV 123456

出力:ZZZ 5678

 とすると、アドレス「XXX」から「VVVへ 123456P」「ZZZへ5678P」移動したという意味にする感じです。

さて。

今後は実装するブロックの仕様ですけど、ビットコインブロックチェーンを構成する「ブロック」も参考にして、以下のようにしてみます。

内容は、以下のようにします。

ブロックヘッダは、とりあえず

だけの最低限の仕様にします。

あと、ビットコインとかだと、10分間に発生した複数トランザクションを1ブロックにまとめる・・みたいになっているそうですが、今回は1ブロック1トランザクションとして、そのかわり1トランザクションに複数の入出力をもてるということにします。

さてさて。

こんなところで・・実装してみます。

 

Pythonでの実装

 

MyBlockChainクラスの全ソースです。

最低限の骨格だけです。

import datetime as dt
import json
import hashlib


class MyBlockChain(object):
    # ブロックチェーンを初期化する
    def __init__(self):
        self.chain = []

    # 新しいブロックを作成する
    def add_new_block(self, inp, outp):
        # トランザクションを生成する
        new_transaction = self.__create_new_transaction(inp, outp)
       
        # 前のブロックのハッシュを取得。最初だけ固定値
        if len(self.chain) > 0:
            prev_hash = self.chain[-1]['block_header']['tran_hash']
        else:
            prev_hash = "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13"

        # トランザクションを元にブロックを生成して、チェーンに接続する
        new_block = {
            'block_index': len(self.chain) + 1,
            'block_time': dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'block_header': {
                'prev_hash': prev_hash,
                'tran_hash': self.__hash(prev_hash + self.__calc_tran_hash(new_transaction)),
            },
            'tran_counter': len(inp) + len(outp),
            'tran_body': new_transaction,
        }
        self.chain.append(new_block)
        return new_block

    # 新しいトランザクションを生成する
    def __create_new_transaction(self, inp, outp):
        new_transaction = {
            'input': inp,
            'output': outp,
        }
        return new_transaction

    # ハッシュ値を計算する。SortをTrueにしているのはハッシュの整合性維持のため
    def __calc_tran_hash(self, new_transaction):
        tran_string = json.dumps(new_transaction, sort_keys=True).encode()
        return self.__hash(tran_string)
    
    def __hash(self, str_seed):
        return hashlib.sha256(str(str_seed).encode()).hexdigest()

    # ブロックの内容を表示する
    def dump(self, block_index=0):
        if(block_index == 0):
            print(json.dumps(self.chain, sort_keys=False, indent=2))
        else:
            print(
                json.dumps(
                    self.chain[block_index],
                    sort_keys=False,
                    indent=2))

補足説明していきます。

ブロックチェイン全体は「self.chain = []」で管理します。

今回はまだ既存のブロックチェインを読み込んで初期化する処理は実装してません。

処理の順序としては。

  1. 入力と出力を、それぞれ「input」「output」の辞書として用意する。
  2. それを受け取り、新しいトランザクションを生成する。
  3. 前のブロックのハッシュを取得する、ただ、最初のブロックは固定値になる。
  4. トランザクションに前のブロックのハッシュを加えて、新たなハッシュを計算する。
  5. トランザクション・計算済ハッシュなどを使って新しいブロックを生成・追加する。

みたいになります。

 

入力と出力を、それぞれ「input」「output」の辞書として用意する。

 

 これは、上記のクラスには含まれません。

使う時に。

bc = MyBlockChain()
inp = {
    'XXXXXXXX': 12345,
}
out = {
    'YYYYYYYY': 10000,
    'ZZZZZZZZ': 2345,
}
bc.add_new_block(inp, out)

みたいにして使うということです。

入力(inp)と出力(outp)それぞれに対して辞書データを生成して引数にします。

上記の意味は。

  • XXXXXXXX から、12345 をとりだす。
  • YYYYYYYY に、10000 を渡す。
  • ZZZZZZZZZ に、2345 を渡す。

ということを想定してます。

XXXXXXXXとかを、お財布的なものの識別コードと思えば、イメージできるんじゃないでしょうか。

 

それを受け取り、新しいトランザクションを生成する。 

 

これは、以下のメソッドで対応しています。 

    def __create_new_transaction(self, inp, outp):
        new_transaction = {
            'input': inp,
            'output': outp,
        }
        return new_transaction

 単純に、上記の入力・出力をうけとり、トランザクションとして扱うデータ構造(辞書)に格納して返すだけです。

 

前のブロックのハッシュを取得する、ただ、最初のブロックは固定値になる。

 

 コードでは以下の部分です。

if len(self.chain) > 0:
    prev_hash = self.chain[-1]['block_header']['tran_hash']
else:
    prev_hash = "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13"

ブロックチェインが既に存在する場合は、前のブロックのトランザクションハッシュを取得し、無い場合(一番最初のブロック)は、仕方ないので計算済の適当なハッシュ値を初期値にします。

なお、ブロックの構成を以下のようにするので、上記のような取得の仕方になります。

new_block = {
    'block_index: ブロックインデックス,
    'block_time' ブロック生成時刻,
    'block_header' { ブロックヘッダ
        'prev_hash' , 前のブロックのトランザクションハッシュ
        'tran_hash' , このブロックのトランザクションハッシュ
    },
    'tran_counter' , トランザクションの件数
    'tran_body' , トランザクション本体
}

 

トランザクションに前のブロックのハッシュを加えて、新たなハッシュを計算する。

 

ハッシュを、ひとつ前のブロックで計算したハッシュ値を加えて計算します。

まず、ハッシュを計算する部分です。

今回は簡易化のため、トランザクション全体をJSONフォーマットに変換して、それをもとにハッシュを計算する仕様にしました。

# ハッシュ値を計算する。SortをTrueにしているのはハッシュの整合性維持のため
def __calc_tran_hash(self, new_transaction):
    tran_string = json.dumps(new_transaction, sort_keys=True).encode()
    return self.__hash(tran_string)
    
def __hash(self, str_seed):
   return hashlib.sha256(str(str_seed).encode()).hexdigest()
   

ここで求めたハッシュ文字列に、前のブロックのハッシュを連結して、さらにハッシュを計算して、該当ブロックのハッシュコードとします。

ビットコイン等だと1ブロックに複数(それこそ1000とか2000とか)のトランザクションを含むことがあって、トランザクション単位でハッシュを求めて(ルートトランザクション)、それを前に計算したハッシュと連結して、さらにハッシュを求める・・みたいに、複雑な計算をしているらしいので、それっぽくしてみただけですが(笑)

でも、こうすることで、ブロックのトランザクションデータを改ざんしても、ハッシュ値があわなくなりますし、なんとか一つのブロック内で帳尻をあわせても、次のブロックで不整合がおきることになります。

こんな風に、ブロック同士が密に結合するようにして、改ざんをしづらくする仕組みになっているようです。

 

トランザクション・計算済ハッシュなどを使って新しいブロックを生成・追加する。

 

該当する部分です。

new_block = {
    'block_index': len(self.chain) + 1,
    'block_time': dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    'block_header': {
        'prev_hash': prev_hash,
        'tran_hash': self.__hash(prev_hash + self.__calc_tran_hash(new_transaction)),
    },
    'tran_counter': len(inp) + len(outp),
    'tran_body': new_transaction,
}
self.chain.append(new_block)

シンプルに仕様通りの構造にした辞書データをセットしてから、chainに連結しているだけです。

 

試しに使ってみます。

 

上記クラスは、とりあえず初期化状態から「ブロックチェイン」らしきデータ構造を生成できるだけで、保存も読み込みもできません。

ですが。

そのへんを拡張するのは次回以降に、ぼちぼちするとして・・。

いくつかのトランザクションを作って、ブロックを追加してみます。

bc = MyBlockChain()
# ひとつめ
inp = {
    'Y3XNSHTT': 12345,
}
out = {
    'X2VBASDD': 10000,
    'Z45UHRKL': 2345,
}
bc.add_new_block(inp, out)
# ふたつめ
inp1 = {
    'X2VBASDD': 22345,
}
out1 = {
    'Y3XNSHTT': 10000,
    'Z45UHRKL': 12345,
}
bc.add_new_block(inp1, out1)
# みっつめ
inp2 = {
    'Z45UHRKL': 23450,
}
out2 = {
    'Y3XNSHTT': 13000,
    'X2VBASDD': 10450,
}
bc.add_new_block(inp2, out2)
bc.dump()

実行した結果は以下です。

[
  {
    "block_index": 1,
    "block_time": "2020-02-03 22:22:05",
    "block_header": {
      "prev_hash": "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13",
      "tran_hash": "a6cfb4c140cf8cf2b281d5538961cdcfaca30a464befc02bf5af3bc7397cd4d7"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "Y3XNSHTT": 12345
      },
      "output": {
        "X2VBASDD": 10000,
        "Z45UHRKL": 2345
      }
    }
  },
  {
    "block_index": 2,
    "block_time": "2020-02-03 22:22:05",
    "block_header": {
      "prev_hash": "a6cfb4c140cf8cf2b281d5538961cdcfaca30a464befc02bf5af3bc7397cd4d7",
      "tran_hash": "ff0bdebe585ee6da9686455b38d55cccec78c34c329a2e49166807c3d61caf6c"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "X2VBASDD": 22345
      },
      "output": {
        "Y3XNSHTT": 10000,
        "Z45UHRKL": 12345
      }
    }
  },
  {
    "block_index": 3,
    "block_time": "2020-02-03 22:22:05",
    "block_header": {
      "prev_hash": "ff0bdebe585ee6da9686455b38d55cccec78c34c329a2e49166807c3d61caf6c",
      "tran_hash": "d67635b79960ab39729ff0a60c5a43fce9dfce36f5b504e3f260948f6a10d3db"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "Z45UHRKL": 23450
      },
      "output": {
        "Y3XNSHTT": 13000,
        "X2VBASDD": 10450
      }
    }
  }
]

いちおう、意図したようにはできているようです。

最後に、自分がブロックチェーンの勉強のために読んで、一番わかりやすくて役に立ったと思っている本をご紹介だけしておきます。

ブロックチェーン 相互不信が実現する新しいセキュリティ (ブルーバックス)

今回は、こんなところで。

ではでは。