跳至主要內容
進階

Bloom Filters

BIP-37 布隆過濾器:SPV 客戶端交易過濾與隱私問題

15 分鐘

概述

布隆過濾器(Bloom Filter)是一種空間效率極高的機率性資料結構, 用於測試元素是否屬於某個集合。BIP-37 將其引入比特幣, 讓輕量級客戶端(SPV)能夠只接收與自己相關的交易。

⚠️ 已棄用: 由於嚴重的隱私問題,BIP-37 布隆過濾器已被 BIP-157/158 Compact Block Filters 取代。 Bitcoin Core 默認禁用 bloom filter 服務。

資料結構

布隆過濾器原理

布隆過濾器結構:
┌─────────────────────────────────────────────────────┐
│ Bit Array (m bits)                                  │
│ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ │
│ │0│1│0│0│1│0│0│1│0│0│0│1│0│0│0│0│1│0│0│1│0│0│0│1│ │
│ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ │
│   ↑       ↑       ↑                               │
│   │       │       │                               │
│   └───────┼───────┘                               │
│           │                                        │
│     k 個雜湊函數                                    │
└─────────────────────────────────────────────────────┘

插入元素:
1. 對元素計算 k 個雜湊值
2. 將對應的 k 個位元設為 1

查詢元素:
1. 計算 k 個雜湊值
2. 檢查對應位元是否全為 1
   - 全為 1: 可能存在 (有誤判率)
   - 有任何 0: 絕對不存在

特性:
- 不會漏報 (false negative = 0)
- 可能誤判 (false positive > 0)
- 無法刪除元素

參數設計

布隆過濾器參數:

m = 過濾器大小 (bits)
n = 預期元素數量
k = 雜湊函數數量
p = 誤判率 (false positive rate)

最佳雜湊函數數量:
k = (m/n) × ln(2) ≈ 0.693 × (m/n)

誤判率計算:
p ≈ (1 - e^(-kn/m))^k

給定 n 和 p,計算所需大小:
m = -n × ln(p) / (ln(2))²

範例:
- n = 100 個元素
- p = 0.01 (1% 誤判率)
- m ≈ 958 bits ≈ 120 bytes
- k ≈ 7 個雜湊函數

BIP-37 規範

過濾器結構

BIP-37 Bloom Filter:

┌─────────────────────────────────────┐
│ nFilterBytes (VarInt)               │
├─────────────────────────────────────┤
│ filter[nFilterBytes] (bytes)        │
├─────────────────────────────────────┤
│ nHashFuncs (uint32)                 │
├─────────────────────────────────────┤
│ nTweak (uint32)                     │
├─────────────────────────────────────┤
│ nFlags (uint8)                      │
└─────────────────────────────────────┘

限制:
- 最大大小: 36,000 bytes
- 最大雜湊函數: 50
- 如果 filter 為空且 nHashFuncs = 0,匹配所有交易

nFlags 選項:
- BLOOM_UPDATE_NONE (0): 不自動更新
- BLOOM_UPDATE_ALL (1): 匹配時加入 outpoint
- BLOOM_UPDATE_P2PUBKEY_ONLY (2): 僅 P2PK/P2MS 時更新

雜湊函數

// BIP-37 使用 MurmurHash3 雜湊函數
function murmurHash3(data: Uint8Array, seed: number): number {
  const c1 = 0xcc9e2d51;
  const c2 = 0x1b873593;
  const r1 = 15;
  const r2 = 13;
  const m = 5;
  const n = 0xe6546b64;

  let hash = seed;
  const len = data.length;
  const nblocks = Math.floor(len / 4);

  // Process 4-byte blocks
  for (let i = 0; i < nblocks; i++) {
    let k = data[i * 4] |
            (data[i * 4 + 1] << 8) |
            (data[i * 4 + 2] << 16) |
            (data[i * 4 + 3] << 24);

    k = Math.imul(k, c1);
    k = (k << r1) | (k >>> (32 - r1));
    k = Math.imul(k, c2);

    hash ^= k;
    hash = (hash << r2) | (hash >>> (32 - r2));
    hash = Math.imul(hash, m) + n;
  }

  // Process remaining bytes
  let k1 = 0;
  const tailIndex = nblocks * 4;
  switch (len & 3) {
    case 3: k1 ^= data[tailIndex + 2] << 16;
    case 2: k1 ^= data[tailIndex + 1] << 8;
    case 1:
      k1 ^= data[tailIndex];
      k1 = Math.imul(k1, c1);
      k1 = (k1 << r1) | (k1 >>> (32 - r1));
      k1 = Math.imul(k1, c2);
      hash ^= k1;
  }

  // Finalization
  hash ^= len;
  hash ^= hash >>> 16;
  hash = Math.imul(hash, 0x85ebca6b);
  hash ^= hash >>> 13;
  hash = Math.imul(hash, 0xc2b2ae35);
  hash ^= hash >>> 16;

  return hash >>> 0;
}

