Přejít na hlavní obsah

Používání skrytých adres

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

Jste Bill. Z důvodů, do kterých nebudeme zabíhat, chcete přispět na kampaň „Alice královnou světa“ a chcete, aby Alice věděla, že jste přispěli, aby vás mohla odměnit, pokud vyhraje. Její vítězství bohužel není zaručeno. Existuje konkurenční kampaň „Carol císařovnou 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) má řešení. Tento ERC vysvětluje, jak používat skryté adresy (opens in a new tab) pro anonymní převod.

Varování: Kryptografie, na které jsou skryté adresy založeny, je, pokud víme, spolehlivá. Existují však potenciální útoky postranními kanály (side-channel attacks). 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ím je, jak je používat. Tato část postačuje k pochopení zbytku článku. Následuje vysvětlení matematiky, která za tím stojí. Pokud se zajímáte o kryptografii, přečtěte si i tuto část.

Jednoduchá verze (jak používat skryté adresy)

Alice vytvoří dva soukromé klíče a zveřejní odpovídající veřejné klíče (které lze spojit do jedné meta-adresy s dvojnásobnou délkou). Bill také vytvoří soukromý klíč a zveřejní odpovídající veřejný klíč.

Pomocí veřejného klíče jedné strany a soukromého klíče druhé strany lze odvodit sdílené tajemství, které znají pouze Alice a Bill (nelze jej odvodit pouze z veřejných klíčů). Pomocí tohoto sdíleného tajemství Bill získá skrytou adresu a může na ni posílat aktiva.

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

Matematika (proč skryté adresy fungují takto)

Standardní skryté adresy používají kryptografii eliptických křivek (ECC) (opens in a new tab) k dosažení lepšího výkonu s menším počtem bitů klíče, přičemž si zachovávají stejnou úroveň zabezpečení. Většinou to ale můžeme ignorovat a předstírat, že používáme běžnou aritmetiku.

Existuje číslo, které všichni znají, G. Můžete jím násobit. Ale vzhledem k povaze ECC je prakticky nemožné jím dělit. Způsob, jakým kryptografie veřejného klíče v Ethereu obecně funguje, je ten, že můžete použít soukromý klíč, Ppriv, k podepisování transakcí, které jsou pak ověřeny veřejným klíčem, Ppub = GPpriv.

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

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

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

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

Máme samostatný klíč pro prohlížení, abychom Alici umožnili zadat subdodávku službám Daveovy kampaně za ovládnutí světa. Alice je ochotná nechat Davea znát veřejné adresy a informovat ji, když 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í oddělené klíče, může Alice dát Daveovi Vpriv. Pak může Dave vypočítat S = RpubVpriv = GRprivVpriv a tímto způsobem získat veřejné klíče (Ppub = Kpub+G*hash(S)). Ale bez Kpriv Dave nemůže získat soukromý klíč.

Shrnuto, toto jsou hodnoty, které znají různí účastníci.

AliceZveřejněnoBillDave
GGGG
Kpriv---
Vpriv--Vpriv
Kpub = GKprivKpubKpubKpub
Vpub = GVprivVpubVpubVpub
--Rpriv-
RpubRpubRpub = GRprivRpub
S = RpubVpriv = GRprivVpriv-S = RprivVpub = GRprivVprivS = 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)
Ppriv = Kpriv+hash(S)---

Když se skryté adresy pokazí

Na blockchainu neexistují žádná tajemství. Ačkoli vám skryté adresy mohou poskytnout soukromí, toto soukromí je náchylné k analýze provozu. Vezměme si triviální příklad: představte si, že Bill pošle prostředky na adresu a okamžitě odešle transakci, aby zveřejnil hodnotu Rpub. Bez Alicina Vpriv si nemůžeme být jisti, že se jedná o skrytou adresu, ale je to velmi pravděpodobné. Pak vidíme další transakci, která převede všechny ETH z této adresy na adresu fondu Aliciny kampaně. Možná to nedokážeme dokázat, ale je pravděpodobné, že Bill právě přispěl na Alicinu kampaň. 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ůznou dobu, z různých adres). To však nestačí. Vzor, který Carol hledá, je ten, že Bill pošle prostředky na adresu a fond Aliciny kampaně z ní pak vybírá.

