Skip to content

Commit edf55cf

Browse files
committed
update posts - 2025-07
1 parent cde7afe commit edf55cf

9 files changed

+1561
-0
lines changed
File renamed without changes.
File renamed without changes.

public/20250701-af9a9224a9913c7e09e7.md

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)