Blow Up by Black Swan

ブロックチェーンをプログラミングしてみた(後編〜プログラミング)

前回の記事の続きです。

 
今回はPythonを使ったプログラミングが中心になります。基本的には以前も紹介したこちらの記事がメインの参考サイトになります。

自分用により細かく注釈をつけたコードは、githubに載せています(github初体験)。筆者は、基本的にプログラミングもインターネットやその仕組みも初心者ですので、そんな筆者が「こんな説明してくれたらわかりやすかったのに」と思ったことに沿って、記事を書いてます。そのため、ある程度理解がある方からすると、少しざっくりしすぎてたり、細かい言い回しがおかしいと言ったことを感じるかもしれませんが、その点はご了承ください。

2 ブロックチェーン・プログラミング

2.1 前提条件

このブロックチェーン・プログラミングでは、Pythonを使用しています。参考サイトではPycharmを利用していますが、僕はanacondaを使ってPythonを勉強したので、エディターにはSpyderを使ってます。このプログラミングをするときに多々知らないモジュールなどがあったので、本編に関係する部分を最後の方に一括して書いてます。私の知識基準になっていますが、Flaskは当初全く意味がわからなかったので、別記事で詳述する予定です。各メソッドなどに振られている数字ですが、「1つ目(クラス文か実行関数か)ー2つ目(その中の順番)」という振り方をして、わかりやすくしてます。クラス文のコンストラクタだけそれぞれを識別するために番号を振ってます。

ネットワークへのメッセージに対し反応していくという形になるので、それに対応する②部分からコードを読んで、①を見ていくという見方がでわかりやすいと思い、記載順序もそのようにしています。(コード全体はクラス文からの記載になりますので、①と②はそちらに合わせてつけています)。

2.2 全体像

前回の記事で説明した全体と部分に沿って、今回も説明していきます。

  • 取引の実行(トランザクション部分:②-1) → ブロックの作成(マイニング部分:②-2)
  • 各ノードが適宜コンセンサスアルゴリズムでブロックチェーンを相互に検証することででネットワーク全体のブロックチェーンが一致(コンセンサスアルゴリズム部分:②-3)
  • ネットワークに参加するために、他のノードとつながりを持っていく(他のノードとの接続部分:②-4)

2.3 基本的なコード

今回のブロックチェーンのプログラミンで基本となる部分のコードです。

import hashlib
import json
from time import time
from urllib.parse import urlparse
from uuid import uuid4
import requests
from flask import Flask, jsonify, request

class Blockchain:

    ----(省略)----

    #①-2 ラストブロックリターン用クラス関数
    @property
    def last_block(self):
        return self.chain[-1]

app = Flask(__name__)    #ノードの作成

node_identifier = str(uuid4()).replace('-', '')    #ノード独自の識別子を作る

blockchain = Blockchain()    #ブロックチェーンクラスのインスタンス化(コンストラクタの発動)

2.4 【1】トランザクション部分

まず、今回のプログラミングで利用するトランザクションは非常にシンプルな以下のものになります。

 #<トランザクションイメージ>
 {
 "sender": "my address",                #送り手(ウォレットアドレス)
 "recipient": "someone else's address", #受け手(ウォレットアドレス)
 "amount": 5                            #送金量
}

トランザクション部分の具体的な流れは次のようになります(②-1)。

  1. トランザクションが実行される(POSTされる)
  2. 有効なトランザクションか確認
  3. トランザクションリストの末尾に追加(①-3)
  4. ※トランザクション実行時点では、電子署名の実行やウォレット内の保有量の確認、UTXOなどの仕組みがありますが、この記事では省略してます。
#②-1 トランザクション用メソッド
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    values = request.get_json(force=True)         #(Reponse)POSTされたトランザクションデータを解析

    #POSTされたトランザクションが有効か確認
    required = ['sender', 'recipient', 'amount']  #(リスト)トランザクションの定義
    if not all(k in values for k in required):
        return 'Missing values', 400
    index = blockchain.new_transaction(values['sender'],values['recipient'], values['amount'])
     #<int>クラスメソッド(#①-3)の実行

    response = {'message': f'Transaction will be added to Block {index}'}
    return jsonify(response), 201

