技术 › 谁收到了?

一次性地址

隐身地址:向一个地址付款,却不把它写到区块链上

简而言之

发送方选取一个随机数 r,公布 R = r·G,并把钱发送到一次性公钥 P = Hs(8·r·A ‖ i)·G + B。接收方用 a 计算同样的值来找到它:Hs(8·a·R ‖ i)·G + B。花费时使用私钥 x = Hs(8·a·R ‖ i) + b。

任务

我们希望:

  1. 区块链上既不出现接收方的地址,也不出现任何在向同一个人多次付款时会重复的信息;
  2. 接收方无需与发送方沟通,就能自己找到属于自己的输出;
  3. 只有接收方能花费这个输出。

解决方案是在每笔交易中内置一次 Diffie–Hellman 密钥交换。

分步说明

发送方 接收方 r(随机数) R = r·G 知道地址: A, B 共享秘密: 8·r·A P = Hs(8rA ‖ i)·G + B 上链的只有:R 和 P (没有关于地址的其他信息) a, b(自己的私钥) 在交易中看到 R 共享秘密: 8·a·R = 8·a·r·G = 8·r·A 检查: Hs(8aR ‖ i)·G + B == P ?

1. 发送方选取随机数 r

每笔交易都使用一个新的随机数 r(交易私钥)。它的公钥版本 R = r·G(交易公钥)被写入交易的 tx_extra 字段。

2. 共享秘密

发送方计算 8·r·A(A 是地址中的查看公钥)。之后接收方计算 8·a·R。两者是同一个点,因为 a·R = a·r·G = r·a·G = r·A。在门罗币的代码中,它被称为密钥派生(key derivation)。

因子 8(余因子)把点中可能存在的小阶分量“清除”掉:即使有人提供了恶意构造的 R,结果也会落在正确的子群中。

3. 一次性公钥

P = Hs( 8·r·A ‖ i )·G + B i 是输出在交易中的索引(varint),因此同一笔交易的两个输出会得到不同的公钥

这就是一次性地址(输出公钥)。它看起来像一个随机点:没有 a 或 r,就没有任何东西可以拿来和它比较。

4. 接收方扫描

对于每笔新交易中的每个输出 i,钱包计算 Hs(8·a·R ‖ i)·G + B 并与 P 比较。匹配——这个输出就是我们的。这只需要查看私钥 a 和公开的 B——因此只读钱包也能找到收款。

5. 接收方花费

一次性输出的私钥:

x = Hs( 8·a·R ‖ i ) + b 验证:x·G = Hs(…)·G + b·G = Hs(…)·G + B = P ✓

这需要花费私钥 b。私钥 x 用于签署花费,key image 也由它计算得出。

金额也用共享秘密加密

金额的掩码也是从同一个共享秘密派生的。输出金额(8 字节)以 amount ⊕ Keccak("amount" ‖ Hs(8rA ‖ i))[0..8] 的形式存储,承诺的盲化因子为 Hs("commitment_mask" ‖ Hs(8rA ‖ i))。接收方计算同样的值并解密出金额。详见 Pedersen 承诺。

薄弱环节

  • 扫描开销大:每笔交易的每个输出都需要一次点乘法。view tag 部分解决了这个问题。
  • Janus 攻击:怀疑两个子地址属于同一个人的发送方,可以用两者的部分信息拼出一个输出,看接收方是否“注意到”这笔付款。钱包对此有部分防护;Carrot 则彻底堵住了它。
  • 发送方知道 r,因此能向第三方证明自己向某个地址付过款(有时很有用——即“支付证明”)。