跳转至主要内容

使用匿名地址

匿名地址
隐私
加密
rust
wasm
中级
Ori Pomerantz
2025年11月30日
23 分钟阅读

你是比尔。 出于我们不打算深入探讨的原因,你想向“爱丽丝竞选世界女王”活动捐款,并希望爱丽丝知道你捐了款,以便在她获胜后奖励你。 不幸的是,她的胜利没有保障。 有一个竞争活动,“卡罗尔竞选太阳系女皇”。 如果卡罗尔获胜,而她发现你向爱丽丝捐了款,你就会有麻烦。 所以你不能直接从你的帐户向爱丽丝的帐户转移 200 ETH。

ERC-5564opens in a new tab 提供了解决方案。 该 ERC 解释了如何使用匿名地址opens in a new tab进行匿名转账。

警告:据我们所知,匿名地址背后的加密技术是可靠的。 但是,存在潜在的侧信道攻击。 在下文中,你将看到可以采取哪些措施来降低此风险。

匿名地址的工作原理

本文将尝试通过两种方式解释匿名地址。 第一种是如何使用它们。 理解这部分内容足以让你看懂本文的其余部分。 然后,是其背后数学原理的解释。 如果你对加密技术感兴趣,也请阅读此部分。

简单版(如何使用匿名地址){#how-use}

爱丽丝创建两个私钥并发布相应的公钥(可以组合成一个双倍长度的元地址)。 比尔也创建一个私钥并发布相应的公钥。

使用一方的公钥和另一方的私钥,可以派生出一个只有爱丽丝和比尔知道的共享密钥(它不能仅从公钥派生)。 利用此共享密钥,比尔获得匿名地址,并可以将资产发送到该地址。

爱丽丝也从共享密钥中获得地址,但因为她知道她发布的公钥所对应的私钥,所以她也可以获得能让她从该地址取款的私钥。

数学原理(为什么匿名地址会这样工作){#how-math}

标准的匿名地址使用椭圆曲线加密 (ECC)opens in a new tab 以更少的密钥位数获得更好的性能,同时保持相同的安全级别。 但在大多数情况下,我们可以忽略这一点,假装我们正在使用常规算术。

有一个大家都知道的数 G。 你可以乘以 G。 但由于 ECC 的性质,除以 G 实际上是不可能的。 以太坊中公钥加密的工作方式通常是,你可以使用私钥 Ppriv 来签署交易,然后由公钥 Ppub = GPpriv 进行验证。

爱丽丝创建两个私钥 KprivVprivKpriv 将用于从匿名地址花钱,Vpriv 用于查看属于爱丽丝的地址。 然后爱丽丝发布公钥:Kpub = GKprivVpub = GVpriv

比尔创建第三个私钥 Rpriv,并将 Rpub = GRpriv 发布到中央注册表(比尔也可以把它发送给爱丽丝,但我们假设卡罗尔正在监听)。

比尔计算 RprivVpub = GRprivVpriv,他希望爱丽丝也知道这个值(下文解释)。 这个值被称为 S,即共享密钥。 这给了比尔一个公钥,Ppub = Kpub+G*hash(S)。 从这个公钥,他可以计算出一个地址,并向其发送任何他想要的资源。 未来,如果爱丽丝获胜,比尔可以告诉她 Rpriv,以证明资源来自他。

爱丽丝计算 RpubVpriv = GRprivVpriv。 这给了她相同的共享密钥 S。 因为她知道私钥 Kpriv,所以她可以计算 Ppriv = Kpriv+hash(S)。 这个密钥让她可以访问由 Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S) 产生的地址中的资产。

我们有一个单独的查看密钥,让爱丽丝可以分包给戴夫的世界统治竞选服务公司。 爱丽丝愿意让戴夫知道公开地址,并在有更多资金时通知她,但她不希望戴夫花掉她的竞选资金。

由于查看和花费使用不同的密钥,爱丽丝可以给戴夫 Vpriv。 然后戴夫可以计算 S = RpubVpriv = GRprivVpriv,并以此方式获得公钥 (Ppub = Kpub+G*hash(S))。 但是没有 Kpriv,戴夫无法获得私钥。

总而言之,这些是不同参与者知道的值。

爱丽丝已发布比尔戴夫
GGGG
Kpriv
VprivVpriv
Kpub = GKprivKpubKpubKpub
Vpub = GVprivVpubVpubVpub
Rpriv
RpubRpubRpub = GRprivRpub
S = RpubVpriv = GRprivVprivS = RprivVpub = GRprivVprivS = RpubVpriv = GRprivVpriv
Ppub = Kpub+G*hash(S)Ppub = Kpub+G*hash(S)Ppub = Kpub+G*hash(S)
地址=f(Ppub)地址=f(Ppub)地址=f(Ppub)地址=f(Ppub)
Ppriv = Kpriv+hash(S)

匿名地址出错时

区块链上没有秘密。 虽然匿名地址可以为你提供隐私,但这种隐私容易受到流量分析的影响。 举一个简单的例子,假设比尔为一个地址注资,并立即发送一笔交易来发布一个 Rpub 值。 没有爱丽丝的 Vpriv,我们无法确定这是一个匿名地址,但很有可能就是。 然后,我们看到另一笔交易,将所有 ETH 从该地址转移到爱丽丝的竞选基金地址。 我们可能无法证明,但很可能比尔刚刚向爱丽丝的竞选活动捐了款。 卡罗尔肯定会这么想。

比尔很容易将 Rpub 的发布与匿名地址的注资分离开来(在不同时间、从不同地址进行)。 然而,这还不够。 卡罗尔寻找的模式是,比尔为一个地址注资,然后爱丽丝的竞选基金从中取款。

一种解决方案是爱丽丝的竞选活动不直接取款,而是用它来支付给第三方。 如果爱丽丝的竞选活动向戴夫的世界统治竞选服务公司发送 10 ETH,卡罗尔只知道比尔向戴夫的一个客户捐了款。 如果戴夫有足够的客户,卡罗尔就无法知道比尔是捐给了与她竞争的爱丽丝,还是捐给了她不关心的亚当、艾伯特或阿比盖尔。 爱丽丝可以在付款中包含一个哈希值,然后向戴夫提供原像,以证明这是她的捐款。 或者,如上所述,如果爱丽丝把她的 Vpriv 给戴夫,他已经知道这笔款项来自谁。

这个解决方案的主要问题在于,它要求爱丽丝在保密对比尔有利时也要关心保密问题。 爱丽丝可能想维护她的声誉,这样比尔的朋友鲍勃也会向她捐款。 但也有可能她不介意曝光比尔,因为那样他就会害怕卡罗尔获胜后会发生什么。 比尔最终可能会为爱丽丝提供更多支持。

使用多个匿名层

比尔可以自己保护自己的隐私,而不用依赖爱丽丝。 他可以为虚构人物鲍勃和贝拉生成多个元地址。 然后比尔将 ETH 发送给鲍勃,“鲍勃”(实际上是比尔)再将其发送给贝拉。 “贝拉”(也是比尔)将其发送给爱丽丝。

卡罗尔仍然可以进行流量分析,并看到比尔-鲍勃-贝拉-爱丽丝的转账流程。 但是,如果“鲍勃”和“贝拉”也将 ETH 用于其他目的,即使爱丽丝立即从匿名地址取款到她已知的竞选地址,也不会显得比尔向爱丽丝转移了任何东西。

编写匿名地址应用程序

本文讲解了一个在 GitHub 上可用的opens in a new tab匿名地址应用程序。

工具

我们可以使用一个TypeScript 匿名地址库opens in a new tab。 但是,加密操作可能占用大量 CPU。 我更喜欢用Rustopens in a new tab这样的编译语言来实现它们,并使用 WASMopens in a new tab 在浏览器中运行代码。

我们将使用 Viteopens in a new tabReactopens in a new tab。 这些是行业标准工具;如果你不熟悉它们,可以使用本教程。 要使用 Vite,我们需要 Node。

查看匿名地址的实际操作

  1. 安装必要的工具:Rustopens in a new tabNodeopens in a new tab

  2. 克隆 GitHub 存储库。

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
  3. 安装先决条件并编译 Rust 代码。

    1cd src/rust-wasm
    2rustup target add wasm32-unknown-unknown
    3cargo install wasm-pack
    4wasm-pack build --target web
  4. 启动 Web 服务器。

    1cd ../..
    2npm install
    3npm run dev
  5. 浏览到该应用程序opens in a new tab。 此应用程序页面有两个框架:一个是爱丽丝的用户界面,另一个是比尔的用户界面。 这两个框架不通信;它们仅为方便起见而位于同一页面上。

  6. 作为爱丽丝,点击生成匿名元地址。 这将显示新的匿名地址和相应的私钥。 将匿名元地址复制到剪贴板。

  7. 以比尔的身份,粘贴新的匿名元地址,然后点击生成地址。 这会给你为爱丽丝注资的地址。

  8. 复制地址和比尔的公钥,并将它们粘贴到爱丽丝用户界面的“比尔生成的地址的私钥”区域。 填写完这些字段后,你将看到访问该地址资产的私钥。

  9. 你可以使用在线计算器opens in a new tab来确保私钥与地址对应。

程序工作原理

WASM 组件

编译成 WASM 的源代码是用 Rustopens in a new tab 编写的。 你可以在 src/rust_wasm/src/lib.rsopens in a new tab 中看到它。 此代码主要是 JavaScript 代码和 eth-stealth-addressesopens in a new tab之间的接口。

Cargo.toml

Rust 中的 Cargo.tomlopens in a new tab 类似于 JavaScript 中的 package.jsonopens in a new tab。 它包含包信息、依赖项声明等。

1[package]
2name = "rust-wasm"
3version = "0.1.0"
4edition = "2024"
5
6[dependencies]
7eth-stealth-addresses = "0.1.0"
8hex = "0.4.3"
9wasm-bindgen = "0.2.104"
10getrandom = { version = "0.2", features = ["js"] }
显示全部

getrandomopens in a new tab 包需要生成随机值。 这不能通过纯算法方式完成;它需要访问物理过程作为熵的来源。 该定义指定我们将通过询问我们正在运行的浏览器来获得该熵。

1console_error_panic_hook = "0.1.7"

当 WASM 代码发生 panic 且无法继续时,此库opens in a new tab会为我们提供更有意义的错误信息。

1[lib]
2crate-type = ["cdylib", "rlib"]

生成 WASM 代码所需的输出类型。

lib.rs

这是实际的 Rust 代码。

1use wasm_bindgen::prelude::*;

用 Rust 创建 WASM 包的定义。 这里opens in a new tab有相关文档。

1use eth_stealth_addresses::{
2 generate_stealth_meta_address,
3 generate_stealth_address,
4 compute_stealth_key
5};

我们需要从 eth-stealth-addressesopens in a new tab中获取的函数。

1use hex::{decode,encode};

Rust 通常使用字节数组opens in a new tab ([u8; <size>]) 来表示值。 但在 JavaScript 中,我们通常使用十六进制字符串。 hexopens in a new tab为我们翻译一种表示到另一种表示。

1#[wasm_bindgen]

生成 WASM 绑定,以便能够从 JavaScript 调用此函数。

1pub fn wasm_generate_stealth_meta_address() -> String {

返回包含多个字段的对象的简单方法是返回一个 JSON 字符串。

1 let (address, spend_private_key, view_private_key) =
2 generate_stealth_meta_address();

generate_stealth_meta_addressopens in a new tab 返回三个字段:

  • 元地址(KpubVpub
  • 查看私钥 (Vpriv)
  • 花费私钥 (Kpriv)

元组opens in a new tab语法让我们能够再次分离这些值。

1 format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
2 encode(address),
3 encode(view_private_key),
4 encode(spend_private_key)
5 )
6}

使用 format!opens in a new tab 宏生成 JSON 编码的字符串。 使用 hex::encodeopens in a new tab 将数组更改为十六进制字符串。

1fn str_to_array<const N: usize>(s: &str) -> Option<[u8; N]> {

此函数将(由 JavaScript 提供的)十六进制字符串转换为字节数组。 我们用它来解析 JavaScript 代码提供的值。 由于 Rust 处理数组和向量的方式,此函数比较复杂。

<const N: usize> 表达式被称为泛型opens in a new tabN 是一个控制返回数组长度的参数。 该函数实际上被称为 str_to_array::<n>,其中 n 是数组长度。

返回值为 Option<[u8; N]>,这意味着返回的数组是可选的opens in a new tab。 这在 Rust 中是可能失败的函数的典型模式。

例如,如果我们调用 str_to_array::10("bad060a7"),该函数应该返回一个十值数组,但输入只有四个字节。 该函数需要失败,它通过返回 None 来实现。 str_to_array::4("bad060a7") 的返回值将是 Some<[0xba, 0xd0, 0x60, 0xa7]>

1 // decode 返回 Result<Vec<u8>, _>
2 let vec = decode(s).ok()?;

hex::decodeopens in a new tab 函数返回一个 Result<Vec<u8>, FromHexError>Resultopens in a new tab 类型可以包含成功的结果(Ok(value))或错误(Err(error))。

.ok() 方法将 Result 转换为 Option,如果成功,其值为 Ok() 值,否则为 None。 最后,问号运算符opens in a new tab会中止当前函数,并在 Option 为空时返回 None。 否则,它会解包值并返回该值(在这种情况下,为 vec 赋值)。

这看起来像一个奇怪复杂的错误处理方法,但 ResultOption 确保所有错误都以某种方式处理。

1 if vec.len() != N { return None; }

如果字节数不正确,那就是失败,我们返回 None

1 // try_into 消耗 vec 并尝试生成 [u8; N]
2 let array: [u8; N] = vec.try_into().ok()?;

Rust 有两种数组类型。 数组opens in a new tab具有固定大小。 向量opens in a new tab可以增长和收缩。 hex::decode 返回一个向量,但 eth_stealth_addresses 库希望接收数组。 .try_into()opens in a new tab 将一个值转换为另一个类型,例如,将向量转换为数组。

1 Some(array)
2}

在函数末尾返回值时,Rust 不要求你使用 returnopens in a new tab 关键字。

1#[wasm_bindgen]
2pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {

此函数接收一个公共元地址,其中包括 VpubKpub。 它返回匿名地址、要发布的公钥 (Rpub) 和一个单字节扫描值,该值可加快识别哪些已发布的地址可能属于爱丽丝。

扫描值是共享密钥的一部分(S = GRprivVpriv)。 爱丽丝可以使用此值,检查此值比检查 f(Kpub+G*hash(S)) 是否等于已发布地址要快得多。

1 let (address, r_pub, scan) =
2 generate_stealth_address(&str_to_array::<66>(stealth_address)?);

我们使用库的 generate_stealth_addressopens in a new tab

1 format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
2 encode(address),
3 encode(r_pub),
4 encode(&[scan])
5 ).into()
6}