class Blockchain:
    #①-1 コンストラクタ
    def __init__(self):
        self.current_transactions = []     #①-1-1(リスト( ブロックに格納されていないトランザクションリスト

    ・・・(省略)・・・

    #<トランザクション用メソッド>
    #①-3 トランザクション追加用クラスメソッド
    def new_transaction(self, sender, recipient, amount):
        """
        作業内容:次のブロックに入るトランザクションの一覧を作成(トランザクションが実行されるごとに追加されていく)
        :param sender: (str)送り手のアドレス
        :param recipient: (str)受け手のアドレス
        :param amount: (int)送金量
        :return: (int)このトランザクションの塊を含むブロックのインデックス
        """
     #トランザクションリストに辞書型で追加(①-1-1)
        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })
        return self.last_block['index'] + 1 

2.5 【2】マイニング

今回のブロックは以下のようなシンプルなものを作ります。

<ブロックイメージ>
block = {
    'index': 1,                                                #1.インデックス
    'timestamp': 1506057125.900785,                            #2.タイムスタンプ
    'transactions': [                                          #3.トランザクション(複数入る)
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",      #3-1.送り手(ウォレットアドレス)
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",   #3-2.受け手(ウォレットアドレス)
            'amount': 5,                                       #3-3.送金量
        }
    ],
    'proof': 324984774000,                                     #4.プルーフ
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b2938b9824"    #5.1つ前のブロックのハッシュ値
}

次がブロックの生成を行うマイニングです。この点で大事なことの1つがジェネシスブロックです。ジェネシスブロックは、1番最初のブロックのため、直近ブロッックが存在せず、直前のハッシュ値という値も持ちません。そのため、コンストラクターで規定します。ビットコインでは、このジェネシスブロックにSatoshi Nakamotoがイギリスの新聞の一面であった銀行救済の見出しをメモとして残したのは有名な話です。ブロック作成の具体的な流れは以下のようになります(②-2)。まず、最初にブロックチェーンの一番最初のブロックとなるジェネシスブロックが作成されます(①-1-4)。

  1. 以下の情報を取得し、ブロックを生成
    1. 直前のブロック(ラストブロック)から生成したハッシュ値
    2. 直近ブロックのプルーフ
  2. プルーフオブワーク(①-4)
    1. ある定数のプルーフを含めたブロックを生成し、ハッシュ値を計算(①-5)
    2. ハッシュが条件(先頭4つが’0’など)に合うか照合(①-6)
    3. 条件に合わなければプルーフを変更し、再度1から実行
    4. ※ Qiitaのコードでは、直近ブロックのハッシュ値とプルーフに次コードのプルーフを取り込み、ハッシュ値を算出し、そのハッシュ値を算出した後に、ブロックを生成する流れを取っています。
  3. コインベースの実行
  4. ブロックの生成(①-7)
  5. ※ プルーフを見つけた時の各ノードへの一斉通知などは省略しています。
