使用隱匿地址
您是 Bill。 我們不深入探討原因,假設您想捐款給「Alice 競選世界女王」活動,並希望 Alice 知道您捐了款,以便在她勝選後給予您酬勞。 可惜,她不保證會勝選。 有個競爭活動:「Carol 競選太陽系女皇」。 如果 Carol 勝選,而且她發現您捐款給 Alice,您就會有麻煩了。 所以您不能直接從您的帳戶轉 200 ETH 給 Alice。
ERC-5564 (opens in a new tab) 提供了這個問題的解決辦法。 此 ERC 說明了如何使用 隱匿地址 (opens in a new tab) 進行匿名轉帳。
警告:據我們所知,隱匿地址背後的密碼學是健全的。 不過,仍有潛在的旁路攻擊。 您可以在下方看到如何降低此風險。
隱匿地址的運作方式
本文將會以兩種方式說明隱匿地址。 第一種是如何使用。 這部分就足以理解本文的其餘內容。 接著會說明其背後的數學原理。 如果您對密碼學感興趣,也請閱讀這部分。
簡易版本(如何使用隱匿地址)
Alice 建立了兩把私鑰,並發布了相應的公鑰(可合併成單一雙倍長度的中繼地址)。 Bill 也建立了一把私鑰,並發布了相應的公鑰。
使用其中一方的公鑰和另一方的私鑰,您可以推導出只有 Alice 和 Bill 知道的共享密鑰(無法僅從公鑰推導)。 Bill 可透過此共享密鑰取得隱匿地址,並將資產傳送至該地址。
Alice 也能從共享密鑰取得地址,但由於她知道自己所發布公鑰的私鑰,她也能取得讓她從該地址提款的私鑰。
數學原理(隱匿地址為何如此運作)
標準隱匿地址使用橢圓曲線密碼學 (ECC) (opens in a new tab) 來以較少的金鑰位元達到更佳的效能,同時維持相同等級的安全性。 但在大多數情況下,我們可以忽略這點,假裝我們使用的是常規算術。
有一個大家都知道的數字 G。 您可以將其乘以 G。 但由於橢圓曲線密碼學 (ECC) 的性質,幾乎不可能將其除以 G。 以太坊中公鑰密碼學的一般運作方式是,您可以使用私鑰 Ppriv 來簽署交易,然後由公鑰 Ppub = GPpriv 進行驗證。
Alice 建立兩把私鑰:Kpriv 和 Vpriv。 Kpriv 將用來從隱匿地址花費金錢,而 Vpriv 則用來檢視屬於 Alice 的地址。 Alice 接著發布公鑰:Kpub = GKpriv 和 Vpub = GVpriv
Bill 建立了第三把私鑰 Rpriv,並將 Rpub = GRpriv 發布到中央註冊處(Bill 也可以把它傳送給 Alice,但我們假設 Carol 正在竊聽)。
Bill 計算 RprivVpub = GRprivVpriv,他預期 Alice 也會知道(如下文說明)。 此值稱為 S,也就是共享密鑰。 這給了 Bill 一把公鑰:Ppub = Kpub+G*hash(S)。 他可以從這把公鑰計算出一個地址,並將任何他想要的資源傳送到該地址。 未來如果 Alice 勝選,Bill 可以告訴她 Rpriv 以證明資源來自於他。
Alice 計算 RpubVpriv = GRprivVpriv。 這給了她相同的共享密鑰 S。 因為她知道私鑰 Kpriv,她可以計算出 Ppriv = Kpriv+hash(S)。 這把金鑰讓她能存取地址中的資產,而該地址是從 Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S) 產生的。
我們有一把獨立的檢視金鑰,讓 Alice 可以將工作分包給 Dave 的「世界統治競選服務」。 Alice 願意讓 Dave 知道公用地址,並在有更多資金時通知她,但她不希望 Dave 花費她的競選資金。
因為檢視和花費使用不同的金鑰,所以 Alice 可以把 Vpriv 給 Dave。 然後 Dave 可以計算 S = RpubVpriv = GRprivVpriv,並以此取得公鑰(Ppub = Kpub+G*hash(S))。 但如果沒有 Kpriv,Dave 就無法取得私鑰。
總而言之,以下是不同參與者所知道的值。
| Alice | 已發布 | Bill | Dave | |
|---|---|---|---|---|
| G | G | G | G | |
| Kpriv | - | - | - | |
| Vpriv | - | - | Vpriv | |
| Kpub = GKpriv | Kpub | Kpub | Kpub | |
| Vpub = GVpriv | Vpub | Vpub | Vpub | |
| - | - | Rpriv | - | |
| Rpub | Rpub | Rpub = GRpriv | Rpub | |
| S = RpubVpriv = GRprivVpriv | - | S = RprivVpub = GRprivVpriv | S = RpubVpriv = GRprivVpriv | |
| Ppub = Kpub+G*hash(S) | - | Ppub = Kpub+G*hash(S) | Ppub = Kpub+G*hash(S) | |
| Address=f(Ppub) | - | Address=f(Ppub) | Address=f(Ppub) | Address=f(Ppub) |
| Ppriv = Kpriv+hash(S) | - | - | - |
隱匿地址出錯時
區塊鏈上沒有祕密。 雖然隱匿地址可以提供隱私,但這種隱私很容易受到流量分析的影響。 舉一個簡單的例子,假設 Bill 為一個地址提供資金,並立即傳送一筆交易來發布一個 Rpub 值。 如果沒有 Alice 的 Vpriv,我們無法確定這是一個隱匿地址,但可以這麼猜測。 然後,我們看到另一筆交易,將該地址的所有 ETH 轉移到 Alice 的競選基金地址。 我們可能無法證明,但很有可能 Bill 剛剛捐款給 Alice 的競選活動。 Carol 肯定會這麼想。
Bill 很容易將 Rpub 的發布與隱匿地址的資金分開(在不同時間,從不同地址進行)。 然而,這還不夠。 Carol 尋找的模式是 Bill 為一個地址提供資金,然後 Alice 的競選基金從中提款。
一個解決方案是 Alice 的競選活動不要直接提款,而是用它來支付給第三方。 如果 Alice 的競選活動將 10 ETH 傳送到 Dave 的「世界統治競選服務」,Carol 只知道 Bill 捐款給 Dave 的其中一個客戶。 如果 Dave 有足夠的客戶,Carol 就無法知道 Bill 是捐款給與她競爭的 Alice,還是捐給 Carol 不在乎的 Adam、Albert 或 Abigail。 Alice 可以在付款中包含一個哈希值,然後向 Dave 提供原像,以證明這是她的捐款。 或者,如上所述,如果 Alice 給了 Dave 她的 Vpriv,他就已經知道付款來自誰。
這個解決方案的主要問題是,它要求 Alice 在保密對 Bill 有利時,也要關心保密。 Alice 可能想要維持她的聲譽,這樣 Bill 的朋友 Bob 也會捐款給她。 但也有可能她不介意揭發 Bill,因為這樣 Bill 就會害怕如果 Carol 獲勝會發生什麼事。 Bill 最終可能會提供 Alice 更多的支持。
使用多個隱匿層
Bill 可以自己保護自己的隱私,而不是依靠 Alice。 他可以為虛構的人物 Bob 和 Bella 產生多個中繼地址。 然後 Bill 將 ETH 傳送給 Bob,而「Bob」(實際上是 Bill)再將其傳送給 Bella。 「Bella」(也是 Bill)再將其傳送給 Alice。
Carol 仍然可以進行流量分析,並看到從 Bill 到 Bob 再到 Bella 再到 Alice 的管道。 然而,如果「Bob」和「Bella」也將 ETH 用於其他目的,那麼即使 Alice 立即從隱匿地址提款到她已知的競選地址,也不會顯示 Bill 將任何東西轉移給 Alice。
編寫隱匿地址應用程式
本文說明了一個在 GitHub 上可用的 (opens in a new tab)隱匿地址應用程式。
工具
我們可以使用一個 typescript 隱匿地址程式庫 (opens in a new tab)。 然而,密碼學操作可能非常耗費 CPU。 我偏好以 Rust (opens in a new tab) 這類編譯語言實作,並使用 WASM (opens in a new tab) 在瀏覽器中執行程式碼。
我們將使用 Vite (opens in a new tab) 和 React (opens in a new tab)。 這些是業界標準的工具;如果您不熟悉,可以使用此教學。 要使用 Vite,我們需要 Node。
實際操作隱匿地址
-
安裝必要的工具:Rust (opens in a new tab) 和 Node (opens in a new tab)。
-
複製 GitHub 存放庫。
1git clone https://github.com/qbzzt/251022-stealth-addresses.git2cd 251022-stealth-addresses -
安裝先決條件並編譯 Rust 程式碼。
1cd src/rust-wasm2rustup target add wasm32-unknown-unknown3cargo install wasm-pack4wasm-pack build --target web -
啟動網頁伺服器。
1cd ../..2npm install3npm run dev -
瀏覽至應用程式 (opens in a new tab)。 此應用程式頁面有兩個框架:一個用於 Alice 的使用者介面,另一個用於 Bill 的使用者介面。 這兩個框架不互相通訊;它們僅為了方便而放在同一個頁面上。
-
以 Alice 的身分,點擊 Generate a Stealth Meta-Address(產生隱匿中繼地址)。 這會顯示新的隱匿地址和相應的私鑰。 將隱匿中繼地址複製到剪貼簿。
-
以 Bill 的身分,貼上新的隱匿中繼地址並點擊 Generate an address(產生地址)。 這會提供您要為 Alice 提供資金的地址。
-
複製地址和 Bill 的公鑰,並將它們貼到 Alice 使用者介面的「Private key for address generated by Bill」(Bill 產生的地址之私鑰)區域中。 填寫完這些欄位後,您會看到存取該地址資產的私鑰。
-
您可以使用線上計算機 (opens in a new tab)來確保私鑰與地址相符。
程式運作方式
WASM 元件
編譯成 WASM 的原始碼是以 Rust (opens in a new tab) 編寫的。 您可以在 src/rust_wasm/src/lib.rs (opens in a new tab) 中看到它。 此程式碼主要是 JavaScript 程式碼與 eth-stealth-addresses 程式庫 (opens in a new tab)之間的介面。
Cargo.toml
Rust 中的 Cargo.toml (opens in a new tab) 類似於 JavaScript 中的 package.json (opens in a new tab)。 其中包含套件資訊、相依性宣告等。
1[package]2name = "rust-wasm"3version = "0.1.0"4edition = "2024"56[dependencies]7eth-stealth-addresses = "0.1.0"8hex = "0.4.3"9wasm-bindgen = "0.2.104"10getrandom = { version = "0.2", features = ["js"] }顯示全部getrandom (opens in a new tab) 套件需要產生隨機值。 這無法單純透過演算法達成;它需要存取實體程序作為熵的來源。 此定義指定我們將透過詢問我們正在執行的瀏覽器來取得該熵。
1console_error_panic_hook = "0.1.7"此程式庫 (opens in a new tab) 在 WASM 程式碼發生恐慌且無法繼續時,提供我們更有意義的錯誤訊息。
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_key5};我們需要從 eth-stealth-addresses 程式庫 (opens in a new tab) 取得的函式。
1use hex::{decode,encode};Rust 通常使用位元組陣列 (opens in a new tab) ([u8; <size>]) 來表示值。 但在 JavaScript 中,我們通常使用十六進位字串。 hex 程式庫 (opens 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_address (opens in a new tab) 傳回三個欄位:
- 中繼地址(Kpub 和 Vpub)
- 檢視私鑰(Vpriv)
- 花費私鑰(Kpriv)
tuple (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::encode (opens 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 tab)。 N 是一個控制傳回陣列長度的參數。 此函式實際上稱為 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::decode (opens in a new tab) 函式傳回 Result<Vec<u8>, FromHexError>。 Result (opens in a new tab) 類型可以包含成功結果(Ok(value))或錯誤(Err(error))。
.ok() 方法會將 Result 轉換為 Option,其值若成功則為 Ok() 值,否則為 None。 最後,問號運算子 (opens in a new tab) 會在 Option 為空時中止目前的函式並傳回 None。 否則,它會解開值並傳回它(在此情況下,是將值指派給 vec)。
這看起來像是一種處理錯誤的奇怪複雜方法,但 Result 和 Option 確保所有錯誤都以某種方式處理。
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 不要求您使用 return (opens in a new tab) 關鍵字。
1#[wasm_bindgen]2pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {此函式接收一個公用中繼地址,其中包含 Vpub 和 Kpub。 它會傳回隱匿地址、要發布的公鑰(Rpub),以及一個一位元組的掃描值,用於加速識別哪些已發布的地址可能屬於 Alice。
掃描值是共享密鑰(S = GRprivVpriv)的一部分。 此值可供 Alice 使用,檢查此值比檢查 f(Kpub+G*hash(S)) 是否等於已發布地址快得多。
1 let (address, r_pub, scan) = 2 generate_stealth_address(&str_to_array::<66>(stealth_address)?);我們使用程式庫的 generate_stealth_address (opens 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_key (opens in a new tab) 來計算從地址提款的私鑰 (Rpriv)。 此計算需要以下值:
- 地址(Address=f(Ppub))
- Bill 產生的公鑰(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}此程式碼指定將恐慌輸出傳送到 JavaScript 主機。 要看它實際運作,請使用應用程式並給 Bill 一個無效的中繼地址(只要更改一個十六進位數字)。 您會在 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` failed3 left: 04 right: 1後面跟著堆疊追蹤。 然後給 Bill 有效的中繼地址,並給 Alice 一個無效的地址或無效的公鑰。 您將會看到此錯誤:
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同樣地,後面跟著堆疊追蹤。
使用者介面
使用者介面是使用 React (opens in a new tab) 編寫並由 Vite (opens in a new tab) 提供服務。 您可以使用此教學來瞭解它們。 這裡不需要 WAGMI (opens 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";45// https://vite.dev/config/6export default defineConfig({7 plugins: [react(), wasm()],8})我們需要兩個 Vite 外掛程式:react (opens in a new tab) 和 wasm (opens in a new tab)。
App.jsx
此檔案是應用程式的主要元件。 它是一個容器,包含兩個元件:Alice 和 Bill,也就是這些使用者的使用者介面。 與 WASM 相關的部分是初始化程式碼。
1import init from './rust-wasm/pkg/rust_wasm.js'當我們使用 wasm-pack (opens 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 }1516 loadWasm()17 }, []18 )顯示全部useEffect 鉤子 (opens in a new tab) 可讓您指定一個函式,該函式會在狀態變數變更時執行。 在此,狀態變數清單是空的([]),所以此函式只會在頁面載入時執行一次。
效果函式必須立即傳回。 要使用非同步程式碼,例如 WASM init(必須載入 .wasm 檔,因此需要時間),我們定義一個內部 async (opens in a new tab) 函式,並在沒有 await 的情況下執行它。
Bill.jsx
這是 Bill 的使用者介面。 它只有一個動作,就是根據 Alice 提供的隱匿中繼地址建立一個地址。
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 中的程式碼類似,只是 Alice 有兩個動作:
- 產生一個中繼地址
- 取得 Bill 發布的地址的私鑰
結論
隱匿地址並非萬靈丹;它們必須正確使用。 但只要正確使用,它們就可以在公開區塊鏈上實現隱私。
在此查看我的更多作品 (opens in a new tab)。
頁面最後更新時間: 2025年11月14日