准备 JSON 编码的输出字符串。

1#[wasm_bindgen]
2pub fn wasm_compute_stealth_key(
3 address: &str,
4 bill_pub_key: &str,
5 view_private_key: &str,
6 spend_private_key: &str
7) -> Option<String> {
8 .
9 .
10 .
11}
显示全部

该函数使用库的 compute_stealth_keyopens in a new tab 来计算从该地址取款的私钥 (Rpriv)。 此计算需要以下值:

  • 地址 (Address=f(Ppub))
  • 比尔生成的公钥 (Rpub)
  • 查看私钥 (Vpriv)
  • 花费私钥 (Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)]opens in a new tab 指定在初始化 WASM 代码时执行该函数。

1pub fn main() {
2 console_error_panic_hook::set_once();
3}

此代码指定将 panic 输出发送到 JavaScript 控制台。 要查看其操作,请使用该应用程序并为比尔提供一个无效的元地址(只需更改一个十六进制数字)。 你将在 JavaScript 控制台中看到此错误:

1rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs:701:9:
2assertion `left == right` failed
3 left: 0
4 right: 1

后跟一个堆栈跟踪。 然后给比尔有效的元地址,并给爱丽丝一个无效的地址或无效的公钥。 你将看到此错误:

1rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eth-stealth-addresses-0.1.0/src/lib.rs:78:9:
2keys do not generate stealth address