#②-2 マイニングアルゴリズム(※一般的なものとは少し違うと思われる)
@app.route('/mine', methods=['GET'])
def mine():
    last_block = blockchain.last_block             #(リスト)直前のブロック情報の取得(①-2)
    proof = blockchain.proof_of_work(last_block)   #(int)プルーフオブワークの実行(①-4)

    blockchain.new_transaction(                    #コインベースの実行(マイニング成功者への報酬)
        sender="0",                                #「sender=0」でコインベースからの報酬であることを表す
        recipient=node_identifier,                 #受け手のアドレス
        amount=1,                                  #報酬
    )

    previous_hash = blockchain.hash(last_block)         #(str)直前のハッシュ値の計算(①-5)
    block = blockchain.new_block(proof, previous_hash)  #(dict)新しいブロックの生成(①-7)

    response = {
        'message': "New Block Forged",
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200

class Blockchain:
    #①-1 コンストラクタ
    def __init__(self):
        self.current_transactions = []    #①-1-1(リスト) ブロックに格納されていないトランザクションリスト
        self.chain = []                   #①-1-2(リスト) ブロックチェーンを格納するリスト

        self.new_block(previous_hash='1', proof=100)  #①-1-4 ジェネシスブロック

    ・・・(省略)・・・

    #<マイニング用メソッド>
    #①-4 プルーフオブワーク用クラス関数
    def proof_of_work(self, last_block):
        """
        作業内容:シンプルなプルーフオブワークアルゴリズム。条件を満たすプルーフを探す
             ※条件は①-6で指定
        ・具体的なフロー:⑴⑵⑶をそれぞれ求める
                   → ⑴⑵⑶を結合しハッシュ値を求める(①-5で実行)
                     → ハッシュ値の最初の4つが0か確認する(①-6で実行)
                     → 条件を満たすまで③のプルーフをなんども変更して再計算
                     ※⑴直前のブロックのプルーフ
                      ⑵直前のブロックのハッシュ値
                      ⑶今回のプルーフ
        :param last_block: (dict)last Block(from ②-2)
        :return: <int>proof
        """
        last_proof = last_block['proof']       #(int)⑴直前のブロックのプルーフ
        last_hash = self.hash(last_block)      #(str)⑵直前のハッシュ値の算出(①-5)

        proof = 0                              #(int)以下3段でプルーフを探索
        while self.valid_proof(last_proof, proof, last_hash) is False:  #①-6の戻り値
            proof += 1

        return proof

    #①-5 ハッシュ化用クラスメソッド
    @staticmethod
    def hash(block):
        """
        作業内容:ブロックのSHA-256ハッシュ値を作る
        :param block: (dict)ブロック(from ①-4)
        :return : (str)16進数のハッシュ値
        """

        block_string = json.dumps(block, sort_keys=True).encode()  #(byte(文字列のブロック➡︎Json文字列➡︎バイト型の流れ
         #※ソートすることでハッシュ値に一貫性を持たせる('sort_keys=True'辞書型データをキーでソート)
        return hashlib.sha256(block_string).hexdigest()

    #①-6 有効なプルーフを探すクラスメソッド
    @staticmethod
    def valid_proof(last_proof, proof, last_hash):
        """
        作業内容:条件を満たすハッシュ値を探す
        ・条件:ハッシュ値の最初の4つが0になる
        :param last_proof: (int)直前のブロックのプルーフ(from ①-4)
        :param proof: (int)プルーフ(from ①-4)
        :param last_hash: (str)直前のブロックのハッシュ値(from ①-4)
        :return: (bool)bool値を返す
        """
        guess = f'{last_proof}{proof}{last_hash}'.encode()   #(byte)⑴⑵⑶を連結してbyte型に変更
        guess_hash = hashlib.sha256(guess).hexdigest()       #(str)ハッシュ値の算出
        return guess_hash[:4] == "0000"

    #①-7 ブロック生成用クラス関数
    def new_block(self, proof, previous_hash):
        """
        作業内容:新しいブロックを作る
        :param proof: (int)プルーフオブワークの戻り値(from ②-2)
        :param previous_hash: (str)直前のブロックのハッシュ値(②-2)
        :return: (dict)新しいブロック
        """

        block = {                                       #辞書型で記述
            'index': len(self.chain) + 1,               #インデックス
            'timestamp': time(),                        #タイムスタンプ
            'transactions': self.current_transactions,  #トランザクション情報の取得(from ①-1)
            'proof': proof,                             #プルーフ
            'previous_hash': previous_hash or self.hash(self.chain[-1]),  #ハッシュ値(①-5)  
        }

        self.current_transactions = []     #新しいブロック用のトランザクションリストを初期化(①-1-1)

        self.chain.append(block)           #ブロックチェーンの末尾に新しいブロックを追加(①-1-2)
        return block    

おそらく通常のハッシュ値の算出は、現時点までのトランザクション、1つ前のハッシュ値、プルーフを取り込んでブロックを作ってから、ハッシュ値を算出し、算出できるまでブロックを壊しては作り直すというやり方をしていると思います。Qiitaの記事は若干イレギュラーなやり方をしています。この点はAidemyの口座をやるとよく理解できると思います。

2.6 【3】コンセンサスアルゴリズム

続いてはコンセンサスアルゴリズムです。次のような流れになります(②-3,①-8)。
※自身とつながっている全てのノードに対して以下のアルゴリズムを実行します。

  1. ノードからブロックチェーン情報を取得する
  2. 先頭のブロックから以下の確認を行う(①-9)
    1. 自分よりもチェーンが長いか
    2. →長くない場合は次のノードへ
    3. 1つ前のブロックから計算で算出したハッシュ値と現在ブロックに含まれているハッシュ値が等しいか
    4. ブロックに入っているプルーフを使って求めたハッシュ値が条件を満たすハッシュ値になっているか
    5. 全てがYesの場合、自分のチェーンと置き換え
#②-3 コンセンサス・アルゴリズム
@app.route('/nodes/resolve', methods=['GET'])
def consensus():
    replaced = blockchain.resolve_conflicts()        #コンセンサスアルゴリズムを実行(①-8)
    if replaced:                                     #置き換えがあった場合(①-8がTrueを返す場合)
        response = {
            'message': 'Our chain was replaced',
            'new_chain': blockchain.chain
        }
    else:                                            #置き換えがなかった場合
        response = {
            'message': 'Our chain is authoritative',
            'chain': blockchain.chain
        }
    return jsonify(response), 200


class Blockchain:

    ・・・(省略)・・・

    #<コンセンサスアルゴリズム用メソッド>
    #①–8 コンセンサスアルゴリズム
    def resolve_conflicts(self):
        """
        作業内容:他のノードからブロックチェーンをダウンロードし、長いものに置き換える
        :return: (bool)ブロックチェーンが置き換わったばいはTrue、そうでない場合はFalse
        """
        neighbours = self.nodes        #(セット)登録されたノードの情報を変数に格納(①-1-3)
        new_chain = None               #(None)チェーン比較用の変数

        max_length = len(self.chain)   #(int)自身のブロックチェーンの長さを格納(①-1-2)

        for node in neighbours:        #登録されている全てのノードを順番に以下の命令を実行
            response = requests.get(f'http://{node}/chain')  #(Response)比較するノード情報取得
            
            if response.status_code == 200:         #情報が取得できた場合
                length = response.json()['length']  #(int)比較ノードのlengthを格納
                chain = response.json()['chain']    #(int)比較ノードのchainを格納

                if length > max_length and self.valid_chain(chain):     #ブロックチェーン確認(①-9)
                    max_length = length
                    new_chain = chain

        if new_chain:        #自分のとは異なる有効なブロックチェーンがあれば自分のものと置き換え
            self.chain = new_chain
            return True

        return False         #置き換えなかった場合は、Falseを返す

    #①-9 ブロックチェーンが正しいか確認するクラスメソッド
    def valid_chain(self, chain):
        """
        作業内容:ブロックチェーンが正しいか確認する
                ①ハッシュ②プルーフの2点
        :param chain: (dict)ブロックチェーン(from ①-4)※検証先のノードから持ってきたチェーン
        :return: (bool)有効ならTrue、無効ならFalse
        """
        last_block = chain[0]       #(リスト)ブロックチェーンの1つ目から確認
        current_index = 1           #(int)

        while current_index < len(chain):  #「インデックス番号=チェーンの長さ」となるまで下記実行文の繰り返し
            block = chain[current_index]   #<リスト>現在のブロック(last_blockはこの1つ目のブロック)
            print(f'{last_block}')
            print(f'{block}')
            print("\n-----------\n")

            if block['previous_hash'] != self.hash(last_block): #ハッシュ値の照合
                return False                                    #一致しない場合

            if not self.valid_proof(last_block['proof'], block['proof'],  #プルーフを照合(①-6)
                                    last_block['previous_hash']):
                return False
            last_block = block     #1つのブロックの照合が問題なく終わったら、次のブロックの照合へ進む
            current_index += 1
            
        return True                #全部のブロックが有効ならTrueを返す

2.7 ノードの追加

最後にノード追加部分のコードです(②-4)。

  1. POSTでノード情報を受け取る
  2. URLを解析し、有効なURLか確認し、有効であれば、ノードに追加(①-10)
#②-4 ノード登録メソッド
@app.route('/nodes/register', methods=['POST'])
def register_nodes():
    values = request.get_json(force=True)        #POSTされたJsonファイルを解析(mimeタイプで判断)
    
    nodes = values.get('nodes')                  #values→Responseオブジェクト
    if nodes is None:                            #ノードがない場合
        return "Error: Please supply a valid list of nodes", 400

    for node in nodes:                           #ノードがある場合、ノードを登録
        blockchain.register_node(node)           #ノードに登録する(①-10)

    response = {
        'message': 'New nodes have been added',
        'total_nodes': list(blockchain.nodes),
    }
    return jsonify(response), 201


class Blockchain:

    ・・・(省略)・・・

    #<ノードの登録用メソッド>
    #①-10 新しいノードの登録
    def register_node(self, address):
        """
        作業内容:ノードリストに新しいノードリストを登録する
        :param address: (str)ノードのアドレス(from ②-4)
                        例)'http://192.168.0.5:5000'
        :return : None
        """
        parsed_url = urlparse(address)               #(タプル)addressを解析
        if parsed_url.netloc:                        #netloc属性値
            self.nodes.add(parsed_url.netloc)        #①-1-3
        elif parsed_url.path:                        #path属性値('192.168.0.5:5000'など)
            self.nodes.add(parsed_url.path)          #①-1-3
        else:                                         #それ以外の場合
            raise ValueError('Invalid URL')           #ValueErrorを返す

2.8 コードの整理

最後にブロックチェーン全体を見るためのコードとこのブロックチェーンシステムを動かすためのコードです(Flaskについては別記事で書く予定です)。

#②-5 全てのブロックチェーンを返すメソッド
@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,          #ブロックチェーンの一覧(①-1-2)
        'length': len(blockchain.chain),    #ブロックチェーンの長さ(①-1-2)
    }
    return jsonify(response), 200