Jedním z řešení je, aby Alicina kampaň nevybírala peníze přímo, ale použila je k zaplacení třetí straně. Pokud Alicina kampaň pošle 10 ETH Daveovým službám kampaně za ovládnutí světa, 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á jí konkuruje, nebo Adamovi, Albertovi či Abigail, o které se Carol nezajímá. Alice může k platbě připojit hashovanou hodnotu a poté poskytnout Daveovi předobraz (preimage), aby dokázala, že to byl její dar. Alternativně, jak bylo uvedeno výše, pokud Alice dá Daveovi svůj Vpriv, on už ví, od koho platba přišla.

Hlavním problémem tohoto řešení je, že vyžaduje, aby se Alice starala o utajení, když z tohoto utajení těží Bill. 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 pak se bude bát, co se stane, když Carol vyhraje. Bill by nakonec mohl Alici poskytnout ještě větší podporu.

Použití více skrytých vrstev

Místo toho, aby se Bill spoléhal na Alici, že zachová jeho soukromí, může to udělat sám. Může vygenerovat více meta-adres pro fiktivní osoby, Boba a Bellu. Bill pak 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 cestu Bill-Bob-Bella-Alice. Pokud však „Bob“ a „Bella“ používají ETH i k jiným účelům, nebude to vypadat, že Bill převedl něco Alici, i když Alice okamžitě vybere prostředky ze skryté adresy na svou známou adresu kampaně.

Psaní aplikace se skrytými adresami

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

Nástroje

Existuje knihovna pro skryté adresy v TypeScriptu (opens in a new tab), kterou bychom mohli použít. Kryptografické operace však mohou být náročné na procesor. Raději je implementuji v kompilovaném jazyce, jako je Rust (opens in a new tab), a používám WASM (opens in a new tab) ke spuštění kódu v prohlížeči.

Budeme používat Vite (opens in a new tab) a React (opens in a new tab). Jedná se o standardní oborové nástroje; pokud s nimi nejste obeznámeni, můžete použít tento tutoriál. K použití Vite potřebujeme Node.

Podívejte se na skryté adresy v akci

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

  2. Naklonujte repozitář z GitHubu.

    git clone https://github.com/qbzzt/251022-stealth-addresses.git
    cd 251022-stealth-addresses
    
  3. Nainstalujte prerekvizity a zkompilujte kód v Rustu.

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

    cd ../..
    npm install
    npm 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 Billovo. 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 (Vygenerovat skrytou meta-adresu). Zobrazí se nová skrytá adresa a odpovídající soukromé 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 (Vygenerovat adresu). Tím získáte adresu, na kterou můžete Alici poslat prostředky.

  8. Zkopírujte adresu a Billův veřejný klíč a vložte je do oblasti „Private key for address generated by Bill“ (Soukromý klíč pro adresu vygenerovanou Billem) v uživatelském rozhraní Alice. Jakmile jsou tato pole vyplněna, uvidíte soukromý 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 soukromý 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 ho vidět v src/rust_wasm/src/lib.rs (opens in a new tab). Tento kód je primárně rozhraním mezi kódem v JavaScriptu 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.

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

console_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ří (panic) a nemůže pokračovat.

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

Typ výstupu potřebný k vytvoření kódu WASM.

lib.rs

Toto je samotný kód v Rustu.

use wasm_bindgen::prelude::*;

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

use eth_stealth_addresses::{
    generate_stealth_meta_address,
    generate_stealth_address,
    compute_stealth_key
};

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

use hex::{decode,encode};

Rust pro hodnoty obvykle používá 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) za nás překládá z jedné reprezentace do druhé.

#[wasm_bindgen]

Vygenerování vazeb (bindings) WASM, abychom mohli tuto funkci volat z JavaScriptu.

pub fn wasm_generate_stealth_meta_address() -> String {

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

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

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

  • Meta-adresu (Kpub a Vpub)
  • Soukromý klíč pro prohlížení (Vpriv)
  • Soukromý klíč pro utrácení (Kpriv)

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

    format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
        encode(address),
        encode(view_private_key),
        encode(spend_private_key)
    )
}

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

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

