你用一个新地址收了一笔钱,从没在任何地方留过名字。你觉得这是匿名的。

三年后,你在一个需要实名的交易所提了一次币。

那一刻起,这个地址过去三年和未来所有的交易,全部对应到了你。 而且这件事是追溯生效的——不是从今天开始暴露,是连同全部历史一起暴露。

说准确一点:链上地址不是"匿名",是一个你一直在用的笔名

笔名本身不含真名,但你用它写了三年。只要有任何一篇文章泄露了你是谁,此前此后的每一篇都跟着对上号了——而且这些文章已经被无数人抄录、索引、永久保存,你收不回来。

一、伪匿名不是匿名

链上地址是一个【持久化的假名】。
它不含姓名,但它是【稳定的、可追踪的、永久公开的】。

⚠️ 致命的性质是回溯性:

⭐ 一旦某个地址在【任何一个时刻】被关联到真实身份,
   它的【全部历史交易和全部未来交易】都被暴露。

和传统金融对比一下,就能看出这有多严重:

银行账户:⚠️ 银行、监管、和你自己能看到流水
链上地址:【全世界任何人】、【永久】、【可编程地批量分析】

⟹ 这不是"隐私差一点",是一个【量变到质变】的区别。

而且泄露是不可撤销的:链上数据已经被无数人抓取和索引,你无法收回。

二、链分析的四类启发式

链分析公司做的事,就是把地址聚成"实体簇"。核心工具有四类:

① 共同输入所有权

一笔比特币交易花了 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 能证明任意可表达成约束的陈述,包括"我不在某个集合里”。
  • 心态上的纠正:链上隐私要恢复的不是特殊待遇,而是传统金融里默认存在的财务边界。工资互相可见、竞争对手实时看到你的营业额、大额持有者的人身风险,都是真实后果。

思考题

  1. 为什么说伪匿名的回溯性比"泄露一次"严重得多?请对比一次信用卡号泄露与一次地址关联。
  2. 一笔比特币交易有 3 个输入、2 个输出,其中一个输出是 0.5 BTC 整数,另一个是 0.13782 BTC。请推断哪个是找零,并说明依据。
  3. 为什么以太坊的账户模型比 UTXO 更不利于隐私?ENS 域名在这里起什么作用?
  4. CoinJoin 的输出金额为什么必须相等?如果金额各不相同,观察者能做什么?
  5. 完整描述零知识池的存取流程。为什么公开 nullifier 不会暴露是哪一笔存款?
  6. 为什么零知识池必须固定面额?如果允许任意金额,请构造一个关联攻击。
  7. 一个隐私池只有 5 个人用过。观察者猜中来源的概率是多少?这说明了什么?
  8. 为什么"隐私必须默认开启"?请说明可选隐私如何反过来缩小匿名集。
  9. 你从一个 KYC 交易所提币,经过隐私池后取到新地址。请列出所有可能暴露关联的环节,并给出对策。
  10. 用第 32 讲的框架说明"关联集证明"要证明的陈述是什么。它如何在不暴露具体存款的前提下自证清白?