SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

ブロックチェーンを実装する:三回目:マイニング機能を追加する/Pythonサンプル

f:id:arakan_no_boku:20200130223854p:plain

目次

ブロックチェーンを実装する:三回目:マイニング機能を追加する/Pythonサンプル

 ブロックチェーンのお勉強がてら、pythonで実装してみる三回目です。

今回は二回目で改善したソースをベースにします。

arakan-pgm-ai.hatenablog.com

ここに「マイニング」と呼ばれる部分を追加します。 

 

マイニング機能を追加する

ブロックの完成とみなしてもらえる条件を満たすまで、ナンスを変えながら、ひたすら計算し続ける行為を「マイニング」と呼びます。

マイニングでやっていること、例えば、ビットコインなどだと、こんな感じです。

  1. ブロックのデータでハッシュ値をもとめる。
  2. それに自分で考えたデータ(ナンス)を足してハッシュ値を求める。
  3. その結果がブロックの完成とみなす条件を満たしているかどうか検査する。
  4. 満たしていればOK、満たしていなければナンスを変えて再度試みる。

なぜ、こんなことをするかというと。

ブロックチェーンのような暗号通貨だと、この計算の答えを見つけてブロックを追加することで報酬がもらえる仕組みだからです。

この「マイニング」の難易度設定を

  • 難しすぎるとブロックの追加がいつまでもできない。
  • 簡単すぎると成功者が乱立して、同時に成功するブロックが複数できたりする。 

この間のちょうど良いところ・・例えば、ビットコインだと10分程度の時間で解ける程度の難易度・・に調整できるかが暗号通貨などのひとつの肝となります。 

 

実装するマイニングの仕様 

前二回で実装した「MyBlockChain」は、別に暗号通貨っぽいことをしたいわけではないので、ブロックの構築に難易度設定しても意味がありません。

でもまあ。

マイニングの雰囲気だけでもやってみて、どの程度の難易度設定で、どのくらい時間がかかるのか?・・を確かめてみようと思います。

あまり、難しくすると大変なので、以下のような仕様にします。

  • ナンスは00001 からFFFFF(10進数で1,048,575)まで
  • トランザクションハッシュにナンスを足して先頭4文字が「0fea」になったら成功

 

マイニングのサンプル実装

ハッシュの計算とトランザクションデータ作成は、前回までの「MyBlockChain」のメソッドを使って、上記の仕様をまんま実装してみます。

とりあえず、見つかったらprintでメッセージを表示するだけのソースはこちら。

    # マイニング作業をやってみる
    def calc_proof(self, inp, outp):
        # トランザクションを生成する
        new_transaction = self.__create_new_transaction(inp, outp)
        root_hash = self.__calc_tran_hash(new_transaction)
        for i in range(1, 1048575):
            hex_str = format(i, 'x')
            result_a = self.__calc_tran_hash(root_hash + hex_str)
            result = result_a.lower()
            if result[0:4] == "0fea":
                print(hex_str, "でブロック完成条件を満たしました。")
                return True
        print("最後まで一致しませんでした。_| ̄|○")
        return False

この程度なら、自分のノートPCでも数秒で終わります。

以下のようにして、ざっくり実行してみたら。

bc = MyBlockChain()
inp = {
'xxxxxxxxxxxxxx': 123456
}
outp = {
'yyyyyyyyyyy': 7890918
}
bc.calc_proof(inp, outp)

 こんな感じの結果です。

1282a でブロック完成条件を満たしました。

でも。

上記は4文字だけの条件なので、すぐ終わりますが、条件を5文字にすると104万回のループでは「最後まで一致しませんでした。_| ̄|○」が頻発します。

短くも長くもない狙った時間で 正解を見つけられるような難易度の設定というのは、実に難しいということが、やってみるとわかります。

 

マイニングを「MyBlockChain」クラスに組み込む

 せっかく作ったので、MyBlockChainに組み込んでみます。

ファイルから読み込んだ時は行わず、テストデータを生成するときにだけ、ちょっとしたマイニング行為の負荷をかける・・と、まあ、そういう仕様で。

実装ポイント

add_new_block()の中で改良します。

ファイルから読み込まないでブロックを追加するときに、マイニングのロジックが動くようにします。

# テストデータを作るためのとりあえず処理
    if check_hash == self.testkey:
        self.r_mode = False
        check_ok = self.__calc_proof(inp, outp)
    else:
        # JSONファイルから読み込んだブロックの検査
        if check_hash == tran_hash:
            check_ok = True

その結果をブロックヘッダに記録します。 

正解が出た時の16進文字列を、self.current_nonceにセットして、ブロックヘッダに以下のようにセットするわけです。

