3
3

More than 5 years have passed since last update.

4.Pythonを使ってブロックチェーンを作る

Last updated at Posted at 2019-03-13

この記事は
https://learnblockchain.cn
Learn Blockchains by Building One
を元に編集したものです。
翻訳者のTwitter Account:taro03293

この記事では、Pythonを使ってブロックチェーンを構築します。
実際に手を動かして、ブロックチェーンに対する理解を深めましょう。

仮にこの内容が理解できなくても、
ブロックチェーンは各ノードに記録されているブロックチェーンの一貫性を守るために、各ノード内で一番長いノードを見つけ、自動的にそれに置き換える
ということだけは覚えておいてください

本記事を取り組むにあたり

・Pythonへの理解
・Pythonを書ける、読めるスキル
・HTTPの基礎的な理解
が必要です

環境設定

お使いのパソコンに、事前にPython3.6+, pip , Flask, requestsをインストールしてください。

インストール方法1

・コマンドで下記を実行します
pip install Flask==0.12.2 requests==2.18.4
|
Python 3.6+ をインストールしてください
https://www.python.org/downloads/
|
pipenvをインストールしましょう
$ pip install pipenv
|
virtual envを作りましょう
$ pipenv --python=python3.6
|
pipenvをインストール
$ pipenv install
|
ノードを追加
$ pipenv run python blockchain.py
$ pipenv run python blockchain.py -p 5001
$ pipenv run python blockchain.py --port 5002

インストール方法2(Dockerで動かす)

・dockerを構築
$ docker build -t blockchain .
|
動かす
$ docker run --rm -p 80:5000 blockchain
|
ノードを追加
$ docker run --rm -p 81:5000 blockchain
$ docker run --rm -p 82:5000 blockchain
$ docker run --rm -p 83:5000 blockchain

Blockchainを構築する

blockchain.pyというファイルを作りましょう,この章で使うコードは全てそこに書いてください。

Blockchainクラスを作る
まずはBlockchain Classを繰りましょう。
コンストラクタに2つのリストを作成します。
1つはブロックチェーンを格納するためのもので、もう1つはトランザクションを格納するためのものです。

Python
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

上記のBlockchain classは、チェーンを管理し、取引を保存し、新しいブロックに追加します。
类用来管理链条,它能存储交易,加入新块等,下面我们来进一步完善这些方法。

一つ一つのブロックは
・属性(Index)
・Unixタイムスタンプ(timestamp)
・取引記録(transactions)
・仕事量の証明(Proof後程説明します)
・ひとつ前のブロックのHash値(previous_hash)
を含んでいます。

下記はブロックの構造をコード化したものです。

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

上記コードの通り、新しいブロックはその一つ前のブロックのHashを含んでいます。
これがブロックチェーンの過去ブロックは変更不可能であるということを保証しています。
もし、誰かが過去のどのブロックを変更した場合、そのブロック以降の全てのHashが無効(不正確)になります(Hashについてわからない場合は、過去の投稿を読んでください)。

取引を追加する

続けて、取引を追加しましょう。
下記のコードでnew_transactionメソッドを完成させます。

Python
class Blockchain(object):
    ...

    def new_transaction(self, sender, recipient, amount):
        """
        新しい取引記録を作り、その情報を下のマイニング待ちのブロックに追加します
        :param sender: <str> 送り手のアドレス
        :param recipient: <str> 受け手のアドレス
        :param amount: <int> 金額
        :return: <int> この取引を記録するブロックの番号(Index)
        """

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

        return self.last_block['index'] + 1

このメソッドは取引記録をリストに追加し、記録が追加されるブロック(マイニングされる次のブロック)の番号(Index)を返します。これはユーザーが取引を行うときに使うことができます。

新しいブロックを作る

ブロックチェーンがインスタンス化されたら、Genesisブロック(一番最初のブロック)を作成し、それに仕事量の証明(POW)を加える必要があります。
インスタントについての説明はこちら

各ブロックは、一般にマイニングと呼ばれる仕事量の証明が必要になります。詳細は後程説明します。
Genesisブロックを作るために、先程のnew_block(), new_transaction() 、hash()メソッドを完成させましょう。