同样,后跟一个堆栈跟踪。

用户界面

用户界面是使用 Reactopens in a new tab 编写的,并由 Viteopens in a new tab 提供服务。 你可以使用本教程来学习它们。 这里不需要 WAGMIopens in a new tab,因为我们不直接与区块链或钱包交互。

用户界面中唯一不明显的部分是 WASM 连接。 下面是它的工作原理。

vite.config.js

此文件包含 Vite 配置opens in a new tab

1import { defineConfig } from 'vite'
2import react from '@vitejs/plugin-react'
3import wasm from "vite-plugin-wasm";
4
5// https://vite.dev/config/
6export default defineConfig({
7 plugins: [react(), wasm()],
8})

我们需要两个 Vite 插件:reactopens in a new tabwasmopens in a new tab

App.jsx

此文件是应用程序的主组件。 它是一个容器,包含两个组件:AliceBill,即这些用户的用户界面。 WASM 的相关部分是初始化代码。

1import init from './rust-wasm/pkg/rust_wasm.js'

当我们使用 wasm-packopens in a new tab 时,它会创建我们在这里使用的两个文件:一个包含实际代码的 wasm 文件(此处为 src/rust-wasm/pkg/rust_wasm_bg.wasm)和一个包含使用定义的 JavaScript 文件(此处为 src/rust_wasm/pkg/rust_wasm.js)。 该 JavaScript 文件的默认导出是需要运行以启动 WASM 的代码。

1function App() {
2 .
3 .
4 .
5 useEffect(() => {
6 const loadWasm = async () => {
7 try {
8 await init();
9 setWasmReady(true)
10 } catch (err) {
11 console.error('Error loading wasm:', err)
12 alert("Wasm error: " + err)
13 }
14 }
15
16 loadWasm()
17 }, []
18 )
显示全部

useEffect 钩子opens in a new tab允许你指定一个在状态变量更改时执行的函数。 这里,状态变量列表为空([]),因此此函数仅在页面加载时执行一次。

效果函数必须立即返回。 要使用异步代码,例如 WASM init(它必须加载 .wasm 文件,因此需要时间),我们定义一个内部 asyncopens in a new tab 函数并在没有 await 的情况下运行它。

Bill.jsx

这是比尔的用户界面。 它只有一个操作,即根据爱丽丝提供的匿名元地址创建一个地址。

1import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'

除了默认导出之外,wasm-pack 生成的 JavaScript 代码还为 WASM 代码中的每个函数导出一个函数。

1 <button onClick={() => {
2 setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))
3 }}>

要调用 WASM 函数,我们只需调用 wasm-pack 创建的 JavaScript 文件导出的函数。

Alice.jsx

Alice.jsx 中的代码是类似的,只是爱丽丝有两个操作:

  • 生成元地址
  • 获取比尔发布的地址的私钥

结论

匿名地址不是万能药;它们必须被正确使用。 但如果使用得当,它们可以在公共区块链上实现隐私。

点击此处查看我的更多作品opens in a new tab

页面最后更新: 2025年11月14日

本教程对你有帮助吗?