教程区块链区块链技术ch1010.2 混币与环签名

本页目录

如果区块链的透明性让每笔交易都暴露在链分析之下,那么混币环签名就是最早的"幕布"——它们不创造数学上的绝对隐私,但通过混淆交易链路,让追踪者面对一个巨大的"可能性集合",从而在实践中保护用户。


10.2.1 混币(CoinJoin)的基本原理

直觉:多人凑份子发钱

混币的核心思想简单得出奇:把多笔输入和输出混合在一次交易中,让外部的观察者无法判断哪个输入对应哪个输出。

想象五个人各自拿着 10的纸钞,他们把五张10 的纸钞,他们把五张10 都放进一个黑箱子,然后每个人从中随机取出一张 10。如果你站在外面看,你知道10。如果你站在外面看,你知道50 进去了、$50 出来了,但你无法知道谁的钱最终给了谁

比特币的 CoinJoin 正是这一思想的链上实现。

CoinJoin 交易结构

在普通的比特币交易中,输入地址向输出地址直接转账,链路清晰:

\text{Alice \xrightarrow{1~BTC} \text{Bob}} \quad \text{(可追踪)}

在 CoinJoin 中,多组输入和输出被整合为一个大交易:

graph LR
    A1[Alice 1 BTC<br/>(utxo_A)] --> OUT1[1 BTC]
    B1[Bob 1 BTC<br/>(utxo_B)] --> OUT2[1 BTC]
    C1[Carol 1 BTC<br/>(utxo_C)] --> OUT3[1 BTC]
    D1[Diana 1 BTC<br/>(utxo_D)] --> OUT4[1 BTC]
    A2[...]
    
    OUT1 --> A_out[Alice新地址]
    OUT2 --> B_out[Bob新地址]
    OUT3 --> C_out[Carol新地址]
    OUT4 --> D_out[Diana新地址]
    
    style A_out fill:#e1f5e1
    style B_out fill:#e1f5e1
    style C_out fill:#e1f5e1
    style D_out fill:#e1f5e1

匿名集(Anonymity Set)

混币的效果可以用匿名集来量化:

k=参与混币的等值输出数量k = \text{参与混币的等值输出数量}

在这个交易中有 kk1 BTC1\text{ BTC} 的输出。外部观察者只能判断"Alice 输入了 1 BTC"和"有人得到了 1 BTC",但无法确定这 1 BTC1\text{ BTC} 究竟流向了 kk 个输出中的哪一个。

攻击者将真实的资金流向猜对的概率为:

Pcorrect=1kP_{\text{correct}} = \frac{1}{k}

匿名集越大,隐私越强。 但混币面临两个核心问题:

  1. 需要信任协调者:谁来组织这个混币交易?
  2. 等值约束:所有参与金额必须相等,否则金额差异会泄露链路。

类型Script 模拟:混币交易的输入输出匹配

typescript
/**
 * 混币(CoinJoin)交易模拟
 * 模拟多输入多输出的匿名集混淆效果
 */
interface UTXO {
  txid: string;
  vout: number;
  value: bigint;     // 以 satoshi 为单位
  owner: string;
  scriptPubKey: string;
}

interface CoinJoinParticipant {
  inputs: UTXO[];
  changeOutput: { value: bigint; scriptPubKey: string } | null;
  min anonymitySet: number;  // 最小匿名集要求
}

function createCoinJoin(
  participants: CoinJoinParticipant[],
  denomination: bigint,    // 混币面额(所有人必须相等)
): {
  inputs: UTXO[];
  outputs: { value: bigint; scriptPubKey: string }[];
  anonymitySet: number;
  entropy: number;         // 香农熵: pilog2pi\sum p_i \log_2 p_i
} {
  const allInputs: UTXO[] = [];
  const allOutputs: { value: bigint; scriptPubKey: string }[] = [];
  
  for (const p of participants) {
    // 所有参与者必须提供至少等于 denomination 的输入
    const totalInput = p.inputs.reduce((s, u) => s + u.value, 0n);
    if (totalInput < denomination) {
      throw new Error(`Participant lacks sufficient funds for denomination ${denomination}`);
    }
    allInputs.push(...p.inputs);
    
    // 等值混币输出
    allOutputs.push({ value: denomination, scriptPubKey: p.changeOutput!.scriptPubKey });
    
    // 找零输出(如果有剩余)
    const change = totalInput - denomination;
    if (change > 0n && p.changeOutput) {
      allOutputs.push({ value: change, scriptPubKey: p.changeOutput.scriptPubKey });
    }
  }
  
  // 匿名集 = 等值输出的数量
  const anonymitySet = participants.length;
  
  // 香农熵 = i=1k1klog2(k)=log2(k)\sum_{i=1}^k \frac{1}{k} \log_2(k) = \log_2(k)
  const p = 1 / anonymitySet;
  const entropy = -anonymitySet * (p * Math.log2(p));
  
  return { inputs: allInputs, outputs: allOutputs, anonymitySet, entropy };
}

