比特幣隱私技術深度實作教程:從 CoinJoin 到 Silent Payments,程式碼範例與風險分析

完整的比特幣隱私保護實作教程,深入解析 CoinJoin、PayJoin、Silent Payments 等技術的原理與程式碼實作,包含 Python 程式碼範例、錢包工具推薦、以及對區塊鏈分析威脅模型的深度分析。

比特幣隱私技術深度實作教程:從 CoinJoin 到 Silent Payments,程式碼範例與風險分析

說實話,我剛開始折騰比特幣隱私技術的時候,被一堆專有名詞搞得頭暈。CoinJoin、PayJoin、Silent Payments...到底哪個是什麼?哪個更安全?代碼怎麼寫?

這篇文章就是我在踩坑過程中的筆記,目標是用最直白的方式把這些技術的原理和實作講清楚。代碼會給,但別指望一行解釋都沒有——我會一段一段帶你讀。

先搞清楚敵人:區塊鏈分析能做到什麼?

在談隱私保護之前,你得先知道對手有什麼武器。不然就像打仗不知道敵人長什麼樣,防守也是盲目的。

共同輸入所有權啟發(Common-Input-Ownership Heuristic)

這是區塊鏈分析最基礎的一招。

比特幣交易有 input 和 output。如果一個交易的 inputs 來自地址 A 和地址 B,區塊鏈分析師就會推斷:地址 A 和地址 B 屬於同一個人。

為什麼?因為要花費一個 UTXO,你需要持有對應的私鑰。如果地址 A 和地址 B 的 UTXO 同時被花掉,說明這兩個地址的私鑰都在同一個人手裡。

這招不是 100% 準確——有人故意用多簽名錢包來混淆——但在大多數情況下有效得很。

# 簡化版的地址聚類邏輯
def cluster_addresses(transactions):
    """
    根據共同輸入所有權啟發聚類地址
    現實中分析公司會結合更多 heuristic 來提高準確率
    """
    clusters = []
    
    for tx in transactions:
        input_addresses = tx['inputs']
        
        # 檢查這些地址是否已經在某個 cluster 裡
        merged = False
        for cluster in clusters:
            # 如果有任何地址已經在 cluster 中,就把所有 input 加入
            if any(addr in cluster for addr in input_addresses):
                for addr in input_addresses:
                    cluster.add(addr)
                merged = True
                break
        
        # 沒有在任何現有 cluster 中,就建立新的
        if not merged:
            clusters.append(set(input_addresses))
    
    return clusters

# 測試一下
test_txs = [
    {'inputs': ['1A', '1B']},  # 1A 和 1B 被視為同一個人
    {'inputs': ['1B', '1C']},  # 1C 也加入
    {'inputs': ['2A', '2B']},  # 獨立的 cluster
]
clusters = cluster_addresses(test_txs)
print(f"Cluster 1: {clusters[0]}")  # {'1A', '1B', '1C'}
print(f"Cluster 2: {clusters[1]}")  # {'2A', '2B'}

交易圖譜追蹤

知道了地址之間的關聯之後,下一步就是追蹤資金流向。

攻擊者視角:
交易所熱錢包 → 小明的地址 → 小明的冷錢包 → 小明轉出的地址
   ↑
   這裡已知(小明做了 KYC)

把所有的「邊」連起來,你就得到了一張交易圖。只要其中任何一個節點的身份被識別(比如交易所的熱錢包),攻擊者就能沿著這條鏈一路追蹤。

金額分析和時間分析

比特幣交易的金額是透明的。如果一筆交易的 output 金額是 0.12345 BTC,攻擊者就能推斷:「這筆交易的找零大概就是這個金額。」

時間分析則更微妙。如果小明總是在北京時間晚上 9 點轉帳,而且金額模式符合某個規律(比如總是留 0.1 BTC 的零頭),這些都會成為關聯分析的線索。

CoinJoin:把你的交易藏進人群中

原理

CoinJoin 的核心思想很簡單:把多個人的交易混在一起,讓外部觀察者分不清誰給了誰多少錢

想像一下:

傳統做法是三筆獨立的交易。但 CoinJoin 把這三筆交易「拼接」成一個:

Inputs:           Outputs:
老王的 1 BTC  →  小李的 1 BTC
小張的 2 BTC  →  小王的 2 BTC  
小美的 0.5 BTC →  小趙的 0.5 BTC

等等,這不是跟原來一樣嗎?外部觀察者一看 inputs 和 outputs,立刻就知道是老王給了小李 1 BTC。

聰明的地方在於:參與者的數量可以更多,而且輸出金額可以相同

