如果区块链的透明性让每笔交易都暴露在链分析之下,那么混币与环签名就是最早的"幕布"——它们不创造数学上的绝对隐私,但通过混淆交易链路,让追踪者面对一个巨大的"可能性集合",从而在实践中保护用户。
10.2.1 混币(CoinJoin)的基本原理
直觉:多人凑份子发钱
混币的核心思想简单得出奇:把多笔输入和输出混合在一次交易中,让外部的观察者无法判断哪个输入对应哪个输出。
想象五个人各自拿着 10 都放进一个黑箱子,然后每个人从中随机取出一张 50 进去了、$50 出来了,但你无法知道谁的钱最终给了谁。
比特币的 CoinJoin 正是这一思想的链上实现。
CoinJoin 交易结构
在普通的比特币交易中,输入地址向输出地址直接转账,链路清晰:
在 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)
混币的效果可以用匿名集来量化:
在这个交易中有 个 的输出。外部观察者只能判断"Alice 输入了 1 BTC"和"有人得到了 1 BTC",但无法确定这 究竟流向了 个输出中的哪一个。
攻击者将真实的资金流向猜对的概率为:
匿名集越大,隐私越强。 但混币面临两个核心问题:
- 需要信任协调者:谁来组织这个混币交易?
- 等值约束:所有参与金额必须相等,否则金额差异会泄露链路。
类型Script 模拟:混币交易的输入输出匹配
/**
* 混币(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; // 香农熵:
} {
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;
// 香农熵 =
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.entropy.toFixed(2)} bits`);
// 输出:匿名集: 4, 熵: 2.00 bits
// 攻击者猜对的概率 Wasabi 钱包:Chaumian 盲签名混币
现代混币方案使用 Chaumian 盲签名 来解决"信任协调者"问题:
- 用户先对输出地址进行盲签名——用盲因子 包裹地址,让协调者签名时不知道真实地址
- 协调者对盲化后的地址签名后返回
- 用户移除盲因子 ,得到一个"协调者不知道对应关系"的有效签名
- 协调者只能看到所有等值输出,但不知道哪个用户对应哪个输出
这消除了对协调者诚实性的依赖:即使协调者串通,也无法将输入与输出关联。
10.2.2 环签名(Ring Signature)
从"混合多签名"到"环签名"
环签名是一种更优雅的"内建"方案——不需要外部协调者,交易本身就包含一个"环",证明签名者来自其中某一个地址,但不暴露具体是哪一个。
定义:环签名允许一个签名者从一组公钥(包含自己的公钥)中生成一个签名,使得验证者可以确认这组公钥中至少有一人签名,但无法确定具体是谁。
关键性质
环签名满足三个核心性质:
- 无条件匿名性:给定环签名,识别出真实签名者的计算复杂度等价于破解底层困难问题
- 不可伪造性:非环成员无法伪造有效签名
- 无需环成员许可(区别于群签名):环成员甚至不知道自己被包含在某个环中
环签名链接概率
在包含 个成员的环中,外部观察者正确猜出真实签名者的概率为:
当环大小 (Monero 默认),攻击者的正确识别概率仅为 。经过三轮交易,匿名性将呈指数级提升:
密码学构造(简化版 - CryptoNote 风格)
环签名基于一次性密钥(stealth address)和密钥映像(key image)设计。核心公式:
其中 是随机生成的混淆公钥的"变形因子", 是椭圆曲线基点。真实签名者的位置索引 被嵌套在环中。
签名 必须满足循环验证方程:
验证时检查 的循环一致性。
10.2.3 隐匿地址(Stealth Address)
核心思想:接收方一次性地址
区块链地址一旦公开,就成为追踪的固定锚点。隐匿地址让每个发送方都能为接收方生成一个独特的一次性地址,而接收方使用自己的私钥扫描区块链来发现属于自己的这些地址。
一次性地址生成(简化 Diffie-Hellman 方案)
发送方(Alice)使用接收方(Bob)的公钥 :
- Alice 生成临时私钥 ,计算公钥
- 共享秘密:
- 一次性地址公钥:
接收方(Bob)扫描时,用私钥 对每个交易的 计算 ,如果生成的公钥匹配自己的,则知道该交易是给他的。
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
/**
* 隐匿地址(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(`{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(`{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 三性质(形式化定义)
对于陈述 和证据 ,证明者 向验证者 证明 (其中 是某个 NP 语言):
zk-SNARK 的核心优势
| 特性 | 说明 |
|---|---|
| 证明大小 | (仅 ~200 字节) |
| 验证时间 | (毫秒级) |
| 可信设置 | 需要一次性 CRS 生成仪式 |
| 抗量子 | 否(基于椭圆曲线配对) |
可信设置(Trusted Setup)的脆弱性
zk-SNARK 需要生成公共参考字符串(CRS),推导过程中会产生有毒废料(toxic waste)——如果设置参与者合谋保留了废料,就可以伪造证明。
解决方案:多方计算仪式(MPC Ceremony),如 Zcash 的 Powers of Tau:
- 第一个人贡献随机值 ,计算
- 第二个人用新随机值 再次"混合":
- 最终只要至少一个人诚实销毁了自己的随机值,整个 CRS 就是安全的
zk-STARK:无需可信设置
| 特性 | zk-SNARK | zk-STARK |
|---|---|---|
| 可信设置 | 需要 | 不需要 |
| 证明大小 | ~200 字节 | ~50-200 KB |
| 验证时间 | (仍很快) | |
| 抗量子 | 否 | 是 |
| 依赖假设 | 椭圆曲线配对 | 哈希函数(抗碰撞) |
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 合并为单一证明):
- 所属权:私钥对应某个未被花费的 UTXO
- 余额非负:输入总额 >= 输出金额(在密文层面)
- 无双重花费:输入的零知识承诺是唯一的(通过 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评论加载中…