プログラミングを通して知るビットコイン・ブロックチェーン

最近色々な意味でまた騒がれている暗号通貨ですが、技術は非常に面白そうということで、前回は論文を通して P2P ビットコイン取引システムについて学びました。

しかし、あれを読んだだけでは処理部分がよくわからん、と。ブロックチェーンの概念はわかったけれども、詳細な仕組みはどうなってるんだろう、と。

そこで、今回はブロックチェーンを実装・構築することでビットコインの仕組みについてより知識を深めたいと思います。

最初に

使う言語は Python (3.6) です。参考にした記事が Python で書いてたし、一番欠けるのが Python なので都合がいい。

あと、Flask も使います。これも Web アプリ開発で触ってるので都合がいい。素晴らしい。

参考にしたのは、Qiita で今人気のこの記事です。ソースコードはコピペです。

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

ブロックとは

最初にトランザクションを含んでいるブロックとはどのような構造なのか、例を見てみます。

block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

index, timestamp はわかるとして、そのほかを見てみます。

transaction に含まれるのが、送信者、受信者のアドレスと送信するビットコインの量。previous_hash がハッシュ値。proof は PoW で求められた値 Nonce です。これを見るだけでわくわくしますね。(強制)

previous_hash を含んでいることが重要な点で、この値を含んだ状態でこのブロックでハッシュ値を求め、その値を次のブロックに送信します。これを繰り返すことで、どこかのブロックでハッシュ値が書き換えられたらその後のブロックもハッシュ値を書き換えないと不整合が起きるという構造になるわけです。

ブロックチェーン構築

基本構造

# coding: UTF-8

class Blockchain(object):
    def __init__(self):
        self.chain = []
        self.current_transactions = []

    def new_block(self):
        # 新しいブロックを作り、チェーンに加える
        pass

    def new_transaction(self):
        # 新しいトランザクションをリストに加える
        pass

    @staticmethod
    def hash(block):
        # ブロックをハッシュ化する
        pass

    @property
    def last_block(self):
        # チェーンの最後のブロックをリターンする
        pass

説明はコメントの通り。

トランザクションの追加

class Blockchain(object):

    def new_transaction(self, sender, recipient, amount):
        """
        次に採掘されるブロックに加える新しいトランザクションを作る
        :param sender: <str> 送信者のアドレス
        :param recipient: <str> 受信者のアドレス
        :param amount: <int> 量
        :return: <int> このトランザクションを含むブロックのアドレス
        """

        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

ブロックの説明で見た、トランザクションの部分です。論文を見てわかる通り、一つのブロックには複数のトランザクションが含まれているので、current_transaction はリスト型になっています。

ブロックの作成

import hashlib
import json
from time import time


class Blockchain(object):
    def __init__(self):
        self.current_transactions = []
        self.chain = []

        # ジェネシスブロックを作る
        self.new_block(previous_hash=1, proof=100)


    def new_block(self, proof, previous_hash=None):
        """
        ブロックチェーンに新しいブロックを作る
        :param proof: <int> プルーフ・オブ・ワークアルゴリズムから得られるプルーフ
        :param previous_hash: (オプション) <str> 前のブロックのハッシュ
        :return: <dict> 新しいブロック
        """

        block = {
            'index': len(self.chain) + 1,
            'timestamp': time(),
            'transactions': self.current_transactions,
            'proof': proof,
            'previous_hash': previous_hash or self.hash(self.chain[-1]),
        }

        # 現在のトランザクションリストをリセット
        self.current_transactions = []

        self.chain.append(block)
        return block


    @property
    def last_block(self):
        return self.chain[-1]


    @staticmethod
    def hash(block):
        """
        ブロックの SHA-256 ハッシュを作る
        :param block: <dict> ブロック
        :return: <str>
        """

        # 必ずディクショナリがソートされている必要がある
        # そうでないと、一貫性のないハッシュとなる
        block_string = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()

ジェネシスブロックとは、ブロックチェーンを構成する最初のブロックを指します。

Proof-of-Work (PoW)

