24
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

170行くらいで実際に動くブロックチェーンを実装してみた

Last updated at Posted at 2019-12-08

この記事はBlockchain Advent Calendar 2019の6日目です。昨日は@y-chanさんのatomicswapを理解するでした。


事の起こり

「ブロックチェーンの解説本とかたくさん出てるけど、結局どういう動きするのかよくわからん」
「構造体的なブロックチェーンは簡単そうだけど、ブロックの中身の作り方とか、マイニングの仕組みとかいまいちよくわからん」

と思っていたのですが
 「解説聞いてわからないなら実装してみればいいじゃないプログラマーだもの by むむお」
ということで実装してみました。

教材はGerald Nash氏の

です。

前提条件

教材にしたGerald氏の記事はPython2で実装されていたので、Python3(3.5)で実装し直しています。ついでに「処理順こっちのほうが良くね?」とか「これ作るだけ作って呼んでないじゃない」みたいな部分もちょいちょい直しています。
元記事では分割したコードのインポート部分を省略しながら掲載していましたが、本記事では動作するコードで記載してきます1
またコード中のコメントは元記事にとらわれず必要を感じたら自由に追記しています。

作成したソースコードはすべて同一ディレクトリに配置されている前提で記述しています。

実装の順番

  • 『すごくちっちゃいブロックチェーンを作ってみよう』の範囲(だいたい50行くらい)
    1. ブロックの実装
    2. ブロックチェーンの動作確認
  • 『すごくちっちゃいブロックチェーンをおっきくしてみよう』の範囲(だいたい130行くらい)
    3. トランザクション登録と新規ブロック追加

の順番に実装していきます。コメント行を除いた行数は全体で170行ほどで出来ました。

いざ実装

ブロックの実装

まずはブロックを実装します。いわゆるマイニングの単位になるやつです。
含まれる情報は

  • インデックス情報
  • タイムスタンプ
  • データ
    • 今回は暗号通貨っぽく実装しているので受け渡し情報(送り元、送り先、受渡量)
    • この部分がいわゆるブロックチェーンで「改ざんが難しくなる」とされている部分
    • ここに含めるデータを工夫することで様々な分野への応用が考えられている
  • 前のブロックのハッシュ値
    • 前のブロックのハッシュを持つことで途中のブロックの改ざんを難しくしている
      • 改ざんしたブロック以降をすべて改ざんする必要があるため
  • 上記4つの情報から算出した自ブロックのハッシュ値

になります。

snakecoin_block.py

import hashlib

class Block:
  def __init__(self, index, timestamp, data, previous_hash):
    self.index = index
    self.timestamp = timestamp
    self.data = data
    self.previous_hash = previous_hash
    self.hash = self.hashblock()
  
  def hashblock(self):
    sha = hashlib.sha256()
    sha.update((str(self.index) +
              str(self.timestamp) +
              str(self.data) +
              str(self.previous_hash)).encode('utf-8'))
    return sha.hexdigest()

新しいブロックチェーンを生むためには前のブロックが必要になるため、最初のブロックをあらかじめ格納する必要がある。これをジェネシスブロック(創生ブロック?)というらしい。

データには後述するPoW(Proof of Work)アルゴリズムで算出される最小の値9を先頭値として設定しています。また、前のブロックのハッシュは存在しないので'0'を設定します。

snakecoin_genesis.py

from snakecoin_block import Block
import datetime

def create_genesis_block():
  return Block(0, datetime.datetime.now(), {
    'message': 'Genesis Block',
    'proof-of-work': 9
  }, '0')

ここまでをテストするためにブロックチェーン末端にブロックを追加するテスト処理を書いていきます。テスト用なのでデータには適当な文字列を設定します。

snakecoin_next_block.py

from snakecoin_block import Block
import datetime

def next_block(last_block):
  this_index = last_block.index + 1
  this_timestamp = datetime.datetime.now()
  this_data = "Hey! I'm block " + str(this_index)
  previous_hash = last_block.hash
  return Block(this_index, this_timestamp, this_data, previous_hash)

ブロックチェーンの動作確認

さて、これでデータ構造としてのブロックチェーンは完成です。ここまで作ったものをつなげて動作確認をしてみます。

大まかな手順は

  1. ジェネシスブロックを作ってブロックチェーンを作成する
  2. ブロック情報を出力
  3. 指定回数ループ
    1. 新しいブロックをブロックチェーンに繋げる
    2. 新しいブロック情報を出力

です。

snakecoin_blockchain_test.py

from snakecoin_genesis import create_genesis_block
from snakecoin_next_block import next_block

# ジェネシスブロックを作成してブロックチェーンを作成
blockchain = [create_genesis_block()]
# ジェネシスブロックを末端ブロックとして設定
previous_block = blockchain[0]

# つなげるブロック数
num_of_blocks_to_add = 3

# ジェネシスブロックの情報を出力
print("Block #{} has been added to the blockchain!".format(previous_block.index))
print("Data: {}".format(previous_block.data))
print("PrHh: {}".format(previous_block.previous_hash))
print("Hash: {}\n".format(previous_block.hash))