#②-6 起動用
if __name__ == '__main__':
    from argparse import ArgumentParser     #以下の5行でコマンドラインでノードを増やせるようにしている。
    parser = ArgumentParser()
    parser.add_argument('-p', '--port', default=5000, type=int, help='port to listen on')
    args = parser.parse_args()
    port = args.port

    app.run(host='0.0.0.0', port=port, debug=True)

2.9 関連知識

以下が私がこのコードを理解する上で、なんのことかさっぱりわからずググったものです。Flaskとハッシュ関数については別記事で書こうと思います。

  • nnidモジュール・・・固有の識別子であるUUIDを作るためのモジュール
  • flask.jsonify・・・MIMEタイプapplication/jsonのResponseオブジェクトに変換する
  • flask.request.get_json()・・・POSTされたjsonファイルを解析し返す(jsonでない場合はNoneを返す)。MIMEタイプで判断。
  • force引数…Trueの場合、MIMEタイプは無視される
  • urllibパッケージ・・・URLを扱う幾つかのモジュールを集めたパッケージ
  • urllib.perseモジュール・・・URLを解析して構成要素にするモジュール
  • urllib.perse.urlperse()・・・URLを解析し、6つの構成要素にしてタプルで返す。
  • netloc属性を持ち、ネットワーク上での位置を表す(/以下のサーバー内のリソースの位置は格納されない)
  • netloc属性に何も入らない場合は、空文字列を返す
  • format文字列・・・文字列前の「f」はformat文字列を表し、文字列内の{変数名}に値が入る
  • requests.get()・・・GETメソッドを送信し、レスポンスを返す
  • requests.get().status_code…レスポンスのステータスコードを表す属性
  • argparseモジュール・・・コマンドライン引数の解析モジュール
    • argparse.ArgumentParser()・・・AugumentParserオブジェクトを生成。コマンドラインで「–help」引数が使えるようになる
    • ArgumentParserオブジェクト.add_argument(引数)・・・コマンドラインで引数を受け取って指定する。sys.argsのような機能。ArgumentParser.parse_args()が呼び出された時に実行される。