ハッシュ値が一定数の 0 で終わる Nonce を求める処理が PoW で、この 0 の数をいじることで演算の難易度を調整することができます。Nonce 自体をプルーフといったり PoW といったりするときもあります。

論文には PoW の仕組みは書いてあるものの、これ自体が値なのかアルゴリズムなのか明確に書かれていなかったのですが、たぶんそんなに重要ではない。

ここで重要なのは、演算は難しく、求めた値が正解か確認するのは簡単であるということ。例えば、RSA 暗号においても平文を暗号化するのは簡単ですが、公開されている情報を基に素数を求めるのは不可能です。この非対称性がセキュリティを高めている、と。

で、この PoW を実装すると次のようになります。

from uuid import uuid4

class Blockchain(object):
    ...

    def proof_of_work(self, last_proof):
        """
        シンプルなプルーフ・オブ・ワークのアルゴリズム:
         - hash(pp') の最初の4つが0となるような p' を探す
         - p は前のプルーフ、 p' は新しいプルーフ
        :param last_proof: <int>
        :return: <int>
        """

        proof = 0
        while self.valid_proof(last_proof, proof) is False:
            proof += 1

        return proof

    @staticmethod
    def valid_proof(last_proof, proof):
        """
        プルーフが正しいかを確認する: hash(last_proof, proof)の最初の4つが0となっているか?
        :param last_proof: <int> 前のプルーフ
        :param proof: <int> 現在のプルーフ
        :return: <bool> 正しければ true 、そうでなれけば false
        """

        guess = f'{last_proof}{proof}'.encode()
        guess_hash = hashlib.sha256(guess).hexdigest()

        return guess_hash[:4] == "0000"

蛇足ですが、valid_proof() 内の f'{last_proof}{proof}' のコーディングについて見かけたことがなく、調べたところ f 文字列というものが Python 3.6 から追加されたようです。これによって文字列内に式を埋め込むことなどが可能になりました。

処理内容から、前の Nonce と次の Nonce を繋げたものをハッシュ化し、0 の個数が条件に合えばそのハッシュ値が Nonce となり、その Nonce が次の Nonce とハッシュ化されて…という流れになっていることがわかります。

サーバサイド

ブロックチェーンの部分は大体完成したので、サーバ側を用意します。

基本構造

from textwrap import dedent
from flask import Flask, jsonify


class Blockchain(object):
    ...


app = Flask(__name__)

# このノードのグローバルにユニークなアドレスを作る
node_identifier = str(uuid4()).replace('-', '')

# ブロックチェーンクラスをインスタンス化する
blockchain = Blockchain()


# /transactions/newエンドポイントを作る
@app.route('/transactions/new', methods=['POST'])
def new_transactions():
    return '新しいトランザクションを追加します'


# /mineエンドポイントを作る
@app.route('/mine', methods=['GET'])
def mine():
    return '新しいブロックを採掘します'


# フルのブロックチェーンをリターンする/chainエンドポイントを作る
@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200


if __name__ == '__main__':
    app.run()

トランザクションの追加

Blockchain クラスを利用して書いていきます。

from flask import Flask, jsonify, request


@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    values = request.get_json()

    # 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'])

    response = {'message': f'トランザクションはブロック {index} に追加されました'}
    return jsonify(response), 201

マイニング部分の実装

@app.route('/mine', methods=['GET'])
def mine():
    # 次のプルーフを見つけるためプルーフ・オブ・ワークアルゴリズムを使用する
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # プルーフを見つけたことに対する報酬を得る
    # 送信者は、採掘者が新しいコインを採掘したことを表すために"0"とする
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifire,
        amount=1,
    )

    # チェーンに新しいブロックを加えることで、新しいブロックを採掘する
    block = blockchain.new_block(proof)

    response = {
        'message': '新しいブロックを採掘しました',
        'index': block['index'],
        'transactions': block['transactions'],
        'proof': block['proof'],
        'previous_hash': block['previous_hash'],
    }
    return jsonify(response), 200

PoW を実行し、Nonce を求めた人に通貨を発行するトランザクションが発生します。