Inputs:           Outputs:
老王的 1 BTC  →  A地址的 1 BTC
小張的 1 BTC  →  B地址的 1 BTC
小美的 1 BTC  →  C地址的 1 BTC

現在,三個參與者各出了 1 BTC,總共 3 BTC。三個輸出地址各收到 1 BTC。外部觀察者只知道「有人出了 3 BTC,有人收了 3 BTC」,但不知道誰是誰。

實現細節

CoinJoin 的關鍵是:每個參與者都要對整筆交易簽名,但不會暴露自己的私鑰

比特幣的 SIGHASH 機制讓這成為可能。SIGHASHALL、SIGHASHNONE、SIGHASH_SINGLE...不同的 flag 決定簽名覆蓋交易的哪些部分。

# 概念性的 CoinJoin 協調流程(psuedo-code)
class CoinJoinCoordinator:
    def __init__(self):
        self.participants = []
        self.proposed_tx = None
    
    def register(self, participant_address, amount):
        """參與者報名"""
        self.participants.append({
            'address': participant_address,
            'amount': amount,
            'signed': False
        })
    
    def build_transaction(self):
        """
        建構 CoinJoin 交易
        輸入輸出金額要相同,否則會有金額線索
        """
        # 收集所有 input
        inputs = []
        for p in self.participants:
            utxos = find_utxos(p['address'], p['amount'])
            inputs.extend(utxos)
        
        # 產生新的匿名地址(change addresses)
        outputs = []
        for p in self.participants:
            anon_address = generate_new_address()
            outputs.append({
                'address': anon_address,
                'amount': p['amount']
            })
        
        self.proposed_tx = {
            'inputs': inputs,
            'outputs': outputs
        }
        
        return self.proposed_tx
    
    def collect_signatures(self):
        """
        收集所有參與者的簽名
        每個參與者只簽自己的 input
        """
        for p in self.participants:
            sig = sign_transaction_input(
                self.proposed_tx,
                p['input_index'],
                p['private_key']
            )
            p['signature'] = sig
            p['signed'] = True
        
        # 所有人簽完後廣播
        if all(p['signed'] for p in self.participants):
            self.broadcast()

現實中的 CoinJoin 工具

目前主流的 CoinJoin 工具包括:

JoinMarket:一個去中心化的 CoinJoin 協議。參與者分為「maker」(提供流動性)和「taker」(支付費用)。優點是去中心化程度高,缺點是需要技術知識。

Wasabi Wallet:專注於隱私的比特幣錢包,內建 CoinJoin 功能。介面相對友好,但 CoinJoin 的隱私集合大小依賴於同時在線的用戶數量。

Samourai Wallet:移動端的隱私錢包,提供 Whirlpool(另一種 CoinJoin 實現)。

# JoinMarket 的基本用法示例
# 安裝:pip install jmbitcoin

from jmclient import (Maker, Taker, load_test_config, jm_single)
from twisted.internet import reactor, task

# 設定為 CoinJoin taker
class MyTaker(Taker):
    def __init__(self, ...):
        super().__init__(...)
    
    def on_tx_created(self, txhex):
        # 廣播交易
        broadcast(txhex)
    
    def on_tx_not_created(self, reason):
        print(f"CoinJoin failed: {reason}")

# 啟動
load_test_config()
taker = MyTaker(...)
reactor.run()

風險和局限性

CoinJoin 不是萬能的。

金額糾纏:如果你的 CoinJoin 輸出金額很特殊(比如 1.23456 BTC),外部觀察者可能追蹤這個金額來關聯你的地址。

時序分析:如果你在 CoinJoin 之後立刻又把錢轉走,這個時間模式可能成為關聯的線索。

區塊鏈分析公司的指紋:CoinJoin 有一些獨特的「指紋」。比如說,JoinMarket 的 CoinJoin 交易有一些特定的模式。區塊鏈分析公司已經學會識別這些模式。

你的對手是專業的:Chainalysis、Elliptic 這些公司僱用了密碼學家和資料科學家,專門研究如何去匿名化比特幣交易。CoinJoin 能增加他們的分析成本,但不能完全阻止他們。

PayJoin:打破共同輸入假設

為什麼需要 PayJoin?

想象這個場景:小明向小李買了東西,要付 1 BTC。

普通交易是這樣的:

Input: 小明的 1 BTC 地址
Output: 小李的 1 BTC 地址

區塊鏈分析師一看:哦,小明付了 1 BTC 給小李。這筆交易讓雙方的地址被「捆綁」在一起了。

但如果是 PayJoin(又稱 Pay-to-End-Point,P2EP):

Input 1: 小明的 1 BTC 地址
Input 2: 小李的 0.5 BTC 地址(作為找零 input)
Output 1: 小李的 1.5 BTC 地址
Output 2: 小明的 0.5 BTC 地址