// 示例:4人各混1 BTC
const participants: CoinJoinParticipant[] = [
  { inputs: [{txid:'a1', vout:0, value:1200000000n, owner:'Alice', scriptPubKey:'pk_A'}], changeOutput: {value:0n, scriptPubKey:'pk_A_new'}, minAnonymitySet: 3 },
  { inputs: [{txid:'b1', vout:0, value:1500000000n, owner:'Bob', scriptPubKey:'pk_B'}], changeOutput: {value:0n, scriptPubKey:'pk_B_new'}, minAnonymitySet: 3 },
  { inputs: [{txid:'c1', vout:0, value:1000000000n, owner:'Carol', scriptPubKey:'pk_C'}], changeOutput: {value:0n, scriptPubKey:'pk_C_new'}, minAnonymitySet: 3 },
  { inputs: [{txid:'d1', vout:0, value:1100000000n, owner:'Diana', scriptPubKey:'pk_D'}], changeOutput: {value:0n, scriptPubKey:'pk_D_new'}, minAnonymitySet: 3 },
];

const result = createCoinJoin(participants, 1000000000n);
console.log(`匿名集: result.anonymitySet,:{result.anonymitySet}, 熵:{result.entropy.toFixed(2)} bits`);
// 输出:匿名集: 4, 熵: 2.00 bits
// 攻击者猜对的概率 P=1/4=25%P = 1/4 = 25\%

Wasabi 钱包:Chaumian 盲签名混币

现代混币方案使用 Chaumian 盲签名 来解决"信任协调者"问题:

  1. 用户先对输出地址进行盲签名——用盲因子 rr 包裹地址,让协调者签名时不知道真实地址
  2. 协调者对盲化后的地址签名后返回
  3. 用户移除盲因子 rr,得到一个"协调者不知道对应关系"的有效签名
  4. 协调者只能看到所有等值输出,但不知道哪个用户对应哪个输出

这消除了对协调者诚实性的依赖:即使协调者串通,也无法将输入与输出关联


10.2.2 环签名(Ring Signature)

从"混合多签名"到"环签名"

环签名是一种更优雅的"内建"方案——不需要外部协调者,交易本身就包含一个"环",证明签名者来自其中某一个地址,但不暴露具体是哪一个。

定义:环签名允许一个签名者从一组公钥(包含自己的公钥)中生成一个签名,使得验证者可以确认这组公钥中至少有一人签名,但无法确定具体是谁。

关键性质

环签名满足三个核心性质:

  • 无条件匿名性:给定环签名,识别出真实签名者的计算复杂度等价于破解底层困难问题
  • 不可伪造性:非环成员无法伪造有效签名
  • 无需环成员许可(区别于群签名):环成员甚至不知道自己被包含在某个环中

环签名链接概率

在包含 nn 个成员的环中,外部观察者正确猜出真实签名者的概率为:

Plink=1nP_{\text{link}} = \frac{1}{n}

当环大小 n=11n = 11(Monero 默认),攻击者的正确识别概率仅为 9.09%9.09\%。经过三轮交易,匿名性将呈指数级提升:

Plink, 3 hops=(111)3=113310.075%P_{\text{link, 3 hops}} = \left(\frac{1}{11}\right)^3 = \frac{1}{1331} \approx 0.075\%

密码学构造(简化版 - CryptoNote 风格)

环签名基于一次性密钥(stealth address)和密钥映像(key image)设计。核心公式:

Ri=riG(i=0,,n1)R_i = r_i G \quad (i = 0, \dots, n-1)

其中 rir_i 是随机生成的混淆公钥的"变形因子",GG 是椭圆曲线基点。真实签名者的位置索引 ss 被嵌套在环中。

签名 σ=(c0,c1,,cn1,r0,r1,,rn1)\sigma = (c_0, c_1, \dots, c_{n-1}, r_0, r_1, \dots, r_{n-1}) 必须满足循环验证方程:

Ri=riG+ciPifor isRi=riG+ciPi+hash(m)xsGfor i=s\begin{align} R_i &= r_i G + c_i P_i \quad \text{for } i \neq s \\ R_i &= r_i G + c_i P_i + \text{hash}(m) \cdot x_s G \quad \text{for } i = s \end{align}

验证时检查 ci+1modn=hash(m,Ri)c_{i+1 \mod n} = \text{hash}(m, R_i) 的循环一致性。


10.2.3 隐匿地址(Stealth Address)

核心思想:接收方一次性地址

区块链地址一旦公开,就成为追踪的固定锚点。隐匿地址让每个发送方都能为接收方生成一个独特的一次性地址,而接收方使用自己的私钥扫描区块链来发现属于自己的这些地址。

一次性地址生成(简化 Diffie-Hellman 方案)

发送方(Alice)使用接收方(Bob)的公钥 PBob=xBobGP_{\text{Bob}} = x_{\text{Bob}} G

  1. Alice 生成临时私钥 rRZqr \xleftarrow{R} \mathbb{Z}_q,计算公钥 R=rGR = rG
  2. 共享秘密:S=rPBob=rxBobG=xBob(rG)=xBobRS = r P_{\text{Bob}} = r x_{\text{Bob}} G = x_{\text{Bob}} (rG) = x_{\text{Bob}} R
  3. 一次性地址公钥:Pone-time=hash(Sn)G+PBobP_{\text{one-time}} = \text{hash}(S || n) G + P_{\text{Bob}}

接收方(Bob)扫描时,用私钥 xBobx_{\text{Bob}} 对每个交易的 RR 计算 S=xBobRS' = x_{\text{Bob}} R,如果生成的公钥匹配自己的,则知道该交易是给他的。

