Přeskočit na hlavní obsah

Použití skrytých adres

Skrytá adresa
soukromí
kryptografie
rust
wasm
Středně pokročilý
Ori Pomerantz
30. listopadu 2025
14 minuta čtení

Jste Bill. Z důvodů, do kterých nebudeme zabíhat, chcete přispět na kampaň „Alice za královnu světa“ a chcete, aby Alice věděla, že jste přispěli, aby vás odměnila, pokud vyhraje. Její vítězství však bohužel není zaručeno. Existuje konkurenční kampaň „Carol za císařovnu Sluneční soustavy“. Pokud Carol vyhraje a zjistí, že jste přispěli Alici, budete mít potíže. Nemůžete tedy jen tak převést 200 ETH ze svého účtu na účet Alice.

ERC-5564 (opens in a new tab) nabízí řešení. Tento ERC vysvětluje, jak používat skryté adresy (opens in a new tab) pro anonymní převody.

Upozornění: Kryptografie za skrytými adresami je, pokud víme, spolehlivá. Existují však potenciální útoky postranním kanálem. Níže uvidíte, co můžete udělat pro snížení tohoto rizika.

Jak fungují skryté adresy

Tento článek se pokusí vysvětlit skryté adresy dvěma způsoby. První je jak je používat. Tato část je dostatečná k pochopení zbytku článku. Dále je zde vysvětlení matematiky, která za tím stojí. Pokud se zajímáte o kryptografii, přečtěte si i tuto část.

Zjednodušená verze (jak používat skryté adresy)

Alice vytvoří dva privátní klíče a zveřejní odpovídající veřejné klíče (které lze zkombinovat do jedné meta-adresy dvojnásobné délky). Bill také vytvoří privátní klíč a zveřejní odpovídající veřejný klíč.

Pomocí veřejného klíče jedné strany a privátního klíče druhé strany můžete odvodit sdílené tajemství známé pouze Alici a Billovi (nelze jej odvodit pouze z veřejných klíčů). Pomocí tohoto sdíleného tajemství získá Bill skrytou adresu a může na ni posílat aktiva.

Alice také získá adresu ze sdíleného tajemství, ale protože zná privátní klíče k veřejným klíčům, které zveřejnila, může také získat privátní klíč, který jí umožní vybírat z této adresy.

Matematika (proč skryté adresy fungují takto)

Standardní skryté adresy používají kryptografii na bázi eliptických křivek (ECC) (opens in a new tab), aby dosáhly lepšího výkonu s menším počtem bitů klíče a zároveň zachovaly stejnou úroveň zabezpečení. Ale z větší části to můžeme ignorovat a předstírat, že používáme běžnou aritmetiku.

Existuje číslo, které každý zná, G. Můžete násobit G. Ale vzhledem k povaze ECC je prakticky nemožné dělit G. Způsob, jakým kryptografie s veřejným klíčem v Ethereu obecně funguje, je, že můžete použít privátní klíč Ppriv k podepisování transakcí, které jsou poté ověřeny veřejným klíčem Ppub = GPpriv.

Alice vytvoří dva privátní klíče, Kpriv a Vpriv. Kpriv se použije k utrácení peněz ze skryté adresy a Vpriv k zobrazení adres, které patří Alici. Alice poté zveřejní veřejné klíče: Kpub = GKpriv a Vpub = GVpriv

Bill vytvoří třetí privátní klíč, Rpriv, a zveřejní Rpub = GRpriv v centrálním registru (Bill ho mohl poslat také Alici, ale předpokládáme, že Carol poslouchá).

Bill vypočítá RprivVpub = GRprivVpriv, o kterém předpokládá, že ho zná i Alice (vysvětleno níže). Tato hodnota se nazývá S, sdílené tajemství. Tím Bill získá veřejný klíč, Ppub = Kpub+G*haš(S). Z tohoto veřejného klíče může vypočítat adresu a poslat na ni jakékoli zdroje, které chce. V budoucnu, pokud Alice vyhraje, může jí Bill sdělit Rpriv, aby dokázal, že zdroje pocházejí od něj.