Python
import hashlib
import json
from time import time


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

        # genesisブロックを作ります
        self.new_block(previous_hash=1, proof=100)

    def new_block(self, proof, previous_hash=None):
        """
        新しいブロックを作ります
        :param proof: <int> POWアルゴリズムによる仕事量の証明
        :param previous_hash: (Optional) <str> 前のブロックのHash
        :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

    def new_transaction(self, sender, recipient, amount):
        """
        新しい取引記録を作り、その情報を下のマイニング待ちのブロックに追加します
        :param sender: <str> 送り手のアドレス
        :param recipient: <str> 受け手のアドレス
        :param amount: <int> 金額
        :return: <int> この取引を記録するブロックの番号(Index)
        """
        self.current_transactions.append({
            'sender': sender,
            'recipient': recipient,
            'amount': amount,
        })

        return self.last_block['index'] + 1

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

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

        # Dictionaryが正しい順序であることを確認してください。
        # そうしないと、ハッシュが矛盾します。
        block_string = json.dumps(block, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()

上記のコードから、ブロックチェーンを直感的に理解できると思います。

POW(Proof Of Work:仕事量の証明)を理解する

新しいブロックは、POWアルゴリズム(Proof Of Work:仕事量の証明)によって構築されます。
POWの目的は、特定の条件を満たす数字を見つけることであり、この数字は計算で導き出すのが難しいが、数字の真偽の検証は容易である必要があります。
これがPOW(Proof Of Work:仕事量の証明)の基本概念です。

簡単な例を使って理解を深めましょう
整数X と もう一つの整数Yの積(掛け算)のハッシュ値が絶対に0で終わるとしましょう。
式に表すとhash(x * y) = ac23dc…0のようなものです。
この場合、例えばX=5に設定するとYの値はどう変化するでしょうか?

Pythonを使って計算してみましょう。

Python
from hashlib import sha256
x = 5
y = 0  # この時点でyは不明です
while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0":
    y += 1
print(f'The solution is y = {y}')

この場合(X=5の場合)、Yの値は21です。
何故なら、X=5でHash値の末尾が0で終わるYは21だからです。
hash(5 * 21) = 1253e9373e...5e3600155e860
これがPOWの簡単な概念です。

繰り返しになりますが、Bitcoinでは、Hashcashと呼ばれるPOWアルゴリズムが使用されます。
これは上記の問題と非常によく似ています。
マイニングする人たちはブロックを作成する権利を競うために計算を競いあいました。
通常、計算上の難しさは、ターゲット文字列が満たす必要がある特定の文字数に比例し、結果を計算すると、彼はビットコインの報酬を得ます。
もちろん、Web上でこの結果を確認するのはとても簡単です。

POWを実現する

POWの計算方法に似た方法を使ってPOWの理解を深めましょう。
ここでは、求めたい数をpとし、前のブロックのPOW証明を使って作られたハッシュ値が4のゼロから始まると仮定します。

Python
import hashlib
import json

from time import time
from uuid import uuid4


class Blockchain(object):
    ...

    def proof_of_work(self, last_proof):
        """
        簡単なPOW:
         - 始めの4つの数字が0になるhash(pp')を使って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)は0000から始まるか?
        :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"

アルゴリズムの複雑さを計算する方法は、最初のゼロの数を変更することです。
今回は0を4つに設定しましたが、0を追加すれば(5個、6個…)にすれば結果を計算するのにかかる時間が大幅に増えることに気が付くでしょう。

さて、Blockchainクラスは上記で完成したので、続けてHTTPリクエストを使用してWebとアプリをリンクさせます。
HTTPリクエストについて分からない場合はこちらを参照ください。

ブロックチェーンをAPIインターフェースにする

Python Flaskは初心者にもわかりやすいWebAPIフレームワークの一つで、WebからのリクエストをPython関数に簡単にマッピングすることができます。
今回は、このFlaskを使用して、Blockchainを実装してみましょう。
Flaskの詳細はYasuhiro Nakayamaさんのこちらの記事を参照にしてください。

まずは、3つのインタフェースを作成します。

/ transactions / new :取引を作成してブロックに追加します
/ mine :サーバーに新しいブロックをマイニングするように指示します
/ chain :ブロックチェーン全体を返します

ノードを作る

私たちの "Flask Server"は、ブロックチェーンネットワークのノードの一つとして機能します。
まずは、フレームワークコードを追加しましょう。

Python
import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask


class Blockchain(object):
    ...


# ノードを作る
app = Flask(__name__)

# ノードにランダムな名前を付ける
node_identifier = str(uuid4()).replace('-', '')

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

#mine GETのインターフェースを作成する
@app.route('/mine', methods=['GET'])
def mine():
    return "We'll mine a new Block"

#/transactions/new POSTのインターフェースを作成する
#インターフェースに取引情報を送ることができる
@app.route('/transactions/new', methods=['POST'])
def new_transaction():
    return "We'll add a new transaction"

# chainインターフェイスを作成して、ブロックチェーン全体を返します
@app.route('/chain', methods=['GET'])
def full_chain():
    response = {
        'chain': blockchain.chain,
        'length': len(blockchain.chain),
    }
    return jsonify(response), 200

#port=5000で動かします
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
#Portについての説明はこちらから(https://eng-entrance.com/network-port)

トランザクション(取引)を送信

ノードに送信される取引情報の構造は次のとおりです。

Python
{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

以前に説明した取引を追加するメソッドを使えば、インターフェースに基づいた取引情報を追加することも簡単です。

Python
import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4

from flask import Flask, jsonify, request

...

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

    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'Transaction will be added to Block {index}'}
    return jsonify(response), 201

マイニングを追加

ブロックチェーンにおけるマイニングとは、以下の3つのことを指します。

・POWにかかる電力を計算する
・新しい取引を追加してマイナー(自分)にコインを付与
・新しいブロックを作成してそれをチェーンに追加する

Python
import hashlib
import json

from time import time
from uuid import uuid4

from flask import Flask, jsonify, request

...

@app.route('/mine', methods=['GET'])
def mine():
    # We run the proof of work algorithm to get the next proof...
    # 次の"証明"を取得するためにPOWアルゴリズムを実行します。
    last_block = blockchain.last_block
    last_proof = last_block['proof']
    proof = blockchain.proof_of_work(last_proof)

    # 作業負荷の証明をしたノードに報酬を提供します
    # Sender="0"とは、新しくマイニングされたコインのことです
    blockchain.new_transaction(
        sender="0",
        recipient=node_identifier,
        amount=1,
    )

    # 新しいBlockを作り、チェーンに追加する
    block = blockchain.new_block(proof)

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

ここで取引を受けるのは、私たち自身のサーバーノードです。
この時点で、ブロックチェーンは完成しています。
実際に動かしてみましょう。

ブロックチェーンを動かす

APIを動かすにあたり、PostmanまたはcURLを使用します。

Postmanの場合
サーバーを起動

Python
$ python blockchain.py
* Runing on http://127.0.0.1:5000/ (Press CTRL+C to quit)

http:// localhost:5000 / mineをリクエストしてマイニングを開始
postman_get_mine.png

ポストリクエストで新しい取引を追加
postman_post_new.png

cURLの場合

$ curl -X POST -H "Content-Type: application/json" -d '{
 "sender": "d4ee26eee15148ee92c6cd394edd974e",
 "recipient": "someone-other-address",
 "amount": 5
}' "http://localhost:5000/transactions/new"

