簡単なブロックチェーンをつくる(その9 ノードの同期)

 各ノードのブロックチェーンは /block により別々にブロックが生成される.そこで同期をとる時点で一番長いブロックをもつブロックチェーンを「真の」ブロックチェーンをみなすことにする.その他のノードのブロックチェーンは一番長いブロックチェーンに置き換える.

import sys, hashlib, json
import requests

from time import time
from flask import Flask, jsonify, request
from urllib.parse import urlparse

app = Flask(__name__)

class Blockchain():
    # 別のノードのアドレスのリスト
    nodes = []

    # ハッシュ値の上4文字が0000となるノンスを見つける
    difficulty = 4
    target = '0000'

    def __init__(self):
        # ブロックチェーン本体
        self.blockchain = []
        # 1ブロックのトランザクションリスト
        self.tx_list = []
        # ジェネシスブロックの"前ブロック"のハッシュ値
        self.prev_block_hash = hashlib.sha256("genesis".encode()).hexdigest()
        # ジェネシスブロック
        block = {
            'index': 0,
            'timestamp': time(),
            # コインベーストランザクション: ノード所有者に1000コインを送付
            'tx_list': [{'sender': '0', 'recipent': node_owner, 'amount': 1000}],
            'nonce': 0,
            'prev_block_hash': self.prev_block_hash
        }
        # ジェネシスブロックのノンスを計算
        block['nonce'] = self.proof_of_work(block)
        # ジェネシスブロックをブロックチェーンに追加
        self.blockchain.append(block)

    # プルーフオブワーク
    # ターゲットの条件を満たすハッシュ値を与えるノンスを見つける
    def proof_of_work(self, block):
        while True:
            hash = hashlib.sha256(json.dumps(block, sort_keys=True).encode()).hexdigest()
            if hash[:self.difficulty] == self.target:
                # 見つけたブロックのハッシュ値(次のブロックに必要)
                self.prev_block_hash = hash
                break
            block['nonce'] += 1

        return block['nonce']

    # 他のノードとブロックチェーンを比較
    # 最も長いブロックチェーンに合わせる
    def sync(self):
        new_blockchian = None

        max_length = len(self.blockchain)

        for node in self.nodes:
            k = requests.get(f'http://{node}/blockchain')
            if k.status_code == 200:
                chain_body = k.json()['blockchain']
                length = len(chain_body)
                if length > max_length:
                    max_length = length
                    new_blockchian = chain_body

        if new_blockchian:
            self.blockchain = new_blockchian
            return True

        return False

# ノードの所有者
node_owner = sys.argv[2]
print("node owner=", node_owner)

# ブロックチェーンインスタンス
bc = Blockchain()

# ブロックチェーンを返す
@app.route("/blockchain", methods=['GET'])
def get_blockchain():
    responce = {'blockchain': bc.blockchain}
    return jsonify(responce), 200


# ノード間のブロックチェーンの同期
@app.route("/nodes/sync", methods=['GET'])
def node_sync():
    updated = bc.sync()
    if updated:
        responce = {
            'message': 'updated',
            'blockchain': bc.blockchain
        }
    else:
        responce = {
            'message': 'already latest',
            'blockchain': bc.blockchain
        }
    return jsonify(responce), 200


# マイニングによるブロック生成
@app.route("/mine", methods=["GET"])
def mine():
    # マイナー報酬のコインベーストランザクションをトランザクションリスト
    # の先頭に追加
    # 受領者はnode_owner
    global node_owner
    tx = {
        'sender': '0',
        'recipient': node_owner,
        'amount': 1000
    }
    bc.tx_list.insert(0, tx)

    # マイニング
    block = {
        'index': len(bc.blockchain),
        'tiemstamp': time(),
        'tx_list': bc.tx_list,
        'nonce': 0,
        'prev_block_hash': bc.prev_block_hash
    }
    block['nonce'] = bc.proof_of_work(block)

    # 次のブロック用に現在のブロックのハッシュを計算しておく
    bc.prev_block_hash = hashlib.sha256(json.dumps(block, sort_keys=True).encode()).hexdigest()

    # トランザクションリストをブロックに入れたのでクリア
    bc.tx_list = []

    # ブロックチェーンにブロックを追加
    bc.blockchain.append(block)

    return jsonify(bc.blockchain), 200

# トランザクション生成
@app.route("/tx", methods=['POST'])
def tx():
    tx = request.get_json()

    fields = ['sender', 'recipient', 'amount']
    if (not all(k in tx for k in fields)) or (tx['amount'] <= 0):
        message = 'Input Error'
        status_code = 400
    else:
        bc.tx_list.append(tx)
        message ='Tx added'
        status_code = 201
        print(bc.tx_list)

    responce = {'message': message}
    return jsonify(responce), status_code

# 他のノードの存在を伝える
@app.route('/nodes/add', methods=['POST'])
def add_nodes():
    values = request.get_json()
    address = values.get('nodes')

    if address is None:
        responce = {
            'message': 'Missing node'
            }
        return jsonify(responce), 400

    for k in address:
        url = urlparse(k)
        bc.nodes.append(url.netloc)

    responce = {
        'message': 'New nodes added',
        'nodes': bc.nodes
    }
    return jsonify(responce), 201