// BIP-37 雜湊函數生成
function bloomHash(
  data: Uint8Array,
  hashNum: number,
  tweak: number,
  filterSize: number
): number {
  const seed = (hashNum * 0xfba4c795 + tweak) >>> 0;
  return murmurHash3(data, seed) % (filterSize * 8);
}

TypeScript 實作

布隆過濾器類別

class BloomFilter {
  private filter: Uint8Array;
  private nHashFuncs: number;
  private nTweak: number;
  private nFlags: number;

  constructor(
    nElements: number,
    fpRate: number,
    nTweak: number = 0,
    nFlags: number = 0
  ) {
    // 計算最佳過濾器大小
    const LN2SQUARED = 0.4804530139182014246671025263266649717305529515945455;
    const LN2 = 0.6931471805599453094172321214581765680755001343602552;

    let nFilterBytes = Math.floor(
      Math.min(
        (-1.0 / LN2SQUARED * nElements * Math.log(fpRate)) / 8.0,
        36000 // MAX_BLOOM_FILTER_SIZE
      )
    );
    nFilterBytes = Math.max(1, nFilterBytes);

    this.filter = new Uint8Array(nFilterBytes);

    // 計算最佳雜湊函數數量
    this.nHashFuncs = Math.floor(
      Math.min(
        (this.filter.length * 8 / nElements) * LN2,
        50 // MAX_HASH_FUNCS
      )
    );
    this.nHashFuncs = Math.max(1, this.nHashFuncs);

    this.nTweak = nTweak;
    this.nFlags = nFlags;
  }

  // 插入元素
  insert(data: Uint8Array): void {
    if (this.filter.length === 0) return;

    for (let i = 0; i < this.nHashFuncs; i++) {
      const index = bloomHash(
        data,
        i,
        this.nTweak,
        this.filter.length
      );
      this.filter[Math.floor(index / 8)] |= (1 << (index % 8));
    }
  }

  // 查詢元素
  contains(data: Uint8Array): boolean {
    if (this.filter.length === 0) return true; // 空過濾器匹配所有

    for (let i = 0; i < this.nHashFuncs; i++) {
      const index = bloomHash(
        data,
        i,
        this.nTweak,
        this.filter.length
      );
      if (!(this.filter[Math.floor(index / 8)] & (1 << (index % 8)))) {
        return false;
      }
    }
    return true;
  }

  // 序列化
  serialize(): Uint8Array {
    const result = new Uint8Array(
      this.filter.length + 4 + 4 + 1 + 3 // filter + funcs + tweak + flags + varint
    );

    let offset = 0;

    // Filter length (varint)
    const filterLen = encodeVarInt(this.filter.length);
    result.set(filterLen, offset);
    offset += filterLen.length;

    // Filter data
    result.set(this.filter, offset);
    offset += this.filter.length;

    // nHashFuncs
    new DataView(result.buffer).setUint32(offset, this.nHashFuncs, true);
    offset += 4;

    // nTweak
    new DataView(result.buffer).setUint32(offset, this.nTweak, true);
    offset += 4;

    // nFlags
    result[offset] = this.nFlags;

    return result.slice(0, offset + 1);
  }
}

// 使用範例
const filter = new BloomFilter(
  10,     // 預期 10 個元素
  0.0001, // 0.01% 誤判率
  0,      // tweak
  1       // BLOOM_UPDATE_ALL
);

