Pythonで Merkle root を計算してみた

本投稿は TECOTEC Advent Calendar 2022 の25日目の記事です。

こんにちは。次世代デジタル基盤開発事業部の六本木です。

今回はビットコインを始めとするブロックチェーンでとても重要な Merkle tree (Xmas tree) およびその Merkle root の計算について紹介するとともに、簡単な実装を行いましたのでシェアしたいとおもいます。

Merkle treeとは?

データを要約・検証する技術で、以下計算ルールに従って構成された2分木のことです。(より一般に Hash tree とも言います)

  1. データ列が存在する
  2. 全てのデータ列にそれぞれハッシュ関数を適用する
  3. 得られた結果について、左から順番にペアを作っていく
  4. 列の数が奇数だった場合、最後の(ハッシュされた)データはコピーしてペアとする
  5. 各ペアについて、単純に結合したもので再度ハッシュを適用する
  6. 以下同様の処理を繰り返す(列の数はどんどん半分になっていく)
  7. 最後に残ったひとつのデータはコピーせず、これをMerkle root (hash root, top hash)という。

Merkle tree の説明図
Merkle tree

さて、このMerkle rootはなにがいいのでしょうか。

Merkle rootの存在意義

p2pなどの分散型システムにおいて、取得したデータが破損していないか・改ざんされていないかをチェックするのに有用です。

例えば得られたデータについて、そのハッシュ値と計算過程のハッシュ値(Proofといいます)を使ってMerkle rootを計算した結果、信頼のおけるノードが一律管理しているMerkle rootと一致すれば、データに問題ないと言えます。 (逆に少しでも違うと、Merkle rootの値は全く違うものとなります)

またさらに、Proofは全て取得する必要はなくデータのインデックスさえ分かれば、そのデータが属するrootまでの道筋におけるペアのハッシュ値が分かり、Merkle rootを効率的に計算することが可能です。 (そのようなhash値の集合をMerkle Pathという(下図の{h3, h7, h12}))

例えば、データd (配列0始まりの場合、インデックス3)の検証を行う際は、下図緑のハッシュ値のみ取得できればOKです。

Proofによるデータdの検証を表現した図
Proofによるデータdの検証を表現した図

Merkle treeはブロックチェーンの他、以下サービス・ツールでも利用されているそうです。

  • DynamoDB
  • git

ブロックチェーンにおいては、このMerkle rootをブロックヘッダに記録していることが多く、このお陰でスマートフォンにウォレットを入れることができるようになります。

全てのトランザクションを保持(Full node wallet)する場合と比べ、1000分の1程度の容量のみあれば、トランザクションの検証が可能になります。 この形式をSPV(Simplified Payment Verification) といいます。

Python でビットコインのMerkle root を計算してみる

今回は、 https://www.blockchain.com/explorer/api/blockchain_api を利用して、実際のトランザクションハッシュ全てからMerkle rootを計算し、実際にこのブロックのMerkle rootと一致するか見てみます。

ビットコインの実装では、ハッシュ関数はダブルハッシュsha256(sha256())が利用されています。 ※何故このような関数を利用しているのか(2重でsha256しているのか)は調べても出てきませんでした。謎です。

また注意点として、計算する際は、リトルエンディアンで行う必要があるので、リバースする必要があります。 計算は再帰的に行っています。

from hashlib import sha256
import requests, json

base_url = 'https://blockchain.info/rawblock/'
# block 768278 (2022/12/21 8:46に生成)
block_hash = '00000000000000000001d8047b21fdb06ed35aa2a7feb99b910b8405e2404c00'
end_point = base_url + block_hash

# ビッグエンディアン<-> リトルエンディアンを変換する関数
# https://qiita.com/QUANON/items/e3d91a6f33536bd0de5a
def reverse(hex_be):
    bytes_be = bytes.fromhex(hex_be)
    bytes_le = bytes_be[::-1]
    hex_le = bytes_le.hex()
    return hex_le

# double sha256を計算する関数
def dhash(hash):
    bin = bytes.fromhex(hash)
    hash = sha256(bin).digest()
    hash2 = sha256(hash).hexdigest()
    return hash2

# 再帰的にMerkle treeを構築していく関数
def culculate_merkle(hash_list):
    # 要素数が1になったらそれがMerkle root
    if len(hash_list) == 1:
        return hash_list[0]

    # 要素数の数が奇数だったら最後の要素を複製する
    if len(hash_list) % 2 == 1:
        hash_list.append(hash_list[-1])

    parent_hash_list = []

    # 順番に2つずつペアを組んで処理
    # https://hk29.hatenablog.jp/entry/2020/10/25/134840
    it = iter(hash_list)
    for hash1, hash2 in zip(it, it):
        # hash1とhash2を単純に結合する
        parent_hash_list.append(hash1 + hash2)

    # 全ての要素に対してdhashを適用
    parent_hash_list = list(map(dhash, parent_hash_list))

    # 再帰的にコール
    return culculate_merkle(parent_hash_list)

# トランザクションハッシュを取得するために
# https://www.blockchain.com/explorer/api/blockchain_api を利用
data = requests.get(end_point)
jsondata = json.loads(data.text)

merkleroot_expected = jsondata['mrkl_root']

# transaction hashのみ抽出
# データ⇒ 初回のhashは既に実施済み
tx_list = list(map(lambda tx_object: tx_object['hash'], jsondata['tx']))

# ビッグエンディアン⇒ リトルエンディアンに変換
tx_list = list(map(reverse, tx_list))

# Merkle rootを計算する
output = culculate_merkle(tx_list)
# リトルエンディアン⇒ ビッグエンディアンに変換
output = reverse(output)

print('期待する値:',merkleroot_expected)
print('計算した値:',output)

結果はこちらです。

$ python merkleTree.py
期待する値: ee021ec71f5b4b4c6307ffcda579d2147e24d1d19b24ba89a1e993b2640d384a
計算した値: ee021ec71f5b4b4c6307ffcda579d2147e24d1d19b24ba89a1e993b2640d384a

いかがでしょうか、ブロックヘッダに格納されているMerkle root と計算したMerkle rootが一致することが確認できました。

今後の課題としては、インデックスを受け取り、そのパス上のペアとなるproofを返却するgenerateProof関数と、トランザクションハッシュが正しいか検証するVerify関数を実装してみたいなと思います。

以上、クリスマスツリーではなくマークルツリーのお話でしたが、よい一日を。

参考

def dhash(hash):
    return reverse(sha256(sha256(hash.encode('utf-8')).hexdigest().encode('utf-8')).hexdigest())

www.tecotec.co.jp