'prev_hash': prev_hash,
'tran_hash': tran_hash,
'nonce': self.current_nonce

あと、細かいところですが。

def __calc_proof(self, inp, outp) を少しだけ汎用的にします。

__init__()の中の「self.match_str = "0fea"」の長さで、ループ回数や難易度を調整できるようにします。

ループの最大回数を「 max_loop = 1048575 * magnification」で求めるようにしているのは保険です。

別に無限ループでマッチしたらブレイクでもよかったのですが、個人的に無限ループは嫌いなので、あえて一定の回数を越えたら失敗するようにしてるだけです。

 

マイニングを追加した三回目のソース全文

import datetime as dt
import json
import hashlib


class MyBlockChain(object):
    # ブロックチェーンを初期化する
    def __init__(self, loadfile="chain00.json"):
        self.chain = []
        # マイニング時の難易度
        self.match_str = "0fea"
        # ナンス(マイニングで正解を得た時の値)
        self.current_nonce = "****"
        # テストデータ作成用キー(固定)
        self.testkey = "a6cfb4c140cf8cf2b281d5538961cdcfaca30a464befc02bf5af3bc7397cd4d7"
        # 最初のブロックだけは固定で作る
        self.add_new_block(0, {'747bc42088cf0b3915982af289189e8f': 0}, {
                           '14d3325a7d594bc2d30a7014a536cb13': 0}, self.testkey)
        # 読み込みモード
        self.r_mode = True
        # JSONファイルからブロックデータを読み込んで、チェインに追加する
        try:
            with open(loadfile, "r") as fr:
                loadblocks = json.load(fr)
                for bck in loadblocks:
                    isOK = self.add_new_block(
                        bck['id'], bck['input'], bck['output'], bck['hash'])
                    if not isOK:
                        print("ブロック構築を異常終了します。")
                        break
        except BaseException:
            pass

    # 新しいブロックを作成する
    def add_new_block(self, id, inp, outp, check_hash):
        # デフォルトに一旦戻す
        self.r_mode = True
        self.current_nonce = "****"
        # トランザクションを生成する
        new_transaction = self.__create_new_transaction(inp, outp)

        # 前のブロックのハッシュを取得。最初だけ固定値
        if len(self.chain) > 0:
            prev_hash = self.chain[-1]['block_header']['tran_hash']
        else:
            prev_hash = "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13"

        check_ok = False
        # このブロックのハッシュ値を計算する
        tran_hash = self.__hash(
            prev_hash + self.__calc_tran_hash(new_transaction))
        # テストデータを作るためのとりあえず処理
        if check_hash == self.testkey:
            self.r_mode = False
            check_ok = self.__calc_proof(inp, outp)
        else:
            # JSONファイルから読み込んだブロックの検査
            if check_hash == tran_hash:
                check_ok = True

        if check_ok:
            # トランザクションを元にブロックを生成して、チェーンに接続する
            new_block = {
                'block_index': len(self.chain) + 1,
                'block_time': dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'block_header': {
                    'prev_hash': prev_hash,
                    'tran_hash': tran_hash,
                    'nonce': self.current_nonce
                },
                'tran_counter': len(inp) + len(outp),
                'tran_body': new_transaction,
            }
            self.chain.append(new_block)
        else:
            if self.r_mode:
                print("要注意「", id, "」のトランザクションで改ざんの疑いがあります。!")
            else:
                print("マイニングに失敗しました。")
            return False

        return True

    # 新しいトランザクションを生成する
    def __create_new_transaction(self, inp, outp):
        new_transaction = {
            'input': inp,
            'output': outp,
        }
        return new_transaction

    # ハッシュ値を計算する。SortをTrueにしているのはハッシュの整合性維持のため
    def __calc_tran_hash(self, new_transaction):
        tran_string = json.dumps(new_transaction, sort_keys=True).encode()
        return self.__hash(tran_string)

    # ハッシュ計算内部関数
    def __hash(self, str_seed):
        return hashlib.sha256(str(str_seed).encode()).hexdigest()

    # JSONファイルにブロックチェインの主要要素のみを保存する
    def save(self, outfile="chain00.json"):
        save_arr = []
        for i, x in enumerate(self.chain):
            if i > 0:
                save_dic = {}
                save_dic['id'] = x['block_index']
                save_dic['hash'] = x['block_header']['tran_hash']
                save_dic['input'] = x['tran_body']['input']
                save_dic['output'] = x['tran_body']['output']
                save_arr.append(save_dic)
        fw = open(outfile, 'w')
        json.dump(save_arr, fw, indent=2)

    # ブロックの内容を表示する
    def dump(self, block_index=0):
        if(block_index == 0):
            print(json.dumps(self.chain, sort_keys=False, indent=2))
        else:
            print(
                json.dumps(
                    self.chain[block_index],
                    sort_keys=False,
                    indent=2))

    # テスト用のブロックを強制的に作るためのテストキーを返す
    def get_testkey(self):
        return str(self.testkey)

    # マイニング作業をやってみる
    def __calc_proof(self, inp, outp):
        # トランザクションを生成する
        new_transaction = self.__create_new_transaction(inp, outp)
        root_hash = self.__calc_tran_hash(new_transaction)
        m_str = self.match_str.lower()
        l_int = len(m_str)
        if l_int <= 4:
            magnification = 1
        else:
            magnification = 10 ** (l_int - 4)
        max_loop = 1048575 * magnification
        for i in range(1, max_loop):
            hex_str = format(i, 'x')
            result_a = self.__calc_tran_hash(root_hash + hex_str)
            result = result_a.lower()
            if result[0:l_int] == m_str:
                self.current_nonce = hex_str
                return True
        return False