# host: localhost
if __name__ == "__main__":
    app.run(port=int(sys.argv[1]))

複数ノードでプログラムを動かし,/node/add でそれぞれ別ノードの存在を伝えておく.同期を取りたいときは /node/sync を実行する.sync関数が各ブロックチェーンのブロックの長さを比較する関数で,一番長いものを当該ノードのブロックチェーンにコピーする.まるごとコピーするのでブロックチェーンが長くなると非効率になるが,とりあえずはよしとする.別ノードのブロックチェーンをもってくる,/blockchain も新たに設けた.

 実行例として,2つのノードを立ち上げる.

> python hoge.py 5000 Sato

> python hoge.py 5001 Yamada

ノードの存在を伝え,

> curl -X POST -H "Content-Type: application/json" -d "{\"nodes\": [\"http://localhost:5001\"]}" "http://localhost:5000/nodes/add" | jq

> curl -X POST -H "Content-Type: application/json" -d "{\"nodes\": [\"http://localhost:5000\"]}" "http://localhost:5001/nodes/add" | jq

ポート5000のノードはトランザクションを1回行い,マイニングも1回行う.

> curl -X POST -H "Content-Type: application/json" -d "{\"sender\": \"Sato\", \"recipient\": \"Watanabe\", \"amount\": 123}" "http://localhost:5000/tx" | jq

> curl http://localhost:5000/mine | jq

この時点は5000のブロックチェーンの長さは2,5001の長さは1である.

> curl http://localhost:5000/blockchain |jq

{
  "blockchain": [
    {
      "index": 0,
      "nonce": 74845,
      "prev_block_hash": "aeebad4a796fcc2e15dc4c6061b45ed9b373f26adfc798ca7d2d8cc58182718e",
      "timestamp": 1624965321.6862228,
      "tx_list": [
        {
          "amount": 1000,
          "recipent": "Sato",
          "sender": "0"
        }
      ]
    },
    {
      "index": 1,
      "nonce": 79312,
      "prev_block_hash": "00007b46484e80d57f9ad5b4850f2b836d9a3ea89ddf233cd119ed9f95c21d28",
      "tiemstamp": 1624965354.314841,
      "tx_list": [
        {
          "amount": 1000,
          "recipient": "Sato",
          "sender": "0"
        },
        {
          "amount": 123,
          "recipient": "Watanabe",
          "sender": "Sato"
        }
      ]
    }
  ]
}

> curl http://localhost:5001/blockchain |jq

{
  "blockchain": [
    {
      "index": 0,
      "nonce": 13833,
      "prev_block_hash": "aeebad4a796fcc2e15dc4c6061b45ed9b373f26adfc798ca7d2d8cc58182718e",
      "timestamp": 1624965323.9136956,
      "tx_list": [
        {
          "amount": 1000,
          "recipent": "Yamada",
          "sender": "0"
        }
      ]
    }
  ]
}

ここで5000の上で同期を取ると

> curl http://localhost:5000/nodes/sync |jq

{
  "blockchain": [
    {
      "index": 0,
      "nonce": 74845,
      "prev_block_hash": "aeebad4a796fcc2e15dc4c6061b45ed9b373f26adfc798ca7d2d8cc58182718e",
      "timestamp": 1624965321.6862228,
      "tx_list": [
        {
          "amount": 1000,
          "recipent": "Sato",
          "sender": "0"
        }
      ]
    },
    {
      "index": 1,
      "nonce": 79312,
      "prev_block_hash": "00007b46484e80d57f9ad5b4850f2b836d9a3ea89ddf233cd119ed9f95c21d28",
      "tiemstamp": 1624965354.314841,
      "tx_list": [
        {
          "amount": 1000,
          "recipient": "Sato",
          "sender": "0"
        },
        {
          "amount": 123,
          "recipient": "Watanabe",
          "sender": "Sato"
        }
      ]
    }
  ],
  "message": "already latest"
}

となり,何も起こらない.5001の上で同期を取ると

> curl http://localhost:5001/nodes/sync |jq

{
  "blockchain": [
    {
      "index": 0,
      "nonce": 74845,
      "prev_block_hash": "aeebad4a796fcc2e15dc4c6061b45ed9b373f26adfc798ca7d2d8cc58182718e",
      "timestamp": 1624965321.6862228,
      "tx_list": [
        {
          "amount": 1000,
          "recipent": "Sato",
          "sender": "0"
        }
      ]
    },
    {
      "index": 1,
      "nonce": 79312,
      "prev_block_hash": "00007b46484e80d57f9ad5b4850f2b836d9a3ea89ddf233cd119ed9f95c21d28",
      "tiemstamp": 1624965354.314841,
      "tx_list": [
        {
          "amount": 1000,
          "recipient": "Sato",
          "sender": "0"
        },
        {
          "amount": 123,
          "recipient": "Watanabe",
          "sender": "Sato"
        }
      ]
    }
  ],
  "message": "updated"
}

となり,ブロックチェーンが更新される.