現在,交易金額是 1.5 BTC,而不再是 1 BTC。而且,外部觀察者無法確認哪個 output 才是真正的「轉帳」,因為小李也出了一個 input。

這一招厲害的地方在於:它直接破壞了共同輸入所有權啟發。如果分析師看到一個交易的 inputs 分別來自小明和小李,他無法斷定這兩人是同一個人——更可能是正常的商業交易。

PayJoin 的實現

PayJoin 使用了 SIGHASH 機制的組合,允許發送方和接收方共同簽名同一筆交易的不同部分。

# PayJoin 的概念性實現
class PayJoinSession:
    def __init__(self, sender, receiver):
        self.sender = sender      # 付款方
        self.receiver = receiver  # 收款方
        self.utxo = None
    
    async def initiate(self, amount):
        """
        付款方發起 PayJoin 請求
        """
        # 建立支付的 details
        payment_detail = {
            'address': self.sender.get_new_address(),
            'amount': amount
        }
        
        # 請求接收方提供 input
        receiver_input_proposal = await self.request_receiver_input(
            payment_detail
        )
        
        if receiver_input_proposal:
            # 接收方願意參與,構建 PayJoin 交易
            return await self.build_payjoin_tx(
                amount,
                receiver_input_proposal
            )
        else:
            # 接收方不參與,降級為普通交易
            return await self.build_normal_tx(amount)
    
    async def build_payjoin_tx(self, amount, receiver_utxo):
        """
        建構 PayJoin 交易
        這裡的關鍵是使用 SIGHASH_NONE 或 SIGHASH_SINGLE
        讓雙方只簽名自己相關的部分
        """
        tx = {
            'inputs': [
                {
                    'utxo': self.sender.select_utxo(amount),
                    'sighash_type': 'SIGHASH_ALL'
                },
                {
                    'utxo': receiver_utxo,  # 接收方提供的 UTXO
                    'sighash_type': 'SIGHASH_ALL'
                }
            ],
            'outputs': [
                {
                    'address': self.receiver.get_new_address(),
                    'amount': amount + receiver_utxo['value']
                },
                {
                    'address': self.sender.get_new_address(),  # 找零
                    'amount': self.calculate_change()
                }
            ]
        }
        
        # 雙方各自簽名自己的 input
        tx['inputs'][0]['signature'] = self.sender.sign(tx, 0)
        # 接收方簽名...(需要接收方配合)
        
        return tx
    
    def calculate_change(self):
        """計算找零"""
        # 複雜的邏輯...
        return change_amount

PayJoin 的局限性

PayJoin 的隱私效果取決於接收方的配合。如果接收方不願意或不知道如何參與 PayJoin,這筆交易就只能是普通交易。

此外,PayJoin 需要接收方「出金」——把自己的 UTXO 加入交易。這意味著接收方需要有額外的比特幣來「墊付」,而且交易費也要分擔。

現實中,PayJoin 的採用率還比較低。主要原因:

  1. 需要接收方錢包支持
  2. 需要雙方線上同步
  3. 用戶認知不足

Silent Payments:革命性的靜態地址方案

傳統比特幣地址的隱私問題

你有沒有想過一個問題:如果你要別人轉比特幣給你,你要怎麼給他地址?

答案是:給他你的比特幣地址。

但問題來了:如果你的網站上公開了一個比特幣地址,所有人就能看到這個地址收到了多少比特幣、轉到了哪裡。你的財務隱私就這麼沒了。

即使你每次收款都用新地址,但當你轉帳的時候——特別是當你需要把所有比特幣從一堆地址集中到一個的時候——區塊鏈分析師就能把你的地址聚類在一起。

Silent Payments 的原理

Silent Payments 的想法很巧妙:接收方提供一個「根地址」,發送方根據這個根地址和自己的交易資訊,計算出一個只能用一次的靜態地址

這樣的好處是:

  1. 接收方只需要公開一個「根地址」
  2. 每次收款都有全新的、不可關聯的地址
  3. 不需要任何額外的鏈下通訊
# Silent Payments 的簡化實現概念
import hashlib

