"BOKU"のITな日常

62歳・文系システムエンジニアの”BOKU”は日々勉強を楽しんでます

マイブロックチェーンのマイニング機能をPythonで追加実装する/ブロックチェーンの勉強三回目

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

今回は「マイニング」などと呼ばれている部分です。

f:id:arakan_no_boku:20200130223854p:plain

 

はじめに

 

一回目にブロックチェーンの骨格だけ実装し、二回目でそこにファイル保存や改ざんチェックをつけくわえてみました。

arakan-pgm-ai.hatenablog.com

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に組み込んでみます。

ほぼ、意味はないですが(笑)

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

それを組み込んだソース全文です。

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

ポイントを補足します。

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

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

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

実際、match_strの文字数を5文字にするだけで、だいぶ時間がかかるようになります。

時間がかかっても、難易度をあげたいなら、ここは無限ループに修正したほうがよいだろうなと思います。

あとは。

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

 

実行してみます。

 

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

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回にわたってやってみて、感触だけはわかった気がします。

ではでは。