3 まとめ

このブロックチェーンコードをPOSTMANで実行してもらうと、ブロックチェーンが体感できると思います。POSTMANはインストールして簡単に使うことができますので、使ってみてください。使い方はこの記事では割愛していますが、Qiitaの記事のスクリーンショット通りに入力すればうまくいくはずです。ただ、コンセンサスアルゴリズムは、1つのパソコンで2つのノードを走らせる方法が未だわからず、実行しきれていません・・・笑

ブロックチェーンは、初めてプログラミングやパソコンを扱う時と同じように、その仕組みというよりはその「用語」に惑わされることが多いと思います。どれも案外大した意味を持っているわけではないので、この記事のざっくり定義を参考にして頂ければ、少しはつかめるのではないかと思います。この1つの記事を理解するの、約4ヶ月も(笑)かかりましたが、WEBの仕組みなども含めてかなり勉強になりました(そのうち、3ヶ月くらいFlaskに費やしていました・・・)。引き続き、細かい部分も勉強していき、場合によっては記事を全面改定するくらいのこともありかなとは思います。

一方で、スマートコントラクトやそれを利用した分散型アプリケーションなどの作成にも興味あるので、そちらにも進んでいきたいなとも思っています。今回のようなブログの作成や今後やって見たいと思ってアプリケーションの作成など、試行錯誤の機会を増やしていきたいと思います。長い記事、駄文ではありますが、読んで頂いた方、ありがとうございます。至らぬ点やブロックチェーンなどにまつわるオススメのサイトがあれば、教えて頂けると嬉しいです。

4 参考サイト等

Pythonのクラス文とハッシュ関数についての記事も書いているので、参考までに載せておきます。