for i in range(0, num_of_blocks_to_add):
  # 新しいブロックを作成してブロックチェーンに追加
  block_to_add = next_block(previous_block)
  blockchain.append(block_to_add)

  # 新しいブロック情報を出力
  print("Block #{} has been added to the blockchain!".format(block_to_add.index))
  print("Data: {}".format(block_to_add.data))
  print("PrHh: {}".format(block_to_add.previous_hash))
  print("Hash: {}\n".format(block_to_add.hash))

  # 末端ブロックを更新
  previous_block = block_to_add

実行すると以下のような結果が出力されると思います。自ブロックのハッシュHashが次のブロックに前ブロックのハッシュPrHhとして記録されています。

$ python snakecoin_blockchain_test.py
Block #0 has been added to the blockchain!
Data: {'proof-of-work': 9, 'message': 'Genesis Block'}
PrHh: 0
Hash: 96cab14611cd4e674d78bb2e3a93ccdf2364955575039d4ffa09a2714b12e8ac

Block #1 has been added to the blockchain!
Data: Hey! I'm block 1
PrHh: 96cab14611cd4e674d78bb2e3a93ccdf2364955575039d4ffa09a2714b12e8ac
Hash: 6023a093c0e3449692fe431679a3752a7201e74b17059087f777dfd54105f906

Block #2 has been added to the blockchain!
Data: Hey! I'm block 2
PrHh: 6023a093c0e3449692fe431679a3752a7201e74b17059087f777dfd54105f906
Hash: 18af14f5ab32bd40fa3c141290aba7a23cff058f391eb8769f4b5e4ea84aa0f8

Block #3 has been added to the blockchain!
Data: Hey! I'm block 3
PrHh: 18af14f5ab32bd40fa3c141290aba7a23cff058f391eb8769f4b5e4ea84aa0f8
Hash: 13ff0cbfcac15d705319e67abd48e3768fa6c4465ffe624689e65f29e91bf641

トランザクション登録と新規ブロック追加

さて実際に受渡情報をブロックチェーンに格納していきましょう。インタフェースとしてRESTを使うのでFlaskを使って作っていきます。

インタフェースとして

  • 受渡トランザクションの登録 /transactions
  • トランザクションをブロック化してブロックチェーンに追加 /mines
  • ブロックチェーンの参照 /blocks

です。

細々した挙動や、実際に運用する場合に必要になりそうな処理などについてはコメントとしてコード中に記載しています。

snakecoin_node_transaction.py

from snakecoin_block import Block
from snakecoin_genesis import create_genesis_block
from flask import Flask, request, jsonify
import datetime
import json
import requests

# ブロックチェーンを定義
blockchain = []
blockchain.append(create_genesis_block())

# トランザクションリスト
# このノード内のトランザクションが格納される
this_nodes_tx = []

# ブロックチェーンネットワーク上のノードURLリスト
# TODO: 新しいノードを検出する仕組みを作る
peer_nodes = []

# とりあえずマイナーのアドレスは固定
# TODO: ノードごとに一意に生成して設定する仕組みを作る
miner_address = "q3nf394hjg-random-miner-address-34nf3i4nflkn3oi"

# Proof of Work アルゴリズム
# BitCoinなどでは計算量の多い特定条件のハッシュ値探索だが
# ここでは簡略化するために
#   「処理※回数が9で割り切れる」 AND 「前回の結果で割り切れる」
#   ※今回はただのインクリメント
# を発見することとしている。
# ただし、この状態だと発見処理をサーバーが実行しているため
# 処理が分散されず、ブロックチェーンの分岐も起きやすい状態になる。
# TODO: 計算量が多い部分はクライアント側で実装する想定で、サーバー側では確認処理のみが実装されている状態にする
def proof_of_work(last_proof):
  incrementor = last_proof + 1
  while not (incrementor % 9 == 0 and incrementor % last_proof == 0):
    incrementor += 1
  return incrementor

# 各ノードが保持するブロックチェーン情報を取得する
def find_new_chains():
  other_chains = []
  for node_url in peer_nodes:
    block = requests.get(node_url + '/blocks').content
    block = json.reloads(block)
    other_chains.append(block)
  return other_chains

# 新しいブロックを繋げる末端を探す
def consensus():
  global blockchain
  longest_chain = blockchain
  # 他のノードが保持するブロックチェーン情報を取得
  other_chains = find_new_chains()
  # 最長のブロックチェーンを探索して、最長のブロックチェーンを採用する。
  # なお現状の配列によるブロックチェーン実装だと分岐したブロックチェーンの短い枝の情報がロストする。
  # TODO: 有向グラフのような実装で分岐した枝を保持しつつ、採用のブロックチェーンを採用するのではなく最長の末端を採用するロジックに変更
  for chain in other_chains:
    if len(longest_chain) < len(chain):
      longest_chain = chain
  blockchain = longest_chain