Tato funkce mění hexadecimální řetězec (poskytnutý JavaScriptem) na bajtové pole. Používáme ji k parsování hodnot poskytnutých kódem v JavaScriptu. Tato funkce je složitá kvůli tomu, jak Rust zachází s poli 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 nazývá 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é (optional) (opens in a new tab). Toto je v Rustu typický vzor pro funkce, které mohou selhat.

Pokud například zavoláme str_to_array::10("bad060a7"), funkce by měla vrátit pole o deseti hodnotách, ale vstup má pouze čtyři bajty. Funkce musí selhat a učiní tak vrácením None. Návratová hodnota pro str_to_array::4("bad060a7") by byla Some<[0xba, 0xd0, 0x60, 0xa7]>.

    // decode vrací Result<Vec<u8>, _>
    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() změní Result na Option, jehož hodnota je buď hodnota Ok() v případě úspěchu, nebo None v případě neúspěchu. 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ě rozbalí (unwrap) hodnotu a vrátí ji (v tomto případě pro přiřazení hodnoty do vec).

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

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

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

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

Rust má dva typy polí. Pole (Arrays) (opens in a new tab) mají pevnou velikost. Vektory (Vectors) (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řevede hodnotu na jiný typ, například vektor na pole.

    Some(array)
}

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

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

Tato funkce přijímá veřejnou meta-adresu, která zahrnuje jak Vpub, tak Kpub. Vrací skrytou adresu, veřejný klíč ke zveřejnění (Rpub) a jednobajtovou hodnotu pro skenování, která urychluje identifikaci toho, které zveřejněné adresy mohou patřit Alici.

Hodnota pro skenování je součástí sdíleného tajemství (S = GRprivVpriv). Tato hodnota je dostupná Alici a její kontrola je mnohem rychlejší než kontrola, zda se f(Kpub+G*hash(S)) rovná zveřejněné adrese.

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

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

    format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
        encode(address),
        encode(r_pub),
        encode(&[scan])
    ).into()
}

Příprava výstupního řetězce kódovaného v JSON.

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

  • Adresu (Address=f(Ppub))
  • Veřejný klíč vygenerovaný Billem (Rpub)
  • Soukromý klíč pro prohlížení (Vpriv)
  • Soukromý klíč pro utrácení (Kpriv)
#[wasm_bindgen(start)]

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

pub fn main() {
    console_error_panic_hook::set_once();
}

Tento kód specifikuje, že výstup při panice (panic) se odešle do konzole JavaScriptu. Chcete-li to vidět 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:

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

Následováno trasováním zásobníku (stack trace). Poté dejte Billovi platnou meta-adresu a Alici dejte buď neplatnou adresu, nebo neplatný veřejný klíč. Uvidíte tuto chybu:

rust_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:
keys do not generate stealth address

Opět následováno trasováním zásobníku.

Uživatelské rozhraní

Uživatelské rozhraní je napsáno pomocí React (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. Není zde potřeba Wagmi (opens in a new tab), protože nekomunikujeme přímo s blockchainem ani peněženkou.

Jedinou ne zcela zřejmou částí uživatelského rozhraní je připojení WASM. Zde je návod, jak to funguje.

vite.config.js

Tento soubor obsahuje konfiguraci Vite (opens in a new tab).

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import wasm from "vite-plugin-wasm";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), wasm()],
})

Potřebujeme dva pluginy pro 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.

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

Když použijeme wasm-pack (opens in a new tab), vytvoří to dva soubory, které zde používáme: soubor wasm se samotným kódem (zde src/rust-wasm/pkg/rust_wasm_bg.wasm) a soubor JavaScriptu s definicemi pro jeho použití (zde src/rust_wasm/pkg/rust_wasm.js). Výchozí export (default export) tohoto souboru JavaScriptu je kód, který je třeba spustit k inicializaci WASM.

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

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

Bill.jsx

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

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

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

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

Pro volání funkcí WASM jednoduše zavoláme funkci exportovanou souborem JavaScriptu vytvořeným pomocí wasm-pack.

Alice.jsx

Kód v Alice.jsx je analogický, s tím rozdílem, že Alice má dvě akce:

  • Vygenerovat meta-adresu
  • Získat soukromý klíč pro adresu zveřejněnou Billem

Závěr

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

Zde najdete další mou práci (opens in a new tab).