進階
SPV
深入了解簡化支付驗證(SPV)的原理、實現和安全性考慮。
12 分鐘
什麼是 SPV?
簡化支付驗證(Simplified Payment Verification)是中本聰在比特幣白皮書中描述的輕量級驗證方法。 SPV 客戶端只需下載區塊頭(約 60 MB)而非完整區塊鏈(600+ GB),即可驗證交易是否被確認。
全節點 vs SPV
全節點
- • 下載所有區塊(600+ GB)
- • 驗證所有交易和區塊
- • 維護完整 UTXO 集
- • 最高安全性
SPV 客戶端
- • 只下載區塊頭(~60 MB)
- • 驗證區塊頭和 Merkle 證明
- • 依賴全節點提供交易數據
- • 適合移動設備
工作原理
SPV 驗證流程:
1. 同步區塊頭
┌─────────────────────────────────────────────────────────────┐
│ 區塊頭(80 bytes) │
│ ├── version (4 bytes) │
│ ├── prev_block_hash (32 bytes) │
│ ├── merkle_root (32 bytes) ← 用於驗證交易 │
│ ├── timestamp (4 bytes) │
│ ├── bits (4 bytes) │
│ └── nonce (4 bytes) │
└─────────────────────────────────────────────────────────────┘
2. 驗證區塊頭鏈
├── 檢查 PoW(hash < target)
├── 驗證區塊連接(prev_hash)
└── 選擇最長有效鏈
3. 請求交易的 Merkle 證明
├── 從全節點獲取
└── 包含必要的兄弟節點哈希
4. 驗證 Merkle 證明
├── 計算路徑上的哈希
└── 比對區塊頭中的 merkle_root Merkle 證明
Merkle 證明示例:
假設區塊包含 8 筆交易,驗證 Tx2:
Root(在區塊頭中)
/ \
H01 H23
/ \ / \
H0 H1 H2 H3
| | | |
Tx0 Tx1 Tx2* Tx3
驗證 Tx2 需要的證明:
├── Tx2 本身
├── H3 = Hash(Tx3) ← 兄弟節點
├── H01 = Hash(H0 || H1) ← 兄弟節點
└── 計算:
H2 = Hash(Tx2)
H23 = Hash(H2 || H3)
Root = Hash(H01 || H23)
比對區塊頭的 merkle_root
證明大小:O(log n)
├── 8 筆交易 → 3 個哈希
├── 1024 筆交易 → 10 個哈希
└── 每個哈希 32 bytes import hashlib
def double_sha256(data: bytes) -> bytes:
"""Bitcoin 使用的雙重 SHA256"""
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def verify_merkle_proof(tx_hash: bytes, proof: list,
merkle_root: bytes, index: int) -> bool:
"""
驗證 Merkle 證明
tx_hash: 交易哈希
proof: 兄弟節點哈希列表
merkle_root: 區塊頭中的 merkle root
index: 交易在區塊中的位置
"""
current = tx_hash
for sibling in proof:
if index % 2 == 0:
# 當前節點在左邊
current = double_sha256(current + sibling)
else:
# 當前節點在右邊
current = double_sha256(sibling + current)
index //= 2
return current == merkle_root
# 使用示例
is_valid = verify_merkle_proof(
tx_hash=bytes.fromhex("abc123..."),
proof=[bytes.fromhex("..."), bytes.fromhex("...")],
merkle_root=bytes.fromhex("merkle_root_from_header"),
index=2
) Bloom Filters(BIP-37)
BIP-37 Bloom Filter SPV(已棄用):
工作流程:
1. SPV 客戶端創建 Bloom Filter
├── 包含關注的地址/交易
└── 有一定的誤報率
2. 發送 filterload 消息給節點
└── 節點存儲 filter
3. 節點發送匹配的交易
├── 通過 merkleblock 消息
└── 包含交易和 Merkle 證明
問題:
├── 隱私洩露(節點知道你關注什麼)
├── DoS 風險(節點需要為每個客戶端過濾)
└── 誤報可能被利用來追蹤
狀態:
├── Bitcoin Core 已禁用(-peerbloomfilters=0)
└── 被 BIP-157/158 取代 警告: BIP-37 Bloom Filters 存在嚴重隱私問題,已被棄用。請使用 BIP-157/158 Compact Block Filters。
Neutrino(BIP-157/158)
BIP-157/158 Compact Block Filters:
改進的 SPV 方法:
1. 服務器生成區塊過濾器
├── 每個區塊一個固定過濾器
├── 包含區塊中所有 scriptPubKeys
└── 使用 Golomb-Rice 編碼壓縮
2. 客戶端下載過濾器
├── 過濾器承諾在區塊頭中(可驗證)
└── 按需下載相關區塊
3. 客戶端本地匹配
├── 不向服務器透露興趣
└── 保護隱私
優勢:
├── 隱私:服務器不知道客戶端關注什麼
├── 效率:服務器只需一次計算
├── 可驗證:過濾器承諾可驗證
└── 輕量:每個過濾器約 20 KB # Bitcoin Core 啟用區塊過濾器服務
# bitcoin.conf
blockfilterindex=basic
peerblockfilters=1
# 獲取過濾器頭
bitcoin-cli getblockfilter "blockhash" "basic"
# 輸出
{
"filter": "0187a5...",
"header": "7a3dfc..."
}
# 使用 Neutrino 客戶端(如 btcd 的 neutrino)
# 客戶端會:
# 1. 下載所有過濾器頭
# 2. 下載需要的過濾器
# 3. 本地匹配
# 4. 下載匹配的完整區塊 安全性考慮
SPV 安全模型:
假設:
├── 最長鏈代表最多工作量
├── 多數礦工是誠實的
└── 至少連接到一個誠實節點
可驗證:
├── 區塊頭的 PoW
├── 區塊連接正確
├── 交易包含在區塊中(Merkle 證明)
└── 交易有足夠確認數
無法驗證:
├── 交易本身是否有效
├── 輸入是否未被花費
├── 區塊中的其他交易
└── 共識規則
攻擊向量:
1. 隱藏攻擊
└── 礦工不向 SPV 客戶端發送交易
2. 雙花攻擊
└── 礦工創建沖突區塊
3. 無效區塊攻擊
└── 區塊 PoW 有效但內容無效
4. Eclipse 攻擊
└── 隔離客戶端只連接惡意節點 SPV 優勢
- • 極低資源需求
- • 快速同步
- • 適合移動設備
- • 基本交易驗證
SPV 風險
- • 信任假設更多
- • 無法驗證交易有效性
- • 可能被欺騙
- • 隱私較差(取決於實現)
SPV 實現
| 錢包 | 方法 | 隱私 |
|---|---|---|
| Electrum | Electrum Server | 服務器知道地址 |
| Bitcoin Wallet (Android) | BIP-37 Bloom | 較差 |
| Breez | Neutrino | 較好 |
| Phoenix | Electrum | 服務器知道地址 |
| Wasabi | BIP-157/158 | 較好 |
Electrum 協議
Electrum 架構:
┌─────────────────────────────────────────────────────────────┐
│ Electrum Client ←→ Electrum Server ←→ Bitcoin Core │
└─────────────────────────────────────────────────────────────┘
特點:
├── 服務器維護地址索引
├── 客戶端通過 JSON-RPC 查詢
├── 支持訂閱地址更新
└── 快速交易廣播
隱私考慮:
├── 服務器知道你的所有地址
├── 可以關聯地址到 IP
└── 建議運行自己的服務器
運行自己的 Electrum Server:
├── electrs(Rust)
├── ElectrumX(Python)
└── Fulcrum(C++) # Electrum 協議示例
import socket
import json
def electrum_request(method, params=[]):
"""發送 Electrum 請求"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("electrum.blockstream.info", 50002))
# 使用 SSL 包裝
import ssl
sock = ssl.wrap_socket(sock)
request = {
"id": 1,
"method": method,
"params": params
}
sock.send((json.dumps(request) + "\n").encode())
response = sock.recv(4096).decode()
sock.close()
return json.loads(response)
# 獲取地址餘額
result = electrum_request(
"blockchain.scripthash.get_balance",
["scripthash_of_address"]
)
# 獲取交易歷史
history = electrum_request(
"blockchain.scripthash.get_history",
["scripthash_of_address"]
) 建議
SPV 使用建議:
1. 小額交易
├── SPV 足夠安全
└── 等待更多確認
2. 大額交易
├── 使用全節點驗證
└── 或等待 6+ 確認
3. 隱私敏感
├── 使用 BIP-157/158(Neutrino)
├── 運行自己的後端服務器
└── 通過 Tor 連接
4. 最佳實踐
├── 連接多個節點
├── 驗證區塊頭鏈
├── 等待足夠確認
└── 考慮運行全節點 總結
- ✓ 輕量級:只需區塊頭和 Merkle 證明
- ✓ Neutrino:BIP-157/158 改進隱私
- ✓ 適用:移動設備和資源受限環境
- ⚠ 權衡:安全性低於全節點,大額交易建議全節點
已複製連結