Alice vypočítá RpubVpriv = GRprivVpriv. Tím získá stejné sdílené tajemství, S. Protože zná privátní klíč, Kpriv, může vypočítat Ppriv = Kpriv+haš(S). Tento klíč jí umožňuje přístup k aktivům na adrese, která je výsledkem Ppub = GPpriv = GKpriv+G*haš(S) = Kpub+G*haš(S).

Máme samostatný zobrazovací klíč, který Alici umožňuje zadat subdodávku službám Dave's World Domination Campaign Services. Alice je ochotna dát Davovi vědět veřejné adresy a informovat ji, až bude k dispozici více peněz, ale nechce, aby utrácel peníze z její kampaně.

Protože prohlížení a utrácení používají samostatné klíče, může Alice dát Daveovi Vpriv. Potom může Dave vypočítat S = RpubVpriv = GRprivVpriv a tímto způsobem získat veřejné klíče (Ppub = Kpub+G*haš(S)). Ale bez Kpriv nemůže Dave získat privátní klíč.

Abychom to shrnuli, toto jsou hodnoty známé různým účastníkům.

AliceZveřejněnoBillDave
GGGG
Kpriv
VprivVpriv
Kpub = GKprivKpubKpubKpub
Vpub = GVprivVpubVpubVpub
Rpriv
RpubRpubRpub = GRprivRpub
S = RpubVpriv = GRprivVprivS = RprivVpub = GRprivVprivS = RpubVpriv = GRprivVpriv
Ppub = Kpub+G*haš(S)Ppub = Kpub+G*haš(S)Ppub = Kpub+G*haš(S)
Adresa=f(Ppub)Adresa=f(Ppub)Adresa=f(Ppub)Adresa=f(Ppub)
Ppriv = Kpriv+haš(S)

Když se skryté adresy pokazí

Na blockchainu neexistují žádná tajemství. Zatímco skryté adresy vám mohou poskytnout soukromí, toto soukromí je náchylné k analýze provozu. Jako triviální příklad si představte, že Bill financuje adresu a okamžitě pošle transakci ke zveřejnění hodnoty Rpub. Bez Alicina Vpriv si nemůžeme být jisti, že se jedná o skrytou adresu, ale je to nejpravděpodobnější. Potom vidíme další transakci, která převádí všechny ETH z této adresy na adresu fondu kampaně Alice. Možná to nebudeme schopni dokázat, ale je pravděpodobné, že Bill právě přispěl na kampaň Alice. Carol by si to určitě myslela.

Pro Billa je snadné oddělit zveřejnění Rpub od financování skryté adresy (udělat to v různých časech z různých adres). To je však nedostatečné. Vzor, který Carol hledá, je, že Bill financuje adresu a poté z ní fond kampaně Alice vybírá.

Jedním řešením je, aby kampaň Alice peníze nevybírala přímo, ale použila je k zaplacení třetí straně. Pokud kampaň Alice pošle 10 ETH společnosti Dave's World Domination Campaign Services, Carol ví jen to, že Bill přispěl jednomu z Daveových zákazníků. Pokud má Dave dostatek zákazníků, Carol by nemohla vědět, zda Bill přispěl Alici, která s ní soupeří, nebo Adamovi, Albertovi či Abigail, o které se Carol nezajímá. Alice může k platbě přiložit hašovanou hodnotu a poté poskytnout Daveovi předobraz, aby dokázala, že to byl její dar. Alternativně, jak je uvedeno výše, pokud Alice dá Daveovi svůj Vpriv, už ví, od koho platba přišla.

Hlavním problémem tohoto řešení je, že vyžaduje, aby Alice dbala na utajení, když toto utajení prospívá Billovi. Alice si možná bude chtít udržet svou pověst, aby jí přispěl i Billův přítel Bob. Ale je také možné, že by jí nevadilo Billa odhalit, protože by se pak bál, co se stane, když vyhraje Carol. Bill by mohl nakonec poskytnout Alici ještě větší podporu.

Použití více skrytých vrstev

Místo spoléhání se na Alici, aby chránila Billovo soukromí, to může Bill udělat sám. Může generovat více meta-adres pro fiktivní osoby, Boba a Bellu. Bill poté pošle ETH Bobovi a „Bob“ (což je ve skutečnosti Bill) je pošle Belle. „Bella“ (také Bill) je pošle Alici.