2回のマイニングで、ブロックは合計3つになります。
http:// localhost:5000 / chainをリクエストすれば、全てのブロック情報を得ることができます。

{
  "chain": [
    {
      "index": 1,
      "previous_hash": 1,
      "proof": 100,
      "timestamp": 1506280650.770839,
      "transactions": []
    },
    {
      "index": 2,
      "previous_hash": "c099bc...bfb7",
      "proof": 35293,
      "timestamp": 1506280664.717925,
      "transactions": [
        {
          "amount": 1,
          "recipient": "8bbcb347e0634905b0cac7955bae152b",
          "sender": "0"
        }
      ]
    },
    {
      "index": 3,
      "previous_hash": "eff91a...10f2",
      "proof": 35089,
      "timestamp": 1506280666.1086972,
      "transactions": [
        {
          "amount": 1,
          "recipient": "8bbcb347e0634905b0cac7955bae152b",
          "sender": "0"
        }
      ]
    }
  ],
  "length": 3
}

一貫性

ここまでで、取引記録をマイニングができる基本的なブロックチェーンを作りました。
しかし、ブロックチェーンシステムは分散式である必要があります。
分散式であるならば、一体何をもって各ノードが同じブロックチェーンを持っていると保証すれば良いのでしょうか?
これが一貫性の問題であり、ネットワーク上に複数のノードを持つためには、一貫したアルゴリズムを実装する必要があります。

