ブロックチェ-ンの仕組みを知るには構築するのが最短の方法
この記事を読んでいるということは、仮想通貨の拡大に興奮しているということですね。ブロックチェ-ンの仕組み、背後にある基本的なテクノロジーについて知りたいのでしょう。
しかしブロックチェ-ンを理解するのは簡単ではありません。少なくとも私にはそうでした。大量の動画の中をさまよい、抜けだらけのチュートリアルに従い、結局、実例が少なすぎてフラストレーションが大きくなりました。
私は手を動かして学ぶのが好きです。コードのレベルで内容を扱わざるを得なくなり、そうすることで身に付くからです。同じようにやってもらえば、この解説が終わる頃には、機能するブロックチェーンが出来上がり、どのように動くかがしっかりと把握できるようになるでしょう。
準備
ブロックチェ-ンとはブロックという名の不変でシーケンシャルな一連のレコードだということを覚えてください。トランザクション、ファイルその他のあらゆるデータが本当に何でも含まれています。しかし、大切なのはレコードがハッシュ値を使って連鎖しているということです。
ハッシュ値について不案内な場合はこちらに例があります。
本解説が対象としている読者。基本的なPythonの読み書きに不安がないこと。同様にHTTPのリクエストがどう動くかを理解していること。理由は本稿ではブロックチェ-ンをHTTP経由で動作させてみるためです。
必要なもの。Python3.6以上(pip
あり)がインストールされていること。さらに、FlaskとRequestsライブラリがインストールされていること。
- pip install Flask==0.12.2 requests==2.18.4
おっと、それからHTTP Clientも必要です。PostmanやcURLみたいなものですが、何でもかまいません。
最終のコードのある場所。ソースコードはこちらにあります。
ステップ1: ブロックチェ-ンの構築
お好きなテキストエディタまたはIDEを開いてください(個人的にはPyCharmが好きです❤️)。新しいファイルを作り、blockchain.py
と名付けます。使うファイルは1つだけですが、迷った場合には、いつでもこちらのソースコードを参照できます。
ブロックチェ-ンを表現する
Blockchain
クラスを作ります。このクラスのコンストラクタが初期のリストとして、ブロックチェ-ンの保管を目的とする空リスト、別にトランザクションの保管を目的とする空リストを作ります。以下がブロックチェ-ンの青写真です。
class Blockchain(object): | |
def __init__(self): | |
self.chain = [] | |
self.current_transactions = [] | |
def new_block(self): | |
# Creates a new Block and adds it to the chain | |
pass | |
def new_transaction(self): | |
# Adds a new transaction to the list of transactions | |
pass | |
@staticmethod | |
def hash(block): | |
# Hashes a Block | |
pass | |
@property | |
def last_block(self): | |
# Returns the last Block in the chain | |
pass |
本記事Blockchain
の任務はチェーンを管理することです。トランザクションを保管し、チェーンへ新たなブロックを付加する支援をします。では幾つかメソッドを付け加えるところから始めましょう。
ブロックの構成はどうなってるか
各ブロックは、インデックス、タイムスタンプ(UNIX時間)、トランザクションのリスト、プルーフ(詳しくは後ほど)、前のブロックのハッシュ値を持っています。
下記は単独のブロックの構成例です。
block = { | |
'index': 1, | |
'timestamp': 1506057125.900785, | |
'transactions': [ | |
{ | |
'sender': "8527147fe1f5426f9dd545de4b27ee00", | |
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f", | |
'amount': 5, | |
} | |
], | |
'proof': 324984774000, | |
'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" | |
} |
現時点で、チェーンの考え方が明らかでしょう。新しいブロックが前のブロックのハッシュ値を保持します。これが非常に重要です。というのも、このことによってブロックチェ-ンの不変性がもたらされるからです。つまり、攻撃者がチェーン内のブロックを破壊すると、その後に続くブロックの全てが不正なハッシュ値を持ちます。
理解できそうでしょうか。もしそうでない場合、少し時間をとってもう少し深く考えてみましょう ー これがブロックチェーンの核となっている考え方です。
ブロックにトランザクションを追加する
ブロックにトランザクションを追加する方法が必要になります。new_transaction()
メソッドが追加をしてくれて、とても簡単です。
class Blockchain(object): | |
... | |
def new_transaction(self, sender, recipient, amount): | |
""" | |
Creates a new transaction to go into the next mined Block | |
:param sender: <str> Address of the Sender | |
:param recipient: <str> Address of the Recipient | |
:param amount: <int> Amount | |
:return: <int> The index of the Block that will hold this transaction | |
""" | |
self.current_transactions.append({ | |
'sender': sender, | |
'recipient': recipient, | |
'amount': amount, | |
}) | |
return self.last_block['index'] + 1 |
new_transaction()
はリストにトランザクションを追加すると、戻り値として次のマイニングで利用されることになるトランザクションのインデックス(いまマイニングされたものの次のインデックス)を返します。これは、後程出て来るトランザクションを送信するユーザにとって役に立ちます。
新しいブロックを作成する
Blockchain
のインスタンスが生成されると、ジェネシスブロック(最祖先の、一番初めに作成されるブロック)からブロックチェーンを作成していきます。さらに、マイニングの結果となる「プルーフ」(あるいはプルーフ・オブ・ワーク)をジェネシスブロックに追加する必要があります。マイニングについては後ほど説明します。
コンストラクタではジェネシスブロックを作成する他にnew_block()
やnew_transaction()
、hash()
のメソッドを具体化する必要があります。
import hashlib | |
import json | |
from time import time | |
class Blockchain(object): | |
def __init__(self): | |
self.current_transactions = [] | |
self.chain = [] | |
# Create the genesis block | |
self.new_block(previous_hash=1, proof=100) | |
def new_block(self, proof, previous_hash=None): | |
""" | |
Create a new Block in the Blockchain | |
:param proof: <int> The proof given by the Proof of Work algorithm | |
:param previous_hash: (Optional) <str> Hash of previous Block | |
:return: <dict> New Block | |
""" | |
block = { | |
'index': len(self.chain) + 1, | |
'timestamp': time(), | |
'transactions': self.current_transactions, | |
'proof': proof, | |
'previous_hash': previous_hash or self.hash(self.chain[-1]), | |
} | |
# Reset the current list of transactions | |
self.current_transactions = [] | |
self.chain.append(block) | |
return block | |
def new_transaction(self, sender, recipient, amount): | |
""" | |
Creates a new transaction to go into the next mined Block | |
:param sender: <str> Address of the Sender | |
:param recipient: <str> Address of the Recipient | |
:param amount: <int> Amount | |
:return: <int> The index of the Block that will hold this transaction | |
""" | |
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): | |
""" | |
Creates a SHA-256 hash of a Block | |
:param block: <dict> Block | |
:return: <str> | |
""" | |
# We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes | |
block_string = json.dumps(block, sort_keys=True).encode() | |
return hashlib.sha256(block_string).hexdigest() | |
上記のコードは簡単だと思います。分かりやすくするため、コード中にコメントやdocstring (ドキュメンテーション文字列、以降 docstring)を追加しておきました。これでブロックチェーンの表現がほとんどできました。しかし、この時点であなたは次の新しいブロックがどのように作成されたり、あるいは改ざんされたりマイニングされたりするのか気になっていることでしょう。
PoWを理解する
プルーフ・オブ・ワークアルゴリズム(以降PoW)によって次の新しいブロックがブロックチェーン上に作成されたりマイニングされたりします。PoWの目的は問題解決となる数字を見つけることです。ネットワーク上の誰にとっても数字は見つけにくくなければいけませんが、認証しやすくなければなりません。あくまでもコンピュータ的な認証の話をしています。この概念がPoWの核心部になります。
理解のためにとても簡単な例を見てみましょう。
では、ある整数x
のハッシュ値はx
に別の整数y
を乗算すると末尾が0
になると決めます。数式はhash(x * y) = ac23dc...0
になります。では、簡略化した例のために、x = 5
と決めてしまいます。これを下記のようにPythonに実装します。
- from hashlib import sha256
- x = 5
- y = 0 # We don't know what y should be yet...
- while sha256(f'{x*y}'.encode()).hexdigest()[-1] != "0":
- y += 1
- print(f'The solution is y = {y}')
ここでの解答はy = 21
になります。生成されたハッシュ値の末尾が0
になっていることを確認しましょう。
- hash(5 * 21) = 1253e9373e...5e3600155e860
ビットコインにおいては、PoWアルゴリズムをハッシュ値キャッシュと呼ばれています。上記で見た簡単な例とそれほど違いはありません。これは、マイナーが新しいブロックを作成するために必要な解決策を早く手に入れようとするアルゴリズムです。一般的に、難しさの度合いは文字列で検索される文字の数によって決まります。マイナーは解決策の報酬としてコインをトランザクションで受領します。
ネットワークはマイナーの解決策を簡単に検証することができます。
基本的なPoWを実装する
同じようなアルゴリズムをブロックチェーンに実装してみましょう。ここでは上記の例に似たルールを規定します。
> 前のブロックの解決策でハッシュ値された場合、先行する4つの0
を持つハッシュ値を生成する数字pを探す。
import hashlib | |
import json | |
from time import time | |
from uuid import uuid4 | |
class Blockchain(object): | |
... | |
def proof_of_work(self, last_proof): | |
""" | |
Simple Proof of Work Algorithm: | |
- Find a number p' such that hash(pp') contains leading 4 zeroes, where p is the previous p' | |
- p is the previous proof, and p' is the new proof | |
: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): | |
""" | |
Validates the Proof: Does hash(last_proof, proof) contain 4 leading zeroes? | |
:param last_proof: <int> Previous Proof | |
:param proof: <int> Current Proof | |
:return: <bool> True if correct, False if not. | |
""" | |
guess = f'{last_proof}{proof}'.encode() | |
guess_hash = hashlib.sha256(guess).hexdigest() | |
return guess_hash[:4] == "0000" |
アルゴリズムの難しさを緩和するため、先行する0の数を調整することができます。しかし、ここでは4個で十分でしょう。先行する0を1つでも追加するだけで、解決策を見つけるのに要する時間に莫大な違いを及ぼすことに気付くと思います。
本記事のクラスがほとんど出来上がりましたので、HTTPリクエストを使ってやり取りを始める準備ができました。
ステップ2: APIとしてのブロックチェーン
ここでは、PythonフレームワークのFlaskを使用します。マイクロフレームワークで任意のPython関数をエンドポイントに簡単にマッピングしてくれます。これにより、作成したブロックチェーンのHTTPリクエストを使用してWebを介した通信が可能になります。
下記の3つのメソッドを作成します。
/transactions/new
でブロックに新しいトランザクションを作成。/mine
で新しいブロックをマイニングするようサーバに指示。/chain
で完全なブロックチェーンを返す。
Flaskを設定する
ここで使用する”サーバ”はブロックチーンのネットワーク上で単一のノードを形成します。では、コードのひな形を作成しましょう。
import hashlib | |
import json | |
from textwrap import dedent | |
from time import time | |
from uuid import uuid4 | |
from flask import Flask | |
class Blockchain(object): | |
... | |
# Instantiate our Node | |
app = Flask(__name__) | |
# Generate a globally unique address for this node | |
node_identifier = str(uuid4()).replace('-', '') | |
# Instantiate the Blockchain | |
blockchain = Blockchain() | |
@app.route('/mine', methods=['GET']) | |
def mine(): | |
return "We'll mine a new Block" | |
@app.route('/transactions/new', methods=['POST']) | |
def new_transaction(): | |
return "We'll add a new transaction" | |
@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(host='0.0.0.0', port=5000) |
上記で追加したものを簡単に説明します。
- 15行目: ノードのインスタンス化。Flaskの詳細はこちらでお読みくだい。
- 18行目: ノードの名前を無作為に作成。
- 21行目:
Blockchain
クラスのインスタンス化。 - 24–26行:
GET
リクエストである/mine
エンドポントの作成。 - 28–30行:
POST
リクエストである/transactions/new
エンドポントの作成。このエンドポイントにデータを送信するので必要となります。 - 32–38行: 完全なブロックチェーンを返す
/chain
エンドポントの作成。 - 40–41行: サーバをポート5000で実行。
トランザクションのエンドポイント
トランザクションのリクエストは下記のようになります。このjsonがユーザによってサーバに送信されます。
- {
- "sender": "my address",
- "recipient": "someone else's address",
- "amount": 5
- }
既にブロックにトランザクションを追加するクラスメソッドができていますので、残りの作業は簡単です。トランザクションを追加する関数を書きましょう。
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() | |
# Check that the required fields are in the POST'ed data | |
required = ['sender', 'recipient', 'amount'] | |
if not all(k in values for k in required): | |
return 'Missing values', 400 | |
# Create a new Transaction | |
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の算出。
- 1コインをマイナーに報酬として付与するトランザクションの追加。
- 新しいブロックの改ざんのためにそのブロックをチェーンに追加。
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... | |
last_block = blockchain.last_block | |
last_proof = last_block['proof'] | |
proof = blockchain.proof_of_work(last_proof) | |
# We must receive a reward for finding the proof. | |
# The sender is "0" to signify that this node has mined a new coin. | |
blockchain.new_transaction( | |
sender="0", | |
recipient=node_identifier, | |
amount=1, | |
) | |
# Forge the new Block by adding it to the chain | |
previous_hash = blockchain.hash(last_block) | |
block = blockchain.new_block(proof, previous_hash) | |
response = { | |
'message': "New Block Forged", | |
'index': block['index'], | |
'transactions': block['transactions'], | |
'proof': block['proof'], | |
'previous_hash': block['previous_hash'], | |
} | |
return jsonify(response), 200 |
ここでの注意点は、マイニングしたブロックの受領者はノードのアドレスだということです。また、ここで実行したことのうちほとんどはメソッドとブロックチェーンクラスのやり取りのみということにも着目してください。これで完成です。これで、ブロックチェーンとのやり取りを開始できます。
ステップ3: Blockchainとのやり取り
普通の古いcURLやPostmanを使用してネットワーク上でAPIとインタラクションを実行できます。
サーバを起動しましょう。
- $ python blockchain.py
- * http://127.0.0.1:5000/で起動(CTRL+Cで停止)
http://localhost:5000/mine
にGET
リクエストを出してブロックのマイニングをしてみましょう。
作成したトランザクション構造を持つhttp://localhost:5000/transactions/new
にPOST
リクエストを出して新しいトランザクションを作成してみましょう。
Postmanを使用していない場合は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
- }
ステップ4: 合意
ここが非常に面白いポイントです。トランザクションを受理し、新しいブロックをマイニングできるようにしてくれる基本的なブロックチェーンができています。しかし、ブロックチェーンの主な目的は分散化することにあります。では、もしブロックチェーンが分散化されているとしたら、どのようにして同じチェーンを反映しているか確認すればいいのでしょうか。これが合意問題で、ネットワークに複数のノードが欲しい場合、コンセンサスアルゴリズムを実装する必要があります。
新しいノードを登録する
コンセンサスアルゴリズムを実装する前にノード同士に隣接するノードを認識させる必要があります。ネットワーク上の他のノードが格納されたレジストリを各ノードに入れておく必要があるわけです。そのために、追加のエンドポイントが必要になります。
/nodes/register
で新しいノード一覧をURL形式で受け取る。/nodes/resolve
でコンセンサスアルゴリズムを実装する。これによって矛盾を解決し、ノードが正しいチェーンを保持することを保証する。
ブロックチェーンコンストラクタを修正し、ノードを登録する方法が必要になります。
... | |
from urllib.parse import urlparse | |
... | |
class Blockchain(object): | |
def __init__(self): | |
... | |
self.nodes = set() | |
... | |
def register_node(self, address): | |
""" | |
Add a new node to the list of nodes | |
: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()
を使ったことに注意してください。負荷をかけずに新しいノードの追加が冪等であること確実にする方法です。つまり、特定のノードを何回追加しても追加は実質1回行われることになります。
コンセンサスアルゴリズムを実装する
上述のとおり、矛盾はあるノードが別のノードとは異なるチェーンを持っている場合に起こります。この問題を解決するために、まず、最長の有効チェーンを信頼するというルールを規定します。つまり、ネットワーク上の最長のチェーンがデファクトとなるということです。このアルゴリズムを使用することで、ネットワーク内のノード間で合意(コンセンサス)させるのです。
... | |
import requests | |
class Blockchain(object) | |
... | |
def valid_chain(self, chain): | |
""" | |
Determine if a given blockchain is valid | |
: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") | |
# Check that the hash of the block is correct | |
if block['previous_hash'] != self.hash(last_block): | |
return False | |
# Check that the Proof of Work is correct | |
if not self.valid_proof(last_block['proof'], block['proof']): | |
return False | |
last_block = block | |
current_index += 1 | |
return True | |
def resolve_conflicts(self): | |
""" | |
This is our Consensus Algorithm, it resolves conflicts | |
by replacing our chain with the longest one in the network. | |
:return: <bool> True if our chain was replaced, False if not | |
""" | |
neighbours = self.nodes | |
new_chain = None | |
# We're only looking for chains longer than ours | |
max_length = len(self.chain) | |
# Grab and verify the chains from all the nodes in our network | |
for node in neighbours: | |
response = requests.get(f'http://{node}/chain') | |
if response.status_code == 200: | |
length = response.json()['length'] | |
chain = response.json()['chain'] | |
# Check if the length is longer and the chain is valid | |
if length > max_length and self.valid_chain(chain): | |
max_length = length | |
new_chain = chain | |
# Replace our chain if we discovered a new, valid chain longer than ours | |
if new_chain: | |
self.chain = new_chain | |
return True | |
return False |
最初のvalid_chain()
メソッドは、各ブロックをループし、ハッシュ値とプルーフの両方を検証することによってチェーンが有効かを確認します。
resolve_conflicts()
メソッドは、隣接する全てのノードをループし、ノードのチェーンをダウンロードして、上述のメソッドでチェーンを検証します。自分のチェーンよりも長く有効なチェーンが見つかった場合、自分のチェーンをそのチェーンに置き換えます。
では、APIに2つのエンドポイントを登録しましょう。1つは隣接するノードの追加のために、もう1つは矛盾を解決するために登録します。
@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 |
この時点で別のマシンを使って、異なるノードをネットワークに設定することも可能です。あるいは同じマシンの異なるポートでプロセスを設定することもできます。私は別のノードを自分のマシンに設定し、異なるポートで既存のノードと一緒に登録しました。そのため、2つのノードが存在しています。http://localhost:5000
とhttp://localhost:5001
です。
ノード2で新しいブロックをいくつかマイニングし、確実にチェーンを長くしました。その後、チェーンをコンセンサスアルゴリズムに置き換えられたノード1でGET /nodes/resolve
を呼び出ました。
これで終わりです。あとは友人を集めて作成したブロックチェーンを試してみるだけです。
この記事を読んで新しいものを作ってみるきっかけになれたらと思います。ブロックチェーンは私たちの経済、政府、記録保持に対する考えを急速に変えていくと信じているので、個人的に仮想通貨に熱狂しています。
追記: 追加でパート2を予定しています。ブロックチェーンを拡張してトランザクション検証メカニズムを持たせる方法やブロックチェーンの大量生産化の方法などについて書く予定です。
この解説を楽しんだという方、ご意見やご質問のある方、コメントしてください。誤りに気付かれた方は、遠慮なくここにコードを投稿してください。