|
| 1 | +--- |
| 2 | +title: 「全部まとめてハッシュ」では不十分?Elixir で学ぶ Merkle Tree の強み |
| 3 | +tags: |
| 4 | + - Elixir |
| 5 | + - Bitcoin |
| 6 | + - Blockchain |
| 7 | + - MerkleTree |
| 8 | + - Tapyrus |
| 9 | +private: false |
| 10 | +updated_at: '2025-07-07T21:30:58+09:00' |
| 11 | +id: ab1049c01800594f2040 |
| 12 | +organization_url_name: fukuokaex |
| 13 | +slide: false |
| 14 | +ignorePublish: false |
| 15 | +--- |
| 16 | + |
| 17 | +## はじめに |
| 18 | + |
| 19 | +Git や Blockchain のようなシステムでは、大量のデータの中から「特定のデータが改ざんされていないこと」や「あるデータが確かに存在していたこと」を効率よく証明する必要があります。 |
| 20 | + |
| 21 | +こうした用途に使われているのが Merkle Tree(マークルツリー) です。 |
| 22 | + |
| 23 | +本記事では、以下についてElixirコードを書きながら学んでいきます。 |
| 24 | + |
| 25 | +* 単一ハッシュ方式とその限界 |
| 26 | +* Merkle Tree の仕組みと利点 |
| 27 | +* Elixir での実装と可視化・検証 |
| 28 | + |
| 29 | +## 単一ハッシュ方式 |
| 30 | + |
| 31 | +最もシンプルな整合性チェックの方法は、すべてのデータを連結して一括でハッシュすることです。 |
| 32 | + |
| 33 | +たとえば、複数のファイル(テキスト、画像、動画など)をすべて結合し、SHA-256 を一度だけ計算するという方式です。 |
| 34 | + |
| 35 | +```elixir |
| 36 | +data_blocks = ["foo", "bar", "baz"] |
| 37 | +all_data = Enum.join(data_blocks) |
| 38 | +big_hash = :crypto.hash(:sha256, all_data) |> Base.encode16(case: :lower) |
| 39 | +IO.puts("Big Hash: #{big_hash}") |
| 40 | +``` |
| 41 | + |
| 42 | +図にすると以下のようになります: |
| 43 | + |
| 44 | +``` |
| 45 | +[ "foo" ] [ "bar" ] [ "baz" ] |
| 46 | + ↓ ↓ ↓ |
| 47 | + ────────────────────────────── |
| 48 | + データをすべて連結 |
| 49 | + ────────────────────────────── |
| 50 | + ↓ |
| 51 | + SHA256("foobarbaz") → 単一ハッシュ |
| 52 | +``` |
| 53 | + |
| 54 | +### メリット |
| 55 | + |
| 56 | +* 実装が非常にシンプル |
| 57 | +* データ全体の整合性を一度に検証できる |
| 58 | + |
| 59 | +### 限界と課題 |
| 60 | + |
| 61 | +* **証明ができない** |
| 62 | + 特定のデータが含まれていたことを証明するには、全体のデータを再送・再ハッシュする必要がある |
| 63 | +* **部分更新が非効率** |
| 64 | + 一部のデータが変更されると、全体を再ハッシュする必要がある |
| 65 | +* **メモリや I/O に弱い** |
| 66 | + 大規模データをすべてメモリに載せる必要があり、スパイクの原因になる可能性がある |
| 67 | + |
| 68 | +## Merkle Tree 方式 |
| 69 | + |
| 70 | +Merkle Tree は、データをハッシュのツリー構造で管理し、効率的な整合性チェックと部分証明を可能にします。 |
| 71 | + |
| 72 | +### 基本構造 |
| 73 | + |
| 74 | +1. 各データブロックを個別にハッシュする(葉ノード) |
| 75 | +2. 隣り合うハッシュを結合して再ハッシュ(中間ノード) |
| 76 | +3. 最後に 1 つのルートノード(Merkle Root)を得る |
| 77 | + |
| 78 | +```elixir |
| 79 | +data_blocks = ["foo", "bar", "baz", "qux"] |
| 80 | +tree = MerkleTree.new(data_blocks) |
| 81 | +IO.puts("Merkle Root: #{tree.root}") |
| 82 | +``` |
| 83 | + |
| 84 | +``` |
| 85 | + [ Root ] |
| 86 | + ↑ |
| 87 | + ╱ ╲ |
| 88 | + [H1 + H2] [H3 + H4] |
| 89 | + ↑ ↑ ↑ ↑ |
| 90 | + [H1] [H2] [H3] [H4] |
| 91 | + ↑ ↑ ↑ ↑ |
| 92 | + "foo" "bar" "baz" "qux" |
| 93 | +``` |
| 94 | + |
| 95 | +### メリット |
| 96 | + |
| 97 | +* **部分証明が可能** |
| 98 | + `"baz"` が含まれていたことを、兄弟ノードのハッシュのみで証明できる(全データは不要) |
| 99 | +* **局所的な更新が可能** |
| 100 | + `"baz"` の内容が変更されても、該当ブランチのみを再ハッシュすればよい |
| 101 | +* **固定サイズのルート** |
| 102 | + データ件数が 4 件でも 400 万件でも、ルートハッシュは常に 32 バイト(SHA-256) |
| 103 | + |
| 104 | +https://qiita.com/mnishiguchi/items/bef008fb55d6e55e5e11 |
| 105 | + |
| 106 | +https://qiita.com/mnishiguchi/items/c161613aec53f27f3e5b |
| 107 | + |
| 108 | +### 限界と課題 |
| 109 | + |
| 110 | +* **実装がやや複雑** |
| 111 | + ペアのハッシュ処理や奇数ノード対応、証明パスの生成など、単一ハッシュ方式と比べてロジックが増える |
| 112 | +* **証明のための構造を保持する必要がある** |
| 113 | + 後から証明を行うには、途中のノード情報(証明経路)を保持・管理する仕組みが必要になる |
| 114 | + |
| 115 | +## 単一ハッシュ方式 vs. Merkle Tree の比較 |
| 116 | + |
| 117 | +| 項目 | 単一ハッシュ方式 | Merkle Tree | |
| 118 | +| ------------ | ----------------------- | -------------------------- | |
| 119 | +| 証明サイズ | O(n) — 全データの再送・再ハッシュが必要 | O(log n) — 兄弟ノードのみで証明可能 | |
| 120 | +| 更新コスト | O(n) — 全体を再ハッシュ | O(log n) — 一部のブランチのみ再ハッシュ | |
| 121 | +| メモリ / I/O 負荷 | 高 — 全データを結合 | 低 — ペア単位で処理、ストリーム処理可能 | |
| 122 | +| 並列処理のしやすさ | 難しい — 結合がボトルネック | 容易 — 葉・中間ノード単位で並列処理可能 | |
| 123 | +| 実装の手軽さ | 非常に簡単(2〜3 行) | やや複雑(ツリー構築・証明ロジックが必要) | |
| 124 | +| 主なユースケース | チェックサム、簡易整合性検証 | Git、Bitcoin、Tapyrus、監査ログなど | |
| 125 | + |
| 126 | +### 使い分けの目安 |
| 127 | + |
| 128 | +#### 単一ハッシュ方式が向いている場合 |
| 129 | + |
| 130 | +* 小規模かつ静的なデータを対象とする |
| 131 | +* 全体の整合性を一括で確認したい |
| 132 | +* 証明・部分更新などが不要な場合 |
| 133 | + |
| 134 | +#### Merkle Tree が向いている場合 |
| 135 | + |
| 136 | +* 個別データの存在証明が必要 |
| 137 | +* 部分更新・局所的な検証を効率化したい |
| 138 | +* 分散処理やチェーン構造など、スケーラブルな設計を求められる場合 |
| 139 | + |
| 140 | +## Elixir で体験する 2 つの方式 |
| 141 | + |
| 142 | +ここからは、Elixir を使って両者を実際に比較してみます。 |
| 143 | + |
| 144 | +### サンプルデータの準備 |
| 145 | + |
| 146 | +まずは、テキスト・画像・動画を模した 3 種類のデータを用意します。 |
| 147 | + |
| 148 | +```elixir |
| 149 | +text_data = "闘魂とは己に打ち克つこと" |
| 150 | +image_bytes = :crypto.strong_rand_bytes(1024) # 1KB の疑似画像データ |
| 151 | +video_bytes = :crypto.strong_rand_bytes(2048) # 2KB の疑似動画データ |
| 152 | + |
| 153 | +data_list = [text_data, image_bytes, video_bytes] |
| 154 | +``` |
| 155 | + |
| 156 | +### Merkle Tree モジュールを定義 |
| 157 | + |
| 158 | +次に、簡易的な Merkle Tree を構築・検証できるモジュールを定義します。 |
| 159 | + |
| 160 | +```elixir |
| 161 | +defmodule MerkleTree do |
| 162 | + defstruct root: nil, leaf_hashes: [] |
| 163 | + |
| 164 | + def new(data_blocks) do |
| 165 | + leaf_hashes = |
| 166 | + data_blocks |
| 167 | + |> Enum.map(&(:crypto.hash(:sha256, &1))) |
| 168 | + |> pad_if_odd() |
| 169 | + |
| 170 | + levels = build_tree(leaf_hashes) |
| 171 | + root_hash = List.first(List.last(levels)) |
| 172 | + |
| 173 | + %MerkleTree{ |
| 174 | + root: Base.encode16(root_hash, case: :lower), |
| 175 | + leaf_hashes: List.first(levels) |
| 176 | + } |
| 177 | + end |
| 178 | + |
| 179 | + def verify(%MerkleTree{leaf_hashes: leaves}, item) do |
| 180 | + hash = :crypto.hash(:sha256, item) |
| 181 | + Enum.any?(leaves, &(&1 == hash)) |
| 182 | + end |
| 183 | + |
| 184 | + defp build_tree([hash]), do: [[hash]] |
| 185 | + defp build_tree(level) do |
| 186 | + level = pad_if_odd(level) |
| 187 | + |
| 188 | + next_level = |
| 189 | + level |
| 190 | + |> Enum.chunk_every(2) |
| 191 | + |> Enum.map(fn [a, b] -> :crypto.hash(:sha256, a <> b) end) |
| 192 | + |
| 193 | + [level | build_tree(next_level)] |
| 194 | + end |
| 195 | + |
| 196 | + defp pad_if_odd(list) do |
| 197 | + if rem(length(list), 2) == 1 do |
| 198 | + list ++ [List.last(list)] |
| 199 | + else |
| 200 | + list |
| 201 | + end |
| 202 | + end |
| 203 | +end |
| 204 | +``` |
| 205 | + |
| 206 | +### 単一ハッシュの計算 |
| 207 | + |
| 208 | +データをすべて結合し、1 回の SHA-256 ハッシュで全体の整合性を確認します。 |
| 209 | + |
| 210 | +```elixir |
| 211 | +big_hash = |
| 212 | + data_list |
| 213 | + |> Enum.reduce(<<>>, fn chunk, acc -> |
| 214 | + acc <> chunk |
| 215 | + end) |
| 216 | + |> then(fn merged_data -> |
| 217 | + :crypto.hash(:sha256, merged_data) |
| 218 | + |> Base.encode16(case: :lower) |
| 219 | + end) |
| 220 | +``` |
| 221 | + |
| 222 | +```elixir:出力例 |
| 223 | +"07044c2ec18d57901a9e1a61c6c5c02c3962ec230085eefa7ed7b7c662f1ce55" |
| 224 | +``` |
| 225 | + |
| 226 | +### Merkle Tree の構築とルートハッシュの取得 |
| 227 | + |
| 228 | +同じデータを使って Merkle Tree を構築し、ルートハッシュ(Merkle Root)を確認してみます。 |
| 229 | + |
| 230 | +```elixir |
| 231 | +tree = |
| 232 | + data_list |
| 233 | + |> MerkleTree.new() |
| 234 | +``` |
| 235 | + |
| 236 | +```elixir:出力例 |
| 237 | +%MerkleTree{ |
| 238 | + root: "5188ebac04f78d5c8b5b880cc74b9fdf5876761e60495c9b42ee962b30b2e9fb", |
| 239 | + leaf_hashes: [ |
| 240 | + <<140, 149, 141, 136, 189, 206, 189, 249, 26, 80, 170, 128, 161, 22, 142, 47, 200, 15, 232, 168, |
| 241 | + 179, 84, 172, 15, 47, 242, 178, 89, 110, 125, 27, 98>>, |
| 242 | + <<136, 107, 185, 62, 103, 206, 96, 22, 96, 207, 99, 193, 24, 114, 97, 243, 35, 3, 201, 8, 115, |
| 243 | + 212, 107, 60, 220, 244, 250, 114, 137, 226, 91, 114>>, |
| 244 | + <<117, 161, 124, 206, 230, 137, 13, 72, 170, 171, 205, 209, 149, 76, 132, 94, 134, 173, 48, 139, |
| 245 | + 145, 55, 179, 243, 50, 73, 120, 53, 150, 170, 11, 48>>, |
| 246 | + <<117, 161, 124, 206, 230, 137, 13, 72, 170, 171, 205, 209, 149, 76, 132, 94, 134, 173, 48, 139, |
| 247 | + 145, 55, 179, 243, 50, 73, 120, 53, 150, 170, 11, 48>> |
| 248 | + ] |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +### 特定データの存在検証 |
| 253 | + |
| 254 | +最後に、Merkle Tree を使って「あるデータが含まれていたかどうか」を検証してみます。 |
| 255 | + |
| 256 | +```elixir |
| 257 | +MerkleTree.verify(tree, "闘魂とは己に打ち克つこと") |
| 258 | +#=> true |
| 259 | + |
| 260 | +MerkleTree.verify(tree, "もし負けるということがあると") |
| 261 | +#=> false |
| 262 | +``` |
| 263 | + |
| 264 | +このように、Elixir を使うことで Merkle Tree の構造やメリット を手軽に体感することができます。 |
| 265 | + |
| 266 | +## おわりに |
| 267 | + |
| 268 | +本記事では、データの整合性を担保する手法として次の 2 つを比較し、それぞれを Elixir で実装・検証してみました。 |
| 269 | + |
| 270 | +* 単一ハッシュ方式: 全データを連結して 1 回ハッシュするシンプルな方法 |
| 271 | +* Merkle Tree 方式: ツリー構造により部分証明や局所的な更新を可能にする構造化アプローチ |
| 272 | + |
| 273 | +それぞれの特徴を簡潔にまとめると、次のようになります: |
| 274 | + |
| 275 | + |
| 276 | +| 観点 | 単一ハッシュ方式 | Merkle Tree | |
| 277 | +| --- | -------------- | ------------------------ | |
| 278 | +| 構造 | フラットな 1 ハッシュ | 階層的なツリー構造 | |
| 279 | +| 更新 | 全再計算が必要 | 一部再計算で済む(log n) | |
| 280 | +| 証明 | 不可 | コンパクトに証明可能 | |
| 281 | +| 活用例 | チェックサム、ファイル整合性 | Git、Bitcoin、Tapyrus、監査ログなど | |
| 282 | + |
| 283 | +| シナリオ | 推奨方式 | |
| 284 | +| ---------------------- | ------------- | |
| 285 | +| 小さなデータの整合性をシンプルに確認したい | 単一ハッシュ方式 | |
| 286 | +| データの一部を証明・部分更新したい | Merkle Tree | |
| 287 | +| 分散台帳や改ざん検知などスケーラブルな用途に | Merkle Tree | |
| 288 | + |
| 289 | +このように、Merkle Tree は構造化された整合性・証明の基盤として非常に優れており、Elixir のような関数型言語でも実装しやすいことがわかりました。 |
0 commit comments