ブロックチェーンを実装する:一回目:ブロックチェーンの骨格を作る/Pythonサンプル
目次
- Pythonでブロックチェーンを実装するサンプル/一回目
- 一回目:ブロックチェーンの骨格を作る
- ブロックチェーンクラス一回目時点の全ソース
- ブロックチェーンクラスを使うサンプルコード
- 二回目・三回目のリンクと参考文献
Pythonでブロックチェーンを実装するサンプル/一回目
ブロックチェーンの勉強のため、pythonで「ブロックチェーン」を実装します。
ブロックチェーンは要素がいろいろあるので、3回にわけて少しずつ実装し、三回目での完成を目指します。
- 一回目:ブロックチェーンの骨格を作る
- 二回目:骨格に、保存や改ざんチェック機能を追加する
- 三回目:骨格にさらにマイニング部分を追加する
一回目:ブロックチェーンの骨格を作る
最初にざっくりとしたブロックチェーンのアウトラインを実装します。
それにあたって、まずは概念と仕様を整理します。
ブロックチェーンの概念
ブロックチェーンは「ブロックにまとめたデータを数珠つなぎにしてチェーンを形成したもの」であり、
- 分散型
- 非中央集権的
- 新規書き込み専用・改ざん困難
という特徴を持つデータ管理の仕組です。
これらの特徴により、ビットコインなどの暗号通貨の中核機能になりました。
数珠つなぎになった前のブロックのハッシュを含むデータで新しいハッシュを計算することで改ざんを困難にするアプローチをとってます。
そのため、利用者のPCには、数珠繋ぎになった全てのデータがダウンロードされる必要があるため、ブロックチェイン全体のサイズが大きくなりすぎないよう、ひとつひとつのブロックで扱えるのはごく小さなデータだけです。
それでも、チェインされるブロックの数が増えると結構なサイズになります。
例えば、ビットコインコアの場合だと、2020年1月時点で「約210GBのデータが最初にダウンロードされて、毎月5-10GBデータが増加していく。」そうです。
ざっくり概要としては、こんな感じです。
ブロックの仕様検討
実装するブロックの仕様を決めます。
ビットコインの「ブロック」を参考にして、以下のようにしてみます。
- ブロックインデックス(block_index)
- ブロック生成時刻(block_time)
- ブロックヘッダ(block_header)
- トランザクションカウンタ(tran_counter)
- トランザクション(tran_body)
内容は、以下のようにします。
- ブロックインデックス:ブロックを一意に識別するID
- ブロック生成時刻:ブロックを生成したタイムスタンプ
- トランザクションカウンタ:トランザクションの入出力の件数
- トランザクション:上記みたいな入力と出力の組合せ
ブロックヘッダは、
だけの最低限の仕様にします。
あと、ビットコインとかだと、10分間に発生した複数トランザクションを1ブロックにまとめる・・みたいになっているそうですが、今回は1ブロック1トランザクションとして、そのかわり1トランザクションに複数の入出力をもてるということにします。
ブロックチェーンクラスの仕様検討
MyBlockChainクラスという名前にします。
仕様を決めます。
まず、何をブロックのトランザクションとして管理するか・・ですが、暗号通貨にならって、入力と出力のペアをトランザクションにします。
例えば。
入力:XXX
出力:VVV 123456
出力:ZZZ 5678
とすると、アドレス「XXX」から「VVVへ 123456P」「ZZZへ5678P」移動したという意味にする感じです。
今回はまだ既存のブロックチェインを読み込んで初期化する処理は実装しません。
処理の順序としては。
- 入力と出力を、それぞれ「input」「output」の辞書として用意する。
- それを受け取り、新しいトランザクションを生成する。
- 前のブロックのハッシュを取得する、ただ、最初のブロックは固定値になる。
- トランザクションに前のブロックのハッシュを加えて、新たなハッシュを計算する。
- トランザクション・計算済ハッシュなどを使って新しいブロックを生成・追加する。
みたいにします。
処理順1:入力と出力を「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とかを、お財布的なものの識別コードと思えば、イメージできるんじゃないでしょうか。
処理順2:新しいトランザクションを生成する。
これは、以下のメソッドで対応しています。
def __create_new_transaction(self, inp, outp): new_transaction = { 'input': inp, 'output': outp, } return new_transaction
単純に、上記の入力・出力をうけとり、トランザクションとして扱うデータ構造(辞書)に格納して返すだけです。
処理順3:前のブロックのハッシュを取得する。
コードでは以下の部分です。
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' , トランザクション本体
}
処理順4:新たなハッシュを計算する。
ハッシュを、ひとつ前のブロックで計算したハッシュ値を加えて計算します。
まず、ハッシュを計算する部分です。
今回は簡易化のため、トランザクション全体を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とか)のトランザクションを含むことがあって、トランザクション単位でハッシュを求めて(ルートトランザクション)、それを前に計算したハッシュと連結して、さらにハッシュを求める・・みたいに、複雑な計算をしているらしいので、それっぽくしてみただけですが(笑)
でも、こうすることで、ブロックのトランザクションデータを改ざんしても、ハッシュ値があわなくなりますし、なんとか一つのブロック内で帳尻をあわせても、次のブロックで不整合がおきることになります。
こんな風に、ブロック同士が密に結合するようにして、改ざんをしづらくする仕組みになっているのです。
処理順5:新しいブロックを生成・追加する。
該当する部分です。
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に連結します。
ブロックチェーンクラス一回目時点の全ソース
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))
補足説明していきます。
ブロックチェーンクラスを使うサンプルコード
上記クラスは、とりあえず初期化状態から「ブロックチェイン」らしきデータ構造を生成できるだけで、保存も読み込みもできません。
ですが。
そのへんを拡張するのは次回以降に、ぼちぼちするとして・・。
いくつかのトランザクションを作って、ブロックを追加してみます。
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 } } } ]
いちおう、意図したようにはできているようです。
二回目・三回目のリンクと参考文献
一回目はこれで終わりです。
最後に、自分がブロックチェーンの勉強のために読んで、一番わかりやすくて役に立ったと思っている本をご紹介だけしておきます。
ではでは。