Carol může stále provádět analýzu provozu a vidět řetězec Bill-Bob-Bella-Alice. Pokud však „Bob“ a „Bella“ také používají ETH pro jiné účely, nebude se zdát, že Bill cokoli převedl Alici, i když Alice okamžitě vybere peníze ze skryté adresy na svou známou adresu kampaně.

Napsání aplikace se skrytými adresami

Tento článek vysvětluje aplikaci se skrytými adresami dostupnou na GitHubu (opens in a new tab).

Nástroje

Existuje typescriptová knihovna pro skryté adresy (opens in a new tab), kterou bychom mohli použít. Kryptografické operace však mohou být náročné na CPU. Dávám přednost jejich implementaci v kompilovaném jazyce, jako je Rust (opens in a new tab), a použití WASM (opens in a new tab) ke spuštění kódu v prohlížeči.

Použijeme Vite (opens in a new tab) a React (opens in a new tab). Jedná se o standardní nástroje v oboru; pokud je neznáte, můžete použít tento tutoriál. Pro použití Vite potřebujeme Node.

Podívejte se na skryté adresy v akci

  1. Nainstalujte si potřebné nástroje: Rust (opens in a new tab) a Node (opens in a new tab).

  2. Naklonujte repozitář na GitHubu.

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
  3. Nainstalujte předpoklady a zkompilujte kód Rustu.

    1cd src/rust-wasm
    2rustup target add wasm32-unknown-unknown
    3cargo install wasm-pack
    4wasm-pack build --target web
  4. Spusťte webový server.

    1cd ../..
    2npm install
    3npm run dev
  5. Přejděte do aplikace (opens in a new tab). Tato stránka aplikace má dva rámce: jeden pro uživatelské rozhraní Alice a druhý pro Billa. Tyto dva rámce spolu nekomunikují; jsou na stejné stránce pouze pro pohodlí.

  6. Jako Alice klikněte na Generate a Stealth Meta-Address. Zobrazí se nová skrytá adresa a odpovídající privátní klíče. Zkopírujte skrytou meta-adresu do schránky.

  7. Jako Bill vložte novou skrytou meta-adresu a klikněte na Generate an address. Tím získáte adresu pro financování Alice.

  8. Zkopírujte adresu a Billův veřejný klíč a vložte je do oblasti „Privátní klíč pro adresu generovanou Billem“ v uživatelském rozhraní Alice. Jakmile budou tato pole vyplněna, uvidíte privátní klíč pro přístup k aktivům na této adrese.

  9. Můžete použít online kalkulačku (opens in a new tab), abyste se ujistili, že privátní klíč odpovídá adrese.

Jak program funguje

Komponenta WASM

Zdrojový kód, který se kompiluje do WASM, je napsán v Rustu (opens in a new tab). Můžete si ho prohlédnout v src/rust_wasm/src/lib.rs (opens in a new tab). Tento kód je primárně rozhraním mezi JavaScriptovým kódem a knihovnou eth-stealth-addresses (opens in a new tab).

Cargo.toml

Cargo.toml (opens in a new tab) v Rustu je obdobou package.json (opens in a new tab) v JavaScriptu. Obsahuje informace o balíčku, deklarace závislostí atd.

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"] }
Zobrazit vše

Balíček getrandom (opens in a new tab) potřebuje generovat náhodné hodnoty. Toho nelze dosáhnout čistě algoritmickými prostředky; vyžaduje to přístup k fyzickému procesu jako zdroji entropie. Tato definice určuje, že entropii získáme dotazem na prohlížeč, ve kterém běžíme.

1console_error_panic_hook = "0.1.7"

Tato knihovna (opens in a new tab) nám poskytuje smysluplnější chybové zprávy, když kód WASM zpanikaří a nemůže pokračovat.

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

Výstupní typ potřebný k vytvoření kódu WASM.

lib.rs

Toto je skutečný kód Rustu.

1use wasm_bindgen::prelude::*;

Definice pro vytvoření balíčku WASM z Rustu. Jsou zdokumentovány zde (opens in a new tab).

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

Funkce, které potřebujeme z knihovny eth-stealth-addresses (opens in a new tab).

1use hex::{decode,encode};