// 加入地址
const address = hexToBytes('76a914...88ac');
filter.insert(address);

// 加入公鑰
const pubkey = hexToBytes('02...');
filter.insert(pubkey);

// 檢查是否匹配
console.log(filter.contains(address)); // true
console.log(filter.contains(otherData)); // 可能 false 或 true (誤判)

P2P 訊息

filterload

filterload 訊息:
設定節點的布隆過濾器

┌─────────────────────────────────────┐
│ filter (VarBytes)                   │
├─────────────────────────────────────┤
│ nHashFuncs (uint32)                 │
├─────────────────────────────────────┤
│ nTweak (uint32)                     │
├─────────────────────────────────────┤
│ nFlags (uint8)                      │
└─────────────────────────────────────┘

效果:
- 節點只會發送匹配過濾器的交易
- 節點會發送 merkleblock 而非完整區塊
- 之前設定的過濾器會被替換

filteradd

filteradd 訊息:
向現有過濾器加入元素

┌─────────────────────────────────────┐
│ data (VarBytes, max 520 bytes)      │
└─────────────────────────────────────┘

用途:
- 動態更新過濾器
- 加入新發現的地址或公鑰
- 不需要重新傳送整個過濾器

filterclear

filterclear 訊息:
清除布隆過濾器

- 無內容
- 恢復接收所有交易
- 停止使用 merkleblock

merkleblock

merkleblock 訊息:
只包含匹配交易的 Merkle 證明

┌─────────────────────────────────────┐
│ block_header (80 bytes)             │
├─────────────────────────────────────┤
│ total_transactions (uint32)         │
├─────────────────────────────────────┤
│ hashes (VarInt + 32-byte[])         │
├─────────────────────────────────────┤
│ flags (VarBytes)                    │
└─────────────────────────────────────┘

Merkle 樹遍歷:
         Root
        /    \
      AB      CD
     / \     / \
    A   B   C   D  ← 交易雜湊

如果只有 B 匹配:
- hashes: [A, B, CD]
- flags: 標記遍歷路徑

交易匹配

匹配規則

BIP-37 交易匹配規則:

1. 測試 TXID
   - SHA256(SHA256(tx)) 是否匹配

2. 對每個輸出:
   - 測試 scriptPubKey 的每個 data push
   - 包括公鑰、公鑰雜湊、腳本雜湊等

3. 對每個輸入:
   - 測試 previous outpoint (txid + index)
   - 測試 scriptSig 的每個 data push
   - 測試 witness 的每個 data push

4. 如果輸出匹配且 nFlags != NONE:
   - 自動將該 outpoint 加入過濾器
   - 確保能匹配後續花費此輸出的交易

匹配優先級:
TX → Outputs → Inputs

SPV 客戶端流程

class SPVClient {
  private filter: BloomFilter;
  private addresses: Set<string>;
  private watchedOutpoints: Set<string>;

  constructor() {
    // 初始化過濾器
    this.filter = new BloomFilter(100, 0.0001);
    this.addresses = new Set();
    this.watchedOutpoints = new Set();
  }

  // 加入監控地址
  addAddress(address: string): void {
    const decoded = decodeAddress(address);
    this.filter.insert(decoded.hash);
    this.addresses.add(address);
  }

  // 連接節點後發送過濾器
  async onConnect(peer: Peer): Promise<void> {
    // 設定過濾器
    await peer.send('filterload', this.filter.serialize());

    // 請求區塊頭
    await peer.send('getheaders', {
      locator: this.getBlockLocator(),
      stop: ZERO_HASH
    });
  }

  // 處理 merkleblock
  async onMerkleBlock(
    header: BlockHeader,
    hashes: string[],
    flags: Uint8Array
  ): Promise<void> {
    // 驗證 Merkle 證明
    const { matches, merkleRoot } = this.verifyMerkleProof(
      hashes,
      flags,
      header.merkleRoot
    );

    if (merkleRoot !== header.merkleRoot) {
      throw new Error('Invalid merkle proof');
    }

    // 記錄匹配的交易
    for (const txid of matches) {
      this.pendingTxs.add(txid);
    }
  }