論文では、ブロック内の最初のチェーンが当該トランザクションになると説明がありましたが、これだと当該トランザクションがチェーンの一番後ろに来ます。PoW のときにハッシュ値の最初の数 bit が 0 という説明においても、ここでは最後の数 bit が 0 になるとしているので、最初と最後の概念が自分の思っているのと逆なのか…?

それはともかく、通貨発行のトランザクションをチェーンに追加し、ブロックを作成します。17 行目について、これは最初のブロックなので blockchain.new_block(proof) となっていますが、この後のブロック作成では blockchain.new_block(proof, previous_hash='xxxx') となるのでしょうか。

コンセンサス

あるノードが求めた Nonce が正しいのかを他のノードが確かめる仕組みと、チェーンの長さから正当なチェーンを探す仕組みを実装します。

後者に関して、ネットワーク上ではブロックが全ノードに到達することを保証しないので、各ノードが持つチェーンの長さにばらつきが出ることがありますが、ここで修正されていきます。

from urllib.parse import urlparse


class Blockchain(object):
    def __init__(self):
        ...
        self.nodes = set()
        ...

    def register_node(self, address):
        """
        ノードリストに新しいノードを加える
        :param address: <str> ノードのアドレス 例: 'http://192.168.0.5:5000'
        :return: None
        """

        parsed_url = urlparse(address)
        self.nodes.add(parsed_url.netloc)

    def valid_chain(self, chain):
        """
        ブロックチェーンが正しいかを確認する

        :param chain: <list> ブロックチェーン
        :return: <bool> True であれば正しく、False であればそうではない
        """

        last_block = chain[0]
        current_index = 1

        while current_index < len(chain):
            block = chain[current_index]
            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']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self):
        """
        これがコンセンサスアルゴリズムだ。ネットワーク上の最も長いチェーンで自らのチェーンを
        置き換えることでコンフリクトを解消する。
        :return: <bool> 自らのチェーンが置き換えられると True 、そうでなれけば False
        """

        neighbors = self.nodes
        new_chain = None

        # 自らのチェーンより長いチェーンを探す必要がある
        max_length = len(self.chain)

        # 他のすべてのノードのチェーンを確認
        for node in neighbors
            response = requests.get(f'http://{node}/chain')

            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']

                # そのチェーンがより長いか、有効かを確認
                if length > max_length and self.valid_chain(chain):
                    max_length = length
                    new_chain = chain

        # もし自らのチェーンより長く、かつ有効なチェーンを見つけた場合それで置き換える
        if new_chain:
            self.chain = new_chain
            return True

        return False

話逸れますが、コンストラクタで定義されている変数に使ってる set() 関数が初見なんですけど、すごく便利そうですね。重複した要素は勝手に取り除くし、集合演算も使えますし。

閑話休題。

エンドポイントも追加します。

@app.route('/nodes/register', methods=['POST'])
def register_node():
    values = request.get_json()

    nodes = values.get('nodes')
    if nodes is None:
        return "Error: 有効ではないノードのリストです", 400

    for node in nodes:
        blockchain.register_node(node)

    response = {
        'message': '新しいノードが追加されました',
        'total_nodes': list(blockchain.nodes),
    }
    return jsonify(response), 201


@app.route('/nodes/resolve', methods=['GET'])
def consensus():
    replaced = blockchain.resolve_conflicts()

    if replaced:
        response = {
            'message': 'チェーンが置き換えられました',
            'new_chain': blockchain.chain
        }
    else:
        response = {
            'message': 'チェーンが確認されました',
            'chain': blockchain.chain
        }

    return jsonify(response), 200

テスト

Anaconda で curl しました。ブロックチェーン全体を読み込むならこんな感じです。

$ curl http://localhost:5000/chain

Qiita にすべて載ってるので、何も苦労することはないです。逆にコピペだけで面白さはないかもしれませんが。

最後に

年が明けてからの大暴落、つい先日の Coincheck からの XEM 流出事件など、最近また悪い印象が広まっている暗号通貨界隈。成長途中の技術に対するリスクも考えながら進めていかなければならないですね。