def derive_silent_payment_address(
    recipient_root_key,
    sender_pubkey,
    tx_output_index
):
    """
    根據 BIP-352 提案計算 Silent Payment 地址
    
    參數:
    - recipient_root_key: 接收方的根掃描金鑰(Base58Check 編碼)
    - sender_pubkey: 發送方的公鑰(未壓縮格式)
    - tx_output_index: 這筆交易在區塊中的輸出索引
    """
    # 1. 解析接收方的根掃描金鑰
    scan_key = decode_base58check(recipient_root_key)
    
    # 2. 計算標籤
    # BIP-352 使用 txid 和輸出索引來創建標籤
    label = hashlib.sha256(
        sender_pubkey + bytes(tx_output_index)
    ).digest()
    
    # 3. 計算標籤化的掃描金鑰
    # 這本質上是對接收方掃描金鑰的標量乘法
    tagged_scan_key = tag_hash(label) * scan_key
    
    # 4. 計算最終的付款地址
    # 使用橢圓曲線點加法
    payment_pubkey = tagged_scan_key + scan_key
    
    # 5. 編碼為付款地址(Bech32m 格式)
    address = encode_bech32m('bc', payment_pubkey)
    
    return address

# 接收方(掃描金鑰持有者)的掃描邏輯
def scan_for_silent_payment(block, recipient_scan_key):
    """
    接收方在區塊中掃描發送給自己的 Silent Payments
    """
    for tx in block.transactions:
        for i, output in enumerate(tx.outputs):
            # 嘗試用每個 input 的公鑰計算 Silent Payment 地址
            for input_pubkey in tx.input_pubkeys:
                derived_addr = derive_silent_payment_address(
                    recipient_scan_key,
                    input_pubkey,
                    i
                )
                
                if derived_addr == output.address:
                    # 找到一筆發送給自己的 Silent Payment
                    yield {
                        'txid': tx.txid,
                        'amount': output.amount,
                        'output_index': i
                    }

Silent Payments 的安全性分析

掃描成本:接收方需要遍歷區塊中的每一筆交易、每一個 input 來計算可能的 Silent Payment 地址。這比傳統的「檢查某地址是否收到轉帳」要昂貴得多。

標籤碰撞:BIP-352 的安全性依賴於標籤(label)的碰撞抵抗性。標籤是 SHA256(sender_pubkey || output_index),256 位元的輸出空間使得碰撞在計算上不可行。

私鑰安全性:如果攻擊者知道了你的根掃描金鑰,他可以... 實際上什麼也做不了。因為每次交易的付款地址都不同,而且只有根掃描金鑰無法計算任何私鑰。

Silent Payments vs 傳統地址方案

特性傳統地址Silent Payments
地址數量每筆交易需要新地址只需要一個根地址
鏈下通訊需要告知對方地址只需要告知根地址
隱私性中等更高
掃描成本高(需要遍歷區塊)
錢包支援廣泛有限(正在增長)

實際操作建議

風險評估

在選擇隱私技術之前,你需要評估自己的威脅模型:

業餘觀察者:只是想蹭熱點的普通人。

→ 用新版比特幣錢包,每次轉帳都用新地址就夠了。

有興趣的第三方:比如你的商業夥伴或親戚。

→ 避免在公開場合暴露比特幣地址。

專業區塊鏈分析:Chainalysis、Elliptic 等公司的追蹤。

→ 需要使用 CoinJoin、PayJoin 等混合技術。

國家級行為者:有能力動用大量資源的機構。

→ 老實說,比特幣的隱私性對這種對手是遠遠不夠的。

隱私的「木桶原理」

比特幣隱私性取決於最薄弱的那一環。

舉個例子:你費了好大勁做了一次 CoinJoin,把 1 BTC 洗乾淨了。但轉帳的時候,你用的卻是家裡的 WiFi,IP 位址被記錄了。這樣分析師只要拿到交易所的 KYC 資料,就能一路追到你。

所以,比特幣隱私需要:

  1. 錢包層面:使用隱私保護錢包,避免地址重複使用
  2. 交易層面:適當使用 CoinJoin、PayJoin
  3. 網路層面:配合 Tor 或 VPN 使用比特幣節點
  4. 身份層面:不要在交易所 KYC 後直接轉到隱私地址

推薦的工具組合

桌面端:Wasabi Wallet(CoinJoin + 繁重的鏈分析)

移動端:Samourai Wallet(Whirlpool + PayJoin + SOS)

進階用戶:JoinMarket(做市商模式的 CoinJoin)

別忘了配合:

結語:隱私是一場貓鼠遊戲

比特幣的隱私技術不是一成不變的。區塊鏈分析公司在進步,隱私保護技術也在進步。這是一場持續的軍備競賽。

CoinJoin、PayJoin、Silent Payments...每一種技術都有它的適用場景和局限性。沒有銀彈,只有不斷疊加的防禦層。

我的建議是:根據自己的威脅模型選擇合適的工具,不要過度隱私,也不要太過暴露。最安全的做法,往往也是最不方便的。

在比特幣的世界裡,方便和隱私永遠是個 trade-off。


本文的程式碼僅供概念說明用,切勿直接用於生產環境。隱私技術的實現細節複雜,建議使用成熟的開源錢包。

本文包含

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!