マイニングを組み込んだクラスの実行結果

実行するソースは以下です。

bc = MyBlockChain()
inp = {
    'xxxxxxxxxxxxxx': 123456
}
outp = {
    'yyyyyyyyyyy': 7890918
}
bc.add_new_block(0, inp, outp, bc.get_testkey())
bc.dump()

前回のMyBlockChainの引き続きで、最初のブロック以降はJSONファイルから読み込んで、最後に上記の1ブロックを追加する感じです。

結果は以下。

[
  {
    "block_index": 1,
    "block_time": "2020-02-13 22:55:55",
    "block_header": {
      "prev_hash": "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13",
      "tran_hash": "ec3400824e188b13a5ca7821d180fe4084e120fd02a6d0e14e6848ae070b29c8",
      "nonce": "4f9b"
    },
    "tran_counter": 2,
    "tran_body": {
      "input": {
        "747bc42088cf0b3915982af289189e8f": 0
      },
      "output": {
        "14d3325a7d594bc2d30a7014a536cb13": 0
      }
    }
  },
  {
    "block_index": 2,
    "block_time": "2020-02-13 22:55:55",
    "block_header": {
      "prev_hash": "ec3400824e188b13a5ca7821d180fe4084e120fd02a6d0e14e6848ae070b29c8",
      "tran_hash": "f31daf4fa73faa5d890cbc0d8ad285ec6dcddf64c0236fc726124e922552126b",
      "nonce": "****"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "Y3XNSHTT": 12345
      },
      "output": {
        "X2VBASDD": 10000,
        "Z45UHRKL": 2345
      }
    }
  },
  {
    "block_index": 3,
    "block_time": "2020-02-13 22:55:55",
    "block_header": {
      "prev_hash": "f31daf4fa73faa5d890cbc0d8ad285ec6dcddf64c0236fc726124e922552126b",
      "tran_hash": "7eca4ab3c0dd2b825d7c7568f324f3f7a7b4d883e6c9e85d5028bfcb825b470c",
      "nonce": "****"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "X2VBASDD": 22345
      },
      "output": {
        "Y3XNSHTT": 10000,
        "Z45UHRKL": 12345
      }
    }
  },
  {
    "block_index": 4,
    "block_time": "2020-02-13 22:55:55",
    "block_header": {
      "prev_hash": "7eca4ab3c0dd2b825d7c7568f324f3f7a7b4d883e6c9e85d5028bfcb825b470c",
      "tran_hash": "e582060c816f5930fc9874f7a8f18eeb25ccf053d40a84d2a2bb088c64cc9f1a",
      "nonce": "****"
    },
    "tran_counter": 3,
    "tran_body": {
      "input": {
        "Z45UHRKL": 23450
      },
      "output": {
        "Y3XNSHTT": 13000,
        "X2VBASDD": 10450
      }
    }
  },
  {
    "block_index": 5,
    "block_time": "2020-02-13 22:55:55",
    "block_header": {
      "prev_hash": "e582060c816f5930fc9874f7a8f18eeb25ccf053d40a84d2a2bb088c64cc9f1a",
      "tran_hash": "7add9e141b25000ac8f48af5b4fd9f2917faa1cc539305ae942206d89bafe7e1",
      "nonce": "1282a"
    },
    "tran_counter": 2,
    "tran_body": {
      "input": {
        "xxxxxxxxxxxxxx": 123456
      },
      "output": {
        "yyyyyyyyyyy": 7890918
      }
    }
  }
]

ファイルから読み込んだブロックは「nonce:"****"」、計算して追加したブロックは「 "nonce": "1282a"」となってます。

OKそうですね。

とりあえず、3回にわたってやってみて、感触だけはわかった気がします。

第一回目と第二回目記事のリンク

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

ではでは。