  // 處理匹配的交易
  async onTransaction(tx: Transaction): Promise<void> {
    // 檢查輸出
    for (let i = 0; i < tx.outputs.length; i++) {
      const output = tx.outputs[i];
      if (this.isOurScript(output.scriptPubKey)) {
        // 記錄 UTXO
        this.utxos.add({
          txid: tx.txid,
          vout: i,
          value: output.value,
          script: output.scriptPubKey
        });

        // 更新過濾器以追蹤花費
        const outpoint = `${tx.txid}:${i}`;
        this.watchedOutpoints.add(outpoint);
      }
    }

    // 檢查輸入 (偵測花費)
    for (const input of tx.inputs) {
      const outpoint = `${input.txid}:${input.vout}`;
      if (this.watchedOutpoints.has(outpoint)) {
        // 我們的 UTXO 被花費了
        this.utxos.delete(outpoint);
      }
    }
  }
}

隱私問題

地址關聯

隱私洩漏分析:

問題 1: 過濾器直接暴露興趣
┌─────────────────────────────────────────────┐
│ SPV Client                 Full Node        │
│     │                          │            │
│     │ ──── filterload ────▶    │            │
│     │   (包含地址雜湊)           │            │
│     │                          │            │
│     │         節點知道:                      │
│     │         - 客戶端的所有地址             │
│     │         - 地址之間的關聯性             │
│     │         - 錢包餘額估計                 │
└─────────────────────────────────────────────┘

問題 2: 時序分析
- 新地址加入時發送 filteradd
- 可以追蹤錢包的活動模式
- 交易時間與過濾器更新相關聯

問題 3: 誤判率過低
- 0.0001% 誤判率 ≈ 幾乎無誤判
- 節點可以確定過濾器內容
- 無法獲得 "人群中隱藏" 的效果

攻擊向量

攻擊 1: 過濾器反推

惡意節點可以:
1. 收集所有已知的公鑰/地址
2. 對每個測試是否匹配過濾器
3. 重建客戶端的地址列表

成功條件:
- 誤判率低時幾乎 100% 準確
- 地址數據庫越大越準確

攻擊 2: 交易圖分析

透過觀察:
- 哪些交易被請求
- filteradd 的時機
- 多次連接的模式變化

可以:
- 連結多個地址到同一錢包
- 追蹤資金流向
- 估計錢包餘額

攻擊 3: Sybil 攻擊

惡意運營商:
1. 運行多個全節點
2. 收集不同時間的過濾器
3. 比對分析以消除誤判
4. 構建完整的用戶檔案

緩解措施(不足)

嘗試的緩解措施:

1. 提高誤判率
   - 加入更多假陽性
   - 問題: 增加頻寬消耗
   - 問題: 仍可被統計分析

2. 使用多個節點
   - 每個節點用不同過濾器
   - 問題: 複雜度增加
   - 問題: Sybil 攻擊仍有效

3. 加入假地址
   - 在過濾器中插入無關地址
   - 問題: 很難生成「看起來真實」的假地址
   - 問題: 節點可能識別出假地址

結論: BIP-37 的設計本質上無法保護隱私

棄用狀態

Bitcoin Core 的處理:

v0.19.0 (2019年11月):
- 默認禁用 bloom filter 服務
- 需要 -peerbloomfilters=1 啟用

v0.21.0 (2021年1月):
- 移除 wallet 的 bloom filter 功能
- 推薦使用 BIP-157/158

替代方案:
- BIP-157/158: Compact Block Filters (Neutrino)
- 自建全節點或信任的 Electrum 服務器
- 使用 SPV 模式但接受隱私風險

服務位標記:
- NODE_BLOOM (1 << 2) = 支持 bloom filter
- NODE_COMPACT_FILTERS (1 << 6) = 支持 compact filters

與 Compact Block Filters 比較

特性 BIP-37 Bloom BIP-157/158 Compact
隱私保護 差 (暴露地址) 好 (客戶端下載)
頻寬使用 低 (按需) 中 (下載過濾器)
伺服器負擔 高 (計算匹配) 低 (預計算)
DoS 抗性
誤判處理 伺服器端 客戶端
狀態 已棄用 推薦使用

相關資源

已複製連結
已複製到剪貼簿