sequenceDiagram
    participant A as Alice (发送方)
    participant C as 区块链
    participant B as Bob (接收方)
    
    A->>A: 生成一次性密钥 r, 计算 R = rG
    A->>A: 共享秘密 S = r * P_Bob
    A->>A: 一次性地址 P = H(S) + P_Bob
    
    A->>C: 交易 (输出到 P, 附带 R)
    
    C->>B: 新区块到账
    B->>B: 遍历每笔交易:check if R 存在
    B->>B: 用私钥 R_Bob 计算 S' = x_Bob * R
    B->>B: 验证 P == H(S') + P_Bob
    B->>B: 匹配 → 确定属于我!
    
    Note over C: 外部观察者仅看到 P<br/>无法关联到 Bob
typescript
/**
 * 隐匿地址(Stealth Address)核心机制模拟
 * 基于椭圆曲线 Diffie-Hellman 的一次性地址
 */

// 简化中:我们用大整数模拟椭圆曲线点乘的"密钥派生"效果
// 实际中需要 secp256k1 点加/点乘;这里用模运算的代数性质来保持逻辑

interface ECPoint {
  x: bigint;
  y: bigint;
}

const P = 2n**256n - 2n**32n - 977n; // secp256k1 p
const G = 79n; // 简化的"生成元"——实际为曲线基点,这里用常数代表代换

function scalarMult(scalar: bigint, point: bigint): bigint {
  // 简化:实际应为椭圆曲线标量乘法
  // 这里用模运算模拟 "scalar * G mod P"
  return (scalar * point) % P;
}

function hashToScalar(data: string): bigint {
  // 简化:使用字符串哈希转整数
  let h = 0n;
  for (let i = 0; i < data.length; i++) {
    h = (h * 31n + BigInt(data.charCodeAt(i))) % P;
  }
  return h;
}

interface StealthKeyPair {
  privateScanKey: bigint;  // 扫描私钥
  privateSpendKey: bigint; // 花费私钥
  publicViewKey: bigint;   // 对应的公钥(用于派生地址)
}

function generateStealthAddress(
  recipientViewKey: bigint,  // Bob 的公钥/视图密钥
  recipientSpendKey: bigint,  // Bob 的公钥/花费密钥(对于简化版,假设相同)
  nonce: bigint,              // 交易索引/随机数,确保可链接性
): {
  oneTimeAddress: bigint;
  ephemeralPubKey: bigint;    // R,包含在交易中让 Bob 检出
  viewTag: string;            // 快速扫描用的短标签
} {
  // Alice 生成临时私钥 r
  const r = (nonce * 7919n + 1234567n) % P; // 简化随机数生成
  const R = scalarMult(r, G);
  
  // 共享秘密 S = r * P_recipient
  const S = scalarMult(r, recipientViewKey);
  
  // 一次性地址公钥: P_one_time = H(S || n) + P_spend
  const h = hashToScalar(`S:{S}:{nonce}`);
  const oneTimeAddress = (scalarMult(h, G) + recipientSpendKey) % P;
  
  // viewTag 用于 O(1) 快速匹配扫描(选择共享秘密的最高8位)
  const viewTag = `0x${(S >> 200n).toString(16).padStart(16, '0')}`;
  
  return { oneTimeAddress, ephemeralPubKey: R, viewTag };
}

// Bob 扫描算法
// Bob 持有 x_view(视图的密钥)
function checkIfMine(
  viewKeyPrivate: bigint,
  ephemeralPubKey: bigint,
  nonce: bigint,
  oneTimeAddress: bigint,
  spendKeyPublic: bigint,
): { isMine: boolean; possibleAddress: bigint } {
  // Bob 恢复 S' = x * R
  const S_prime = scalarMult(viewKeyPrivate, ephemeralPubKey);
  
  // 计算他期望的该交易的一次性地址
  const h = hashToScalar(`Sprime:{S_prime}:{nonce}`);
  const expectedAddress = (scalarMult(h, G) + spendKeyPublic) % P;
  
  return { isMine: expectedAddress === oneTimeAddress, possibleAddress: expectedAddress };
}

// --- 演示 ---
const BobViewKey = 123456789n;
const BobSpendKey = 987654321n; // 简化:实际中由独立推导

// Alice 要发送给 Bob
const tx = generateStealthAddress(BobViewKey, BobSpendKey, 42n);
console.log(`生成的隐匿地址: ${tx.oneTimeAddress.toString(16).slice(0, 16)}...`);

// Bob 扫描这三笔交易
const txs = [
  generateStealthAddress(BobViewKey, BobSpendKey, 42n),
  generateStealthAddress(99999999n, 111111111n, 43n), // 不是给 Bob 的
  generateStealthAddress(BobViewKey, BobSpendKey, 44n),
];

let myTxCount = 0;
for (let i = 0; i < txs.length; i++) {
  const check = checkIfMine(BobViewKey, txs[i].ephemeralPubKey, [42n, 43n, 44n][i], txs[i].oneTimeAddress, BobSpendKey);
  if (check.isMine) myTxCount++;
}
console.log(`Bob 检出 ${myTxCount} 笔属于自己的交易`);
// 输出:Bob 检出 2 笔属于自己的交易(第0笔和第2笔)

10.2.4 零知识证明:zk-SNARK 与 zk-STARK 基础

在深入隐私币之前,我们必须理解隐私保护最强有力的技术——零知识证明

ZKP 三性质(形式化定义)

对于陈述 xx 和证据 ww,证明者 PP 向验证者 VV 证明 xLx \in L(其中 LL 是某个 NP 语言):

Completeness: Pr[(P(x,w)V(x))=1]=1\text{Completeness: } \Pr[(P(x,w) \leftrightarrow V(x)) = 1] = 1
Soundness: P,Pr[(P(x)V(x))=1]negl(x)\text{Soundness: } \forall P^*, \Pr[(P^*(x) \leftrightarrow V(x)) = 1] \leq \text{negl}(|x|)
Zero-Knowledge:  Simulator S, such that S(x)c(P(x,w)V(x))\text{Zero-Knowledge: } \exists \text{ Simulator } S, \text{ such that } S(x) \approx_c (P(x,w) \leftrightarrow V(x))

zk-SNARK 的核心优势

特性说明
证明大小π=O(1)|\pi| = O(1)(仅 ~200 字节)
验证时间O(1)O(1)(毫秒级)
可信设置需要一次性 CRS 生成仪式
抗量子(基于椭圆曲线配对)

可信设置(Trusted Setup)的脆弱性

zk-SNARK 需要生成公共参考字符串(CRS),推导过程中会产生有毒废料(toxic waste)——如果设置参与者合谋保留了废料,就可以伪造证明。

解决方案:多方计算仪式(MPC Ceremony),如 Zcash 的 Powers of Tau

  1. 第一个人贡献随机值 τ\tau,计算 (τG,τ2G,,τnG)(\tau G, \tau^2 G, \dots, \tau^n G)
  2. 第二个人用新随机值 x2x_2 再次"混合":τ=x2τ\tau' = x_2 \cdot \tau
  3. 最终只要至少一个人诚实销毁了自己的随机值,整个 CRS 就是安全的

zk-STARK:无需可信设置

特性zk-SNARKzk-STARK
可信设置需要不需要
证明大小~200 字节~50-200 KB
验证时间O(1)O(1)O(log2N)O(\log^2 N)(仍很快)
抗量子
依赖假设椭圆曲线配对哈希函数(抗碰撞)
flowchart LR
    A[zk-SNARKs<br/>Zcash sprout/Orchard] --> A1[优点: 小证明 + 快验证]
    A --> A2[缺点: 需要可信设置 + 不抗量子]
    B[zk-STARKs<br/>StarkNet] --> B1[优点: 无需可信设置 + 抗量子]
    B --> B2[缺点: 较大证明大小]
    
    A2 --> C[应对方案: 多方计算仪式]
    B2 --> D[应对方案: 递归聚合]
    
    style A fill:#ffe0b2
    style B fill:#c8e6c9

零知识证明在隐私币中的核心用途

零知识证明在隐私币中解决的核心问题是:

"我拥有足够的余额可以支付,且这笔输入之前没有被花过,但我不会告诉你具体是哪一个输入或我有多少钱。"

这需要同时证明三个陈述(用 ZK 合并为单一证明):

  1. 所属权:私钥对应某个未被花费的 UTXO
  2. 余额非负:输入总额 >= 输出金额(在密文层面)
  3. 无双重花费:输入的零知识承诺是唯一的(通过 nullifier 机制)
flowchart TB
    subgraph A["输入(已加密的旧 UTXO)"]
        A1[utxo_1] --> A2["承诺: C_1 = commit(v1, r1)"]
        A3[utxo_2] --> A4["承诺: C_2 = commit(v2, r2)"]
    end
    
    subgraph B["零知识证明"]
        B1["\pi: 我拥有这些 UTXO 的私钥"]
        B2["\pi: 输入总值 >= 输出值"]
        B3["\pi: nullifier 是唯一的"]
    end
    
    subgraph C["输出(新的隐匿 UTXO)"]
        C1["新承诺: C_out = commit(v_out, r_out)"]
        C2[找零: C_change = commit(v_change, r_change)]
    end
    
    A --> B
    B --> B4["输出: \(nullifier_1, nullifier_2 \) + \(\pi\) + 新承诺"]
    B4 --> C
    
    A2 -.-> B1
    A4 -.-> B1
    A2 -.-> B2
    A4 -.-> B2

10.2.5 知识地图

mindmap
  root((隐私保护技术))
    混币
      CoinJoin
        链上混合
        等值输出约束
        Wasabi 盲签名混币
      匿名集
        k = 等值输出数量
        P(link) = 1/k
        香农熵
    环签名
      CryptoNote 风格
      无需协调者
      任意大小环
      链接概率 P = 1/n
    隐匿地址
      一次性地址
      Diffie-Hellman 密钥交换
      扫描 vs 花费 分离
    零知识证明
      zk-SNARK
        小证明 / 快验证
        需要 CRS
      zk-STARK
        无需 CRS
        抗量子
        大证明
      平衡证明 + 无双重花费

> ← 上一节:10.1 隐私需求模型 | 前往 → 10.3 零知识证明入门(ZKP 原理) |*

评论

0

评论加载中…

发表评论

0/2000