5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Git や Blockchain のような分散システムでは、大量のデータの整合性を効率よく証明する仕組みが欠かせません。その中核を支えるのが Merkle Tree です。

前回までの記事では、包含証明とローカル更新を Elixir で体験しました。

今回はもう一歩踏み込み、奇数ノードをどう扱うかという実装上の課題に注目します。

Bitcoin の初期実装では、ノード数が奇数だった場合、最後のノードを複製してペアを作るという方式を採用していました。

[A] [B] [C]  → [A,B] [C,C]

一見すると自然な処理に見えますが、のちにCVE-2012-2459として重大な脆弱性が報告されました。

CVE-2012-2459: 末尾複製の落とし穴

末尾ノードをコピーして (C, C) のようなペアを作ると、異なるデータ集合でも同じ Merkle Root が生成されるリスクがあります。
これはデータ整合性を保証する仕組みとしては致命的であり、
場合によっては不正に作られたブロックが正当と見なされることすらあり得ます。

末尾複製あり vs 末尾複製なし

① バランス木(末尾複製あり、非推奨)

defp build_tree(level) do
  padded =
    if rem(length(level), 2) == 1 do
      level ++ [List.last(level)]   # 末尾を複製
    else
      level
    end

  parent =
    padded
    |> Enum.chunk_every(2)
    |> Enum.map(fn [a, b] -> :crypto.hash(:sha256, a <> b) end)

  [level | build_tree(parent)]
end
[A] [B] [C]     ← 3 ノード(奇数)

[A,B] [C,C]     ← C を複製してペア化
   │     │
hash    hash    ← (C+C) は常に同じ値
  │     │
  └──┬──┘
     ▼
    Root

末尾複製方式は実装が簡単という利点がある一方で、同じノードをペアにする (C, C) のような構造を生み出しやすく、これが固定されたハッシュ値を持つため、衝突の温床になってしまいます。

② アンバランス木(末尾複製なし、推奨)

defp build_tree(level) do
  parent =
    level
    |> Enum.chunk_every(2, 2, :discard) # 偶数ペアだけ
    |> Enum.map(fn [a, b] -> :crypto.hash(:sha256, a <> b) end)

  next_level =
    if rem(length(level), 2) == 1 do
      parent ++ [List.last(level)] # 余りをそのまま持ち上げ
    else
      parent
    end

  [level | build_tree(next_level)]
end
[A] [B] [C]     ← 3 ノード(奇数)

[A,B]   [C]     ← C は複製せず複製せず持ち上げる
  │      │
hash     │
  │      │
  └──┬───┘
     ▼
    Root

アンバランス木では、末尾ノードを複製せずにそのまま次の階層へ持ち上げるため、同じノード同士の組み合わせを避けることで、ハッシュ衝突のリスクを軽減し、より安全な構造を実現できます。

Elixir で実装するアンバランス木 Merkle Tree

defmodule MerkleTree do
  defstruct root: nil, leaf_hashes: []

  def new(data_blocks) when is_list(data_blocks) do
    leaf_hashes = Enum.map(data_blocks, &:crypto.hash(:sha256, &1))

    levels = build_tree(leaf_hashes)
    root = List.first(List.last(levels))

    %MerkleTree{
      root: Base.encode16(root, case: :lower),
      leaf_hashes: leaf_hashes
    }
  end

  defp build_tree([hash]), do: [[hash]]
  
  defp build_tree(current_level_hashes) do
    parent_level_hashes =
      current_level_hashes
      |> Enum.chunk_every(2, 2, :discard)
      |> Enum.map(fn [a, b] -> :crypto.hash(:sha256, a <> b) end)

    next_level_hashes =
      if rem(length(current_level_hashes), 2) == 1 do
        parent_level_hashes ++ [List.last(current_level_hashes)]
      else
        parent_level_hashes
      end

    [current_level_hashes | build_tree(next_level_hashes)]
  end
end

動かしてみる

# 奇数ノード(a, b, c)
tree_odd = MerkleTree.new(["a", "b", "c"])

# 末尾複製を模したデータ
tree_dup = MerkleTree.new(["a", "b", "c", "c"])

# ルートが必ず異なることを確認
tree_odd.root != tree_dup.root
#=> true

おわりに

奇数ノード対応としてよく使われる末尾複製には、CVE-2012-2459 にもつながる衝突リスクがあることを学びました。

一方、末尾を複製せずにそのまま持ち上げるアンバランス木は、より安全で、実装も比較的簡単です。

Elixir なら Enum.chunk_every/4 を使って直感的に書けるので、仕組みを理解しながら手を動かすにはぴったりです。

5
1
0

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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?