你用一个新地址收了一笔钱,从没在任何地方留过名字。你觉得这是匿名的。
三年后,你在一个需要实名的交易所提了一次币。
那一刻起,这个地址过去三年和未来所有的交易,全部对应到了你。 而且这件事是追溯生效的——不是从今天开始暴露,是连同全部历史一起暴露。
说准确一点:链上地址不是"匿名",是一个你一直在用的笔名。
笔名本身不含真名,但你用它写了三年。只要有任何一篇文章泄露了你是谁,此前此后的每一篇都跟着对上号了——而且这些文章已经被无数人抄录、索引、永久保存,你收不回来。
一、伪匿名不是匿名
链上地址是一个【持久化的假名】。
它不含姓名,但它是【稳定的、可追踪的、永久公开的】。
⚠️ 致命的性质是回溯性:
⭐ 一旦某个地址在【任何一个时刻】被关联到真实身份,
它的【全部历史交易和全部未来交易】都被暴露。
⭐ 和传统金融对比一下,就能看出这有多严重:
银行账户:⚠️ 银行、监管、和你自己能看到流水
链上地址:【全世界任何人】、【永久】、【可编程地批量分析】
⟹ 这不是"隐私差一点",是一个【量变到质变】的区别。
而且泄露是不可撤销的:链上数据已经被无数人抓取和索引,你无法收回。
二、链分析的四类启发式
链分析公司做的事,就是把地址聚成"实体簇"。核心工具有四类:
① 共同输入所有权
一笔比特币交易花了 5 个 UTXO。
⭐ 要花掉它们,必须同时持有 5 个私钥。
⟹ 启发式:这 5 个地址属于同一个人。
⚠️ 这是链分析最有效的一条——它能把散落的地址迅速聚成大簇。
② 找零识别
第 10 讲说过 UTXO 必须整体花费,所以有找零。识别出哪个输出是找零,就能把新地址接到旧簇上:
线索:
⭐ 地址复用:找零回到了一个用过的地址
整数金额:付款常是整数(0.5 BTC),找零是零头(0.34271 BTC)
脚本类型:找零通常与输入使用同一种地址格式
输出数量:只有两个输出时,非付款的那个就是找零
③ 时间与金额模式
每月固定时间的固定金额 ⟹ 工资
两笔金额恰好相等且时间接近 ⟹ 同一笔资金的两跳
④ 与链下数据关联——最有效的一类
⚠️ 前三类只是把地址聚成簇。
真正给簇【贴上姓名】的,是链下信息:
KYC 交易所的充提记录(最主要的来源)
商家收款地址
公开发布的捐赠地址
你广播交易时的 IP
社交媒体上晒出的截图
⭐ 所以隐私的失守点几乎总是在"链上与链下的交界处",而不是链上本身。
以太坊更糟
账户模型下,同一个地址会被反复使用(第 11 讲)。
⟹ ⭐ 不需要"共同输入"启发式,账户【本身就是】那个簇。
⟹ ENS 域名、NFT 头像等身份标识进一步加剧了关联
三、三条技术路线
① CoinJoin:打破共同输入启发式
多个用户共同构造【一笔】交易:
输入:Alice 1 BTC, Bob 1 BTC, Carol 1 BTC
输出:地址X 1 BTC, 地址Y 1 BTC, 地址Z 1 BTC
⭐ 外部观察者无法确定哪个输出对应哪个输入。
而"共同输入所有权"启发式在这里给出【错误】的结论。
限制:
⚠️ 输出金额必须【完全相等】,否则可以按金额配对
需要协调多个参与者(有专门的协调服务)
匿名集只有本次参与人数那么大
参与者之间可能互相知道(除非用盲签名协议)
② 环签名 + 隐蔽地址 + 金额隐藏(Monero 路线)
三种技术分别解决三个问题:
⭐ 环签名 —— 隐藏【发送方】
你的签名混在 n 个可能签名者中,
验证者能确认"这 n 个人里有一个签了",但不知道是谁。
需要一个"密钥镜像"来防双花(作用类似第五节的 nullifier)
隐蔽地址 —— 隐藏【接收方】
付款方用收款方的公钥【现场推导】出一个一次性地址。
⟹ 链上每笔收款的地址都不同,
而且只有收款方能识别出"这笔是给我的"
RingCT —— 隐藏【金额】
用 Pedersen 承诺(第 8 讲)表示金额,
验证者仍能检查"输入总额 = 输出总额",却看不到具体数字。
必须配合范围证明防止负数金额
⭐ 三者结合,链上几乎看不到任何有用信息。 代价是交易体积大得多、验证慢得多,且环签名的隐私强度受限于环的大小。
③ 零知识池(Zcash / Tornado 模型)
这是与前两讲联系最紧密的一条路。
存款:
① 用户在本地生成两个随机数:secret 和 nullifier
② 计算 commitment = H(secret ‖ nullifier)
③ ⭐ 把 commitment 加入合约维护的一棵 Merkle 树(第 5 讲)
④ 转入固定面额的资产
取款:
① 用户提供一个零知识证明,声称:
"我知道某个 (secret, nullifier),使得
H(secret ‖ nullifier) 是这棵树里的某个叶子"
而【不透露是哪个叶子】
② 同时公开 nullifier 本身
③ 合约检查:证明有效 且 这个 nullifier 从未出现过
④ 记录 nullifier,向【任意指定地址】放款
⭐ 两个精妙之处:
① ⭐ nullifier 让"防双花"和"不可关联"同时成立
公开 nullifier 保证同一笔存款只能取一次,
但由于它是 commitment 的【一部分原像】而不是 commitment 本身,
没有人能把它对应回具体哪次存款。
② 面额必须固定
如果允许任意金额,"存 3.7183 取 3.7183"会直接暴露关联。
这是三条路线中隐私性最强的:在匿名集内部,不可关联性是【密码学保证的】,而不是启发式意义上的模糊。
四、匿名集是唯一的度量
⭐ 笔名那个比方到这里有了刻度:你的隐私 = 有多少人可能是这个笔名的作者。 只有一个候选人时,笔名等于真名;候选人有一万个时,它才真的遮住了你。
所有隐私方案的强度,最终都归结到一个数:
⭐ 匿名集(anonymity set):观察者认为"这笔资金可能来自"的候选数量。
匿名集 = 1 ⟹ ⭐ 零隐私
匿名集 = 3 ⟹ 猜中概率 1/3,基本无用
匿名集 = 10000 ⟹ 真正的隐私
⭐⭐ 由此得到一个非常重要、也非常反直觉的推论:
隐私是一种公共品,具有网络效应。
⚠️ 一个只有你一个人使用的隐私工具,提供的隐私为零。 你的隐私,来自和你使用同一个工具的其他人。
⭐ 这解释了几个常被误解的现象:
① 为什么隐私协议的价值高度依赖使用量
② ⚠️ 为什么"我自己写一个混币器"毫无意义
③ 为什么隐私功能【必须默认开启】才有效——
如果隐私是可选的,那么"选择使用隐私"这个行为本身就成了一个信号,
反而缩小了匿名集。
五、实践中隐私为什么总是失守
即使用了最强的工具,隐私仍然常常被破坏。四个原因:
① ⭐ 出入口关联
从 KYC 交易所提币 → 进入隐私池 → 取出 → 存回同一个交易所
金额和时间的相关性可能足以关联两端。
② 金额与时间的侧信道
存 1.0 ETH,五分钟后取 1.0 ETH,而这段时间只有你一个人在用。
③ Gas 来源
取款到一个全新地址后,这个地址需要 ETH 付 Gas。
如果 Gas 是从你的旧地址转来的,前功尽弃。
(这正是"中继器 / relayer"服务存在的原因——由第三方代付 Gas。)
④ 一次失误,永久暴露
由于回溯性(第一节),只要有一次操作把新旧身份连上,
之前所有的努力全部作废,且【无法补救】。
⭐ 这是隐私工程最残酷的性质:安全性是"最弱一次操作"决定的,而不是平均水平。
六、可审计的隐私
隐私工具具有明确的双重用途性质:同一个技术既能保护普通用户的财务信息,也能被用于洗钱。Tornado Cash 在 2022 年被美国财政部列入制裁名单,是这个张力最受关注的一次事件。
⭐ 这门课不评价政策,只说技术上可以做到什么:
⭐ 零知识证明的表达能力,允许"隐私"和"可审计"同时成立。
几个具体方向:
① 查看密钥(view key)
Zcash 支持把一个只读密钥交给审计方或税务机关,
⭐ 让对方能看到你的交易,而对其他人仍然保密。
⟹ 隐私变成【选择性披露】而不是全有全无。
② 关联集证明(Privacy Pools)
取款时,除了证明"我在池子里",
还额外证明"我属于池子的某个【指定子集】"——
比如"我的存款不来自任何一个已知的黑名单地址"。
⟹ 用户主动【证明自己清白】,而无需暴露具体是哪一笔存款。
这把问题从"要不要允许隐私"变成了"用户能否自证合规"。
③ 合规证明
证明"我的资金满足某组规则",而不透露资金本身。
⭐ 技术上的关键认识:零知识证明能证明的不只是"我有钱",而是【任意可被表达成约束的陈述】(第 32 讲第七节)——包括"我不在某个集合里"。
七、关于隐私的心态
一个常见的错误框架是"隐私 = 隐藏坏事"。
⭐ 更准确的看法:
在传统金融里,下面这些是【默认保密】的:
你的工资、你的余额、你的每一笔消费、你的债务
⚠️ 而在一条完全透明的链上,它们是【默认公开且永久】的。
⭐ 所以链上隐私要恢复的,不是什么特殊待遇,而是财务信息的基本边界。 几个具体的现实后果:
⚠️ 收工资的地址被公开 ⟹ 同事之间的薪资全部互相可见
商家收款地址被公开 ⟹ 竞争对手能实时看到你的营业额
大额持有者被识别 ⟹ 人身安全风险(现实中已发生过绑架案)
交易被前置 ⟹ 第 34 讲的 MEV
最后一条值得注意:下一讲会看到,「交易在被打包前对所有人可见」不只是隐私问题,它直接造成经济损失。
八、Go:承诺-nullifier 池
package privacy
import (
"crypto/rand"
"errors"
"golang.org/x/crypto/sha3"
)
func keccak(parts ...[]byte) [32]byte {
h := sha3.NewLegacyKeccak256()
for _, p := range parts {
h.Write(p)
}
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// Note 是用户在【本地】保存的秘密。⚠️ 丢了就取不出钱。
type Note struct {
Secret [32]byte
Nullifier [32]byte
}
// NewNote 本地生成一张票据。
// 这两个随机数从不上链——上链的只有它们的哈希。
func NewNote() (*Note, error) {
n := &Note{}
if _, err := rand.Read(n.Secret[:]); err != nil {
return nil, err
}
if _, err := rand.Read(n.Nullifier[:]); err != nil {
return nil, err
}
return n, nil
}
// Commitment 是存款时公开的值:H(secret ‖ nullifier)。
func (n *Note) Commitment() [32]byte {
return keccak(n.Secret[:], n.Nullifier[:])
}
// Pool 是链上合约的状态。
type Pool struct {
commitments []([32]byte) // 一棵 Merkle 树的叶子(第 5 讲)
spent map[[32]byte]bool // 已使用的 nullifier
Denomination uint64 // 固定面额——否则金额会直接暴露关联
}
func NewPool(denom uint64) *Pool {
return &Pool{spent: make(map[[32]byte]bool), Denomination: denom}
}
// Deposit 把 commitment 加入树。
// 观察者看到的只有一个哈希——它与任何身份无关。
func (p *Pool) Deposit(commitment [32]byte, amount uint64) error {
if amount != p.Denomination {
return errors.New("privacy: 金额必须等于固定面额")
}
p.commitments = append(p.commitments, commitment)
return nil
}
// WithdrawProof 是取款时提交的内容。
type WithdrawProof struct {
Nullifier [32]byte // 公开,用于防双花
Root [32]byte // 证明所针对的 Merkle 根
Recipient [20]byte // 任意地址,与存款地址无关
ZKProof []byte // "我知道树中某叶子的原像"(第 32 讲)
}
// Withdraw 处理取款。
// 精妙之处:nullifier 保证同一笔存款只能取一次,
// 但由于它只是 commitment 的【一部分原像】,
// 没有人能把它对应回具体哪一次存款。
func (p *Pool) Withdraw(w *WithdrawProof, verify func(*WithdrawProof) bool) error {
if p.spent[w.Nullifier] {
return errors.New("privacy: nullifier 已被使用(双花)")
}
if !p.knownRoot(w.Root) {
return errors.New("privacy: 未知的 Merkle 根")
}
if !verify(w) {
return errors.New("privacy: 零知识证明无效")
}
p.spent[w.Nullifier] = true // 必须在放款前记录
return nil
}
func (p *Pool) knownRoot(r [32]byte) bool {
// 实际实现会保留最近若干个历史根,
// 因为用户生成证明时的树状态可能已经被新存款改变。
return true
}
// AnonymitySet 返回当前的匿名集大小。
// 这是隐私强度的【唯一】度量:
// 匿名集为 1 时隐私为零,无论用了多强的密码学。
func (p *Pool) AnonymitySet() int {
return len(p.commitments) - len(p.spent)
}
// GuessProbability 返回观察者猜中来源的概率。
// 它直接说明了为什么隐私是一种【公共品】:
// 你的隐私,来自和你使用同一个工具的其他人。
func (p *Pool) GuessProbability() float64 {
n := p.AnonymitySet()
if n <= 0 {
return 1.0
}
return 1.0 / float64(n)
}
九、本讲小结
- ⭐⭐ 伪匿名的致命性质是回溯性:任何时刻被关联一次,全部历史与未来交易都被暴露。
- 与银行的差别是量变到质变:银行流水只有少数方能看到,链上数据是全世界、永久、可编程批量分析的,且泄露不可撤销。
- 链分析四类启发式:共同输入所有权(最有效的聚簇工具)、找零识别、时间金额模式、与链下数据关联(真正给簇贴上姓名的那一类)。
- 隐私的失守点几乎总在链上与链下的交界处,而不是链上本身。以太坊的账户模型让"账户本身就是那个簇",情况更糟。
- 三条路线:CoinJoin(打破共同输入启发式,要求等额输出)、Monero 路线(环签名藏发送方 + 隐蔽地址藏接收方 + Pedersen 承诺藏金额)、零知识池(commitment-nullifier,匿名集内的不可关联性是密码学保证的)。
- nullifier 的精妙:它让防双花和不可关联同时成立——公开它能防重复取款,但它只是 commitment 的一部分原像,无法对应回具体存款。
- ⭐⭐ 匿名集是隐私的唯一度量。由此推出:隐私是一种公共品,有网络效应——你的隐私来自和你用同一个工具的其他人。
- 推论:隐私功能必须默认开启才有效。 如果隐私是可选的,“选择使用隐私"本身就成了信号,反而缩小匿名集。
- 实践中失守的四个原因:出入口关联、金额与时间的侧信道、Gas 来源(新地址的 Gas 从旧地址转来就前功尽弃,这是中继器存在的原因)、一次失误永久暴露且无法补救。
- 隐私工程最残酷的性质:安全性由最弱的那一次操作决定,不是平均水平。
- 技术上隐私与可审计可以并存:查看密钥(选择性披露)、关联集证明(证明"我的存款不来自黑名单"而不暴露是哪一笔)、合规证明。因为 ZK 能证明任意可表达成约束的陈述,包括"我不在某个集合里”。
- 心态上的纠正:链上隐私要恢复的不是特殊待遇,而是传统金融里默认存在的财务边界。工资互相可见、竞争对手实时看到你的营业额、大额持有者的人身风险,都是真实后果。
思考题
- 为什么说伪匿名的回溯性比"泄露一次"严重得多?请对比一次信用卡号泄露与一次地址关联。
- 一笔比特币交易有 3 个输入、2 个输出,其中一个输出是 0.5 BTC 整数,另一个是 0.13782 BTC。请推断哪个是找零,并说明依据。
- 为什么以太坊的账户模型比 UTXO 更不利于隐私?ENS 域名在这里起什么作用?
- CoinJoin 的输出金额为什么必须相等?如果金额各不相同,观察者能做什么?
- 完整描述零知识池的存取流程。为什么公开 nullifier 不会暴露是哪一笔存款?
- 为什么零知识池必须固定面额?如果允许任意金额,请构造一个关联攻击。
- 一个隐私池只有 5 个人用过。观察者猜中来源的概率是多少?这说明了什么?
- 为什么"隐私必须默认开启"?请说明可选隐私如何反过来缩小匿名集。
- 你从一个 KYC 交易所提币,经过隐私池后取到新地址。请列出所有可能暴露关联的环节,并给出对策。
- 用第 32 讲的框架说明"关联集证明"要证明的陈述是什么。它如何在不暴露具体存款的前提下自证清白?