#### endpoints

node = Flask(__name__)

# snakecoinの受け渡しトランザクションを登録する
@node.route('/transactions', methods=['POST'])
def transactions():
  if request.method == 'POST':
    # POSTされたトランザクションデータをトランザクションリストに追加
    new_tx = request.get_json()
    this_nodes_tx.append(new_tx)

    # 追加したトランザクションデータを標準出力
    print("New Transaction")
    print("FROM: {}".format(new_tx['from']))
    print("TO: {}".format(new_tx['to']))
    print("AMOUNT: {}".format(new_tx['amount']))

    return jsonify({'message': 'Transaction submission successful'}), 200

# 受け渡しトランザクションをブロック化し、ブロックチェーンにつなげる
@node.route('/mines', methods=['POST'])
def mines():
  # コンセンサスをとる
  consensus()

  # 最後のproofを取得する
  last_block = blockchain[len(blockchain) - 1]
  last_proof = last_block.data['proof-of-work']

  # マイニングする
  # TODO: 新しいproofをパラメータで受け取って適合性判定のみ行うようにする
  proof = proof_of_work(last_proof)

  # マイナーに報酬として 1 snakecoin を付与するトランザクションを追加
  this_nodes_tx.append({
    "from": "network",
    "to": miner_address,
    "amount": 1
  })

  # 新しいブロックに必要な値の用意
  # ここでトランザクションリストをブロックに格納している
  new_block_index = last_block.index + 1
  new_block_timestamp = this_timestamp = datetime.datetime.now()
  new_block_data = {
    "proof-of-work": proof,
    "transactions": list(this_nodes_tx)
  }
  last_block_hash = last_block.hash

  # 新しいブロックを生成し、ブロックチェーンに追加
  mined_block = Block(
    new_block_index,
    new_block_timestamp,
    new_block_data,
    last_block_hash
  )
  blockchain.append(mined_block)

  # トランザクションリストを初期化
  this_nodes_tx[:] = []

  return jsonify(
    {
      "index": new_block_index,
      "timestamp": new_block_timestamp,
      "data": new_block_data,
      "hash": last_block_hash
    }
  )

# このノードが保持するブロックチェーン情報を参照する
@node.route('/blocks', methods=['GET'])
def get_blocks():
  chain_to_send = blockchain[:]
  for i in range(len(chain_to_send)):
    block = chain_to_send[i]
    # Blockクラスのプロパティを文字列化
    block_index = str(block.index)
    block_timestamp = str(block.timestamp)
    block_data = str(block.data)
    block_hash = block.hash
    # JSON文字列化できるように辞書型に変換
    chain_to_send[i] = {
      "index": block_index,
      "timestamp": block_timestamp,
      "data": block_data,
      "hash": block_hash
    }
  # JSON文字列に変換してクライアントに返す
  return jsonify(chain_to_send)

node.run()

元記事では動作確認にcURLを使っていたので、本記事ではPostmanの実行結果を載せておきます。

$ python snakecoin_node_transaction.py

で起動してGET /blocksするとこんな内容が返ってきます。

image.png

ちゃんとジェネシスブロックを含んだブロックチェーン(配列)が返ってきました。次にPOST /transactionsでsnakecoinの受渡情報を登録してみましょう。

image.png

2 snakecoins を受け渡したトランザクションが無事成功(Status: 200 OK)しました。ただしこのトランザクションはまだブロックチェーンには記録されていません(=未成立)。GET /blocksを叩いても内容が変わっていないのがわかると思います。

image.png

受渡トランザクションを成立させるためにマイニングをしましょう。POST /minesを叩きます。

image.png

新たに作られたブロック情報が返ってきました。2 snakecoinsの受渡情報の他にマイニングした人(マイナー)への報酬として 1 snakecoin の受渡情報が含まれています。

image.png

GET /blocksを確認してもちゃんとブロックチェーンに新しいブロックが追加されているのがわかると思います。

まとめと感想

ブロックチェーンは怖くない!

まだまだP2Pネットワークの構築とかWalletの実装とかPoWじゃなくてEtheriumのPoS(Proof of Stake)が知りたいんじゃ! とか色々不足は感じていますが、「ブロックチェーン完全に理解できた()」までは到達できた気がします。

実際に運用していくまでにはまだ調べないといけないことがありますが、「実運用までの道が見えない」ところから「お遊びで作るポイントシステムくらいならもう少しで作れそう」くらいにはなったと思います。
でもやっぱりマイニングまわりの実装はクライアントにサーバー機能も持たせるものなのか? とかまだ良くわからないところもあるのでアドベントカレンダー記事とか読み漁って勉強していこうと思いました。

これがブロックチェーン理解の一助になれれば幸いです。そして改めてGerald氏に感謝。


明日?は@shu-kobさんのBitcoin Signetを触ってみようです。

  1. 元記事でもフルサイズのソースコードはGistで公開しているのですが「これ使ってないよね?」みたいなものはちらほらあったので修正しています。

24
20
1

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
24
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?