注意
一貫性を実現するアルゴリズムを実装する前に、ノードに、その隣のノードを認識させる方法を見つける必要があります。
各ノードは、ネットワーク内の他のノードが含むレコードを保存する必要があります。
そのために、いくつかインターフェースを追加しましょう。

/ nodes / register :URL形式の新しいノードのリストを受け取ります。
/ nodes / resolve :ノードに正しいチェーンがあることを確認する整合性アルゴリズムを実行します。
ブロックチェーンのinit関数を修正し、登録ノードメソッドを提供します。

...
from urllib.parse import urlparse
...


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

    def register_node(self, address):
        """
        ノードリストに新しいノードを追加する
        :param address: <str> Address of node. Eg. 'http://192.168.0.5:5000'
        :return: None
        """

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

ノードを格納するためにsetを使用します。
setを使えば、ノードを繰り返し追加することを避けることができます。

一貫性アルゴリズム

ノードごとに異なるチェーンがある場合、その中で最長且つ最も効果的なチェーンを最終チェーンとします。
言い換えると、ネットワーク上で最長の有効チェーンが実際のチェーンです。

ネットワーク内で一貫性を実現するには、次のアルゴリズムを使用します。

...
import requests


class Blockchain(object)
    ...

    def valid_chain(self, chain):
        """
        与えられたブロックチェーンが有効かどうか決める
        :param chain: <list> A blockchain
        :return: <bool> True if valid, False if not
        """

        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

            # POWが正しいか確認する
            if not self.valid_proof(last_block['proof'], block['proof']):
                return False

            last_block = block
            current_index += 1

        return True

    def resolve_conflicts(self):
        """
        ノードごとに異なるチェーンがある問題(Conflict)を解決するため、
        ネットワーク上で最も長いチェーンを使う
        :return: <bool> チェーンが置き換えられた場合はtrue、そうでない場合はfalse
        """

        neighbours = self.nodes
        new_chain = None

        # 我々の持つチェーンより長いチェーンを探す
        max_length = len(self.chain)

        # #ネットワーク内のすべてのノードからチェーンを取得して検証する
        for node in neighbours:
            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

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

    nodes = values.get('nodes')
    if nodes is None:
        return "Error: Please supply a valid list of nodes", 400

    for node in nodes:
        blockchain.register_node(node)

    response = {
        'message': 'New nodes have been added',
        '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': 'Our chain was replaced',
            'new_chain': blockchain.chain
        }
    else:
        response = {
            'message': 'Our chain is authoritative',
            'chain': blockchain.chain
        }

    return jsonify(response), 200

異なるマシンでノードを実行したり、1つのマシンで異なるネットワークポートを開いてマルチノードネットワークを試したり、同じマシンで異なるポートデモを開いて実行したり、異なる端末でコマンドを実行したりして、2つのノーd-を起動できます。  :http:// localhost:5000とhttp:// localhost:5001

pipenv run python blockchain.py
pipenv run python blockchain.py -p 5001

postman_register.png

ノード2で二つのブロックをマイニングして、チェーンがさらに長いことを確認してください。
次に、ノード1で/ nodes / resolveインターフェイスにアクセスします。
このとき、一貫性アルゴリズムにより、ノード1の連鎖はノード2の連鎖に置き換えられます。(ノード2により長いブロックがあるため)

説明は以上です。理解することが多くて大変かもしれませんが、
ブロックチェーンは各ノードに記録されているブロックチェーンの一貫性を守るために、各ノード内で一番長いノードを見つけ、自動的に置き換える
ということを抑えてください。

5に続きます

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3