Rust typicky používá pro hodnoty bajtová pole (opens in a new tab) ([u8; <size>]). Ale v JavaScriptu obvykle používáme hexadecimální řetězce. Knihovna hex (opens in a new tab) nám překládá z jedné reprezentace do druhé.

1#[wasm_bindgen]

Vygenerujte vazby WASM, aby bylo možné tuto funkci volat z JavaScriptu.

1pub fn wasm_generate_stealth_meta_address() -> String {

Nejjednodušší způsob, jak vrátit objekt s více poli, je vrátit řetězec JSON.

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

generate_stealth_meta_address (opens in a new tab) vrací tři pole:

  • Meta-adresa (Kpub a Vpub)
  • Zobrazovací privátní klíč (Vpriv)
  • Privátní klíč pro útratu (Kpriv)

Syntaxe n-tice (opens in a new tab) nám umožňuje tyto hodnoty opět oddělit.

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

Použijte makro format! (opens in a new tab) k vygenerování řetězce kódovaného ve formátu JSON. Použijte hex::encode (opens in a new tab) ke změně polí na hexadecimální řetězce.

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

Tato funkce převede hexadecimální řetězec (poskytnutý JavaScriptem) na pole bajtů. Používáme ji k analýze hodnot poskytnutých JavaScriptovým kódem. Tato funkce je komplikovaná kvůli tomu, jak Rust zpracovává pole a vektory.

Výraz <const N: usize> se nazývá generikum (opens in a new tab). N je parametr, který řídí délku vráceného pole. Funkce se ve skutečnosti volá jako str_to_array::<n>, kde n je délka pole.

Návratová hodnota je Option<[u8; N]>, což znamená, že vrácené pole je volitelné (opens in a new tab). Toto je typický vzor v Rustu pro funkce, které mohou selhat.

Například, pokud zavoláme str_to_array::10("bad060a7"), funkce má vrátit pole s deseti hodnotami, ale vstup má pouze čtyři bajty. Funkce musí selhat, a to tak, že vrátí None. Návratová hodnota pro str_to_array::4("bad060a7") by byla Some<[0xba, 0xd0, 0x60, 0xa7]>.

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

Funkce hex::decode (opens in a new tab) vrací Result<Vec<u8>, FromHexError>. Typ Result (opens in a new tab) může obsahovat buď úspěšný výsledek (Ok(value)), nebo chybu (Err(error)).

Metoda .ok() převede Result na Option, jehož hodnota je buď hodnota Ok() v případě úspěchu, nebo None v opačném případě. Nakonec operátor otazníku (opens in a new tab) přeruší aktuální funkci a vrátí None, pokud je Option prázdný. V opačném případě hodnotu rozbalí a vrátí ji (v tomto případě pro přiřazení hodnoty vec).

Vypadá to jako zvláštně spletitá metoda pro zpracování chyb, ale Result a Option zajišťují, že všechny chyby jsou tak či onak zpracovány.

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

Pokud je počet bajtů nesprávný, jedná se o selhání a vrátíme None.

1 // try_into spotřebuje vec a pokusí se vytvořit [u8; N]
2 let array: [u8; N] = vec.try_into().ok()?;

Rust má dva typy polí. Pole (opens in a new tab) mají pevnou velikost. Vektory (opens in a new tab) se mohou zvětšovat a zmenšovat. hex::decode vrací vektor, ale knihovna eth_stealth_addresses chce přijímat pole. .try_into() (opens in a new tab) převádí hodnotu na jiný typ, například vektor na pole.

1 Some(array)
2}

Rust nevyžaduje použití klíčového slova return (opens in a new tab) při vracení hodnoty na konci funkce.

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

Tato funkce přijímá veřejnou meta-adresu, která obsahuje jak Vpub, tak Kpub. Vrací skrytou adresu, veřejný klíč k publikování (Rpub) a jednobajtovou skenovací hodnotu, která urychluje identifikaci toho, které publikované adresy mohou patřit Alici.

Skenovací hodnota je součástí sdíleného tajemství (S = GRprivVpriv). Tato hodnota je k dispozici Alici a její kontrola je mnohem rychlejší než kontrola, zda se f(Kpub+G*haš(S)) rovná publikované adrese.

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

Používáme generate_stealth_address (opens in a new tab) z knihovny.

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

Připravte výstupní řetězec kódovaný ve formátu 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}
Zobrazit vše

Tato funkce používá compute_stealth_key (opens in a new tab) z knihovny k výpočtu privátního klíče pro výběr z adresy (Rpriv). Tento výpočet vyžaduje tyto hodnoty:

  • Adresa (Adresa=f(Ppub))
  • Veřejný klíč vygenerovaný Billem (Rpub)
  • Privátní klíč pro zobrazení (Vpriv)
  • Privátní klíč pro útratu (Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)] (opens in a new tab) určuje, že funkce se provede při inicializaci kódu WASM.

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

Tento kód určuje, že výstup paniky se pošle do konzole JavaScriptu. Abyste to viděli v akci, použijte aplikaci a dejte Billovi neplatnou meta-adresu (stačí změnit jednu hexadecimální číslici). V konzoli JavaScriptu uvidíte tuto chybu:

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

Následuje výpis zásobníku. Poté dejte Billovi platnou meta-adresu a Alici buď neplatnou adresu, nebo neplatný veřejný klíč. Zobrazí se tato chyba:

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:
2klíče negenerují skrytou adresu

Opět následuje výpis zásobníku.

Uživatelské rozhraní

Uživatelské rozhraní je napsáno pomocí Reactu (opens in a new tab) a obsluhováno pomocí Vite (opens in a new tab). Můžete se o nich dozvědět pomocí tohoto tutoriálu. Zde není potřeba WAGMI (opens in a new tab), protože neinteragujeme přímo s blockchainem ani s peněženkou.

Jedinou ne zcela zřejmou částí uživatelského rozhraní je připojení WASM. Funguje to takto.

vite.config.js

Tento soubor obsahuje konfiguraci 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})

Potřebujeme dva pluginy Vite: react (opens in a new tab) a wasm (opens in a new tab).

App.jsx

Tento soubor je hlavní komponentou aplikace. Je to kontejner, který obsahuje dvě komponenty: Alice a Bill, uživatelská rozhraní pro tyto uživatele. Relevantní částí pro WASM je inicializační kód.

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

Když používáme wasm-pack (opens in a new tab), vytvoří se dva soubory, které zde používáme: soubor wasm se skutečným kódem (zde src/rust-wasm/pkg/rust_wasm_bg.wasm) a soubor JavaScript s definicemi pro jeho použití (zde src/rust_wasm/pkg/rust_wasm.js). Výchozí export tohoto souboru JavaScript je kód, který se musí spustit pro inicializaci 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('Chyba při načítání wasm:', err)
12 alert("Wasm error: " + err)
13 }
14 }
15
16 loadWasm()
17 }, []
18 )
Zobrazit vše

Hook useEffect (opens in a new tab) umožňuje určit funkci, která se spustí při změně proměnných stavu. Zde je seznam proměnných stavu prázdný ([]), takže tato funkce se provede pouze jednou při načtení stránky.

Funkce efektu se musí okamžitě vrátit. Pro použití asynchronního kódu, jako je init WASM (který musí načíst soubor .wasm, a proto to trvá), definujeme interní funkci async (opens in a new tab) a spustíme ji bez await.

Bill.jsx

Toto je uživatelské rozhraní pro Billa. Má jednu akci, vytvoření adresy na základě skryté meta-adresy poskytnuté Alicí.

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

Kromě výchozího exportu exportuje JavaScriptový kód generovaný wasm-pack funkci pro každou funkci v kódu WASM.

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

Chcete-li volat funkce WASM, stačí zavolat funkci exportovanou souborem JavaScriptu vytvořeným wasm-pack.

Alice.jsx

Kód v Alice.jsx je analogický, s výjimkou toho, že Alice má dvě akce:

  • Vygenerovat meta-adresu
  • Získat privátní klíč pro adresu publikovanou Billem

Závěr

Skryté adresy nejsou všelék; musí se používat správně. Ale když se používají správně, mohou umožnit soukromí na veřejném blockchainu.

Více z mé práce najdete zde (opens in a new tab).

Stránka naposledy aktualizována: 14. listopadu 2025

Byl tento tutoriál užitečný?