Vai al contenuto principale

Usare gli indirizzi stealth

Indirizzo stealth
privacy
crittografia
Rust
wasm
Intermedio
Ori Pomerantz
30 novembre 2025
16 minuti di lettura

Sei Bill. Per ragioni in cui non entreremo, vuoi donare alla campagna "Alice Regina del Mondo" e far sapere ad Alice che hai donato, in modo che ti ricompensi se vince. Sfortunatamente, la sua vittoria non è garantita. C'è una campagna concorrente, "Carol Imperatrice del Sistema Solare". Se Carol vince e scopre che hai donato ad Alice, sarai nei guai. Quindi non puoi semplicemente trasferire 200 ETH dal tuo account a quello di Alice.

ERC-5564 (opens in a new tab) ha la soluzione. Questo ERC spiega come utilizzare gli indirizzi stealth (opens in a new tab) per i trasferimenti anonimi.

Attenzione: La crittografia alla base degli indirizzi stealth è, per quanto ne sappiamo, solida. Tuttavia, ci sono potenziali attacchi side-channel (a canale laterale). Di seguito, vedrai cosa puoi fare per ridurre questo rischio.

Come funzionano gli indirizzi stealth

Questo articolo cercherà di spiegare gli indirizzi stealth in due modi. Il primo è come usarli. Questa parte è sufficiente per comprendere il resto dell'articolo. Poi, c'è una spiegazione della matematica che c'è dietro. Se ti interessa la crittografia, leggi anche questa parte.

La versione semplice (come usare gli indirizzi stealth)

Alice crea due chiavi private e pubblica le chiavi pubbliche corrispondenti (che possono essere combinate in un singolo meta-indirizzo a doppia lunghezza). Anche Bill crea una chiave privata e pubblica la chiave pubblica corrispondente.

Usando la chiave pubblica di una parte e la chiave privata dell'altra, è possibile derivare un segreto condiviso noto solo ad Alice e Bill (non può essere derivato solo dalle chiavi pubbliche). Usando questo segreto condiviso, Bill ottiene l'indirizzo stealth e può inviarvi degli asset.

Anche Alice ottiene l'indirizzo dal segreto condiviso, ma poiché conosce le chiavi private delle chiavi pubbliche che ha pubblicato, può anche ottenere la chiave privata che le permette di prelevare da quell'indirizzo.

La matematica (perché gli indirizzi stealth funzionano così)

Gli indirizzi stealth standard utilizzano la crittografia a curva ellittica (ECC) (opens in a new tab) per ottenere prestazioni migliori con meno bit di chiave, pur mantenendo lo stesso livello di sicurezza. Ma per la maggior parte possiamo ignorarlo e fingere di usare l'aritmetica normale.

C'è un numero che tutti conoscono, G. Puoi moltiplicare per G. Ma a causa della natura dell'ECC, è praticamente impossibile dividere per G. Il modo in cui la crittografia a chiave pubblica funziona generalmente in Ethereum è che puoi usare una chiave privata, Ppriv, per firmare le transazioni che vengono poi verificate da una chiave pubblica, Ppub = GPpriv.

Alice crea due chiavi private, Kpriv e Vpriv. Kpriv verrà utilizzata per spendere denaro dall'indirizzo stealth, e Vpriv per visualizzare gli indirizzi che appartengono ad Alice. Alice pubblica quindi le chiavi pubbliche: Kpub = GKpriv e Vpub = GVpriv

Bill crea una terza chiave privata, Rpriv, e pubblica Rpub = GRpriv in un registro centrale (Bill avrebbe anche potuto inviarla ad Alice, ma supponiamo che Carol stia ascoltando).

Bill calcola RprivVpub = GRprivVpriv, che si aspetta che anche Alice conosca (spiegato di seguito). Questo valore è chiamato S, il segreto condiviso. Questo dà a Bill una chiave pubblica, Ppub = Kpub+G*hash(S). Da questa chiave pubblica, può calcolare un indirizzo e inviarvi tutte le risorse che desidera. In futuro, se Alice vince, Bill può dirle Rpriv per dimostrare che le risorse provenivano da lui.

Alice calcola RpubVpriv = GRprivVpriv. Questo le dà lo stesso segreto condiviso, S. Poiché conosce la chiave privata, Kpriv, può calcolare Ppriv = Kpriv+hash(S). Questa chiave le permette di accedere agli asset nell'indirizzo risultante da Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S).

Abbiamo una chiave di visualizzazione separata per consentire ad Alice di subappaltare ai Servizi per la Campagna di Dominio del Mondo di Dave. Alice è disposta a far conoscere a Dave gli indirizzi pubblici e a farsi informare quando c'è più denaro disponibile, ma non vuole che lui spenda i soldi della sua campagna.

Poiché la visualizzazione e la spesa utilizzano chiavi separate, Alice può dare a Dave Vpriv. Quindi Dave può calcolare S = RpubVpriv = GRprivVpriv e in questo modo ottenere le chiavi pubbliche (Ppub = Kpub+G*hash(S)). Ma senza Kpriv Dave non può ottenere la chiave privata.

Per riassumere, questi sono i valori conosciuti dai diversi partecipanti.

AlicePubblicatoBillDave
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)---

Quando gli indirizzi stealth falliscono

Non ci sono segreti sulla blockchain. Sebbene gli indirizzi stealth possano fornirti privacy, tale privacy è suscettibile all'analisi del traffico. Per fare un esempio banale, immagina che Bill finanzi un indirizzo e invii immediatamente una transazione per pubblicare un valore Rpub. Senza la Vpriv di Alice, non possiamo essere sicuri che si tratti di un indirizzo stealth, ma è molto probabile. Poi, vediamo un'altra transazione che trasferisce tutti gli ETH da quell'indirizzo all'indirizzo del fondo della campagna di Alice. Potremmo non essere in grado di dimostrarlo, ma è probabile che Bill abbia appena donato alla campagna di Alice. Carol lo penserebbe sicuramente.

È facile per Bill separare la pubblicazione di Rpub dal finanziamento dell'indirizzo stealth (farli in momenti diversi, da indirizzi diversi). Tuttavia, questo è insufficiente. Il modello che Carol cerca è che Bill finanzi un indirizzo, e poi il fondo della campagna di Alice prelevi da esso.

Una soluzione è che la campagna di Alice non prelevi i soldi direttamente, ma li usi per pagare una terza parte. Se la campagna di Alice invia 10 ETH ai Servizi per la Campagna di Dominio del Mondo di Dave, Carol sa solo che Bill ha donato a uno dei clienti di Dave. Se Dave ha abbastanza clienti, Carol non sarebbe in grado di sapere se Bill ha donato ad Alice, che compete con lei, o ad Adam, Albert o Abigail, di cui a Carol non importa nulla. Alice può includere un valore hash con il pagamento, e poi fornire a Dave la preimmagine, per dimostrare che si trattava della sua donazione. In alternativa, come notato sopra, se Alice dà a Dave la sua Vpriv, lui sa già da chi proveniva il pagamento.

Il problema principale di questa soluzione è che richiede ad Alice di preoccuparsi della segretezza quando tale segretezza va a vantaggio di Bill. Alice potrebbe voler mantenere la sua reputazione in modo che anche l'amico di Bill, Bob, le faccia una donazione. Ma è anche possibile che non le dispiaccia esporre Bill, perché allora lui avrà paura di cosa succederà se Carol vince. Bill potrebbe finire per fornire ad Alice ancora più supporto.

Usare più livelli stealth

Invece di fare affidamento su Alice per preservare la privacy di Bill, Bill può farlo da solo. Può generare più meta-indirizzi per persone fittizie, Bob e Bella. Bill invia quindi ETH a Bob, e "Bob" (che in realtà è Bill) li invia a Bella. "Bella" (sempre Bill) li invia ad Alice.

Carol può ancora fare l'analisi del traffico e vedere la pipeline Bill-a-Bob-a-Bella-ad-Alice. Tuttavia, se "Bob" e "Bella" usano ETH anche per altri scopi, non sembrerà che Bill abbia trasferito nulla ad Alice, anche se Alice preleva immediatamente dall'indirizzo stealth al suo indirizzo noto della campagna.

Scrivere un'applicazione per indirizzi stealth

Questo articolo spiega un'applicazione per indirizzi stealth disponibile su GitHub (opens in a new tab).

Strumenti

C'è una libreria typescript per indirizzi stealth (opens in a new tab) che potremmo usare. Tuttavia, le operazioni crittografiche possono essere intensive per la CPU. Preferisco implementarle in un linguaggio compilato, come Rust (opens in a new tab), e usare WASM (opens in a new tab) per eseguire il codice nel browser.

Useremo Vite (opens in a new tab) e React (opens in a new tab). Questi sono strumenti standard del settore; se non hai familiarità con essi, puoi usare questo tutorial. Per usare Vite, abbiamo bisogno di Node.

Vedere gli indirizzi stealth in azione

  1. Installa gli strumenti necessari: Rust (opens in a new tab) e Node (opens in a new tab).

  2. Clona il repository GitHub.

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
1
23. Installa i prerequisiti e compila il codice Rust.
3
4 ```sh
5 cd src/rust-wasm
6 rustup target add wasm32-unknown-unknown
7 cargo install wasm-pack
8 wasm-pack build --target web
  1. Avvia il server web.

    1cd ../..
    2npm install
    3npm run dev
1
25. Vai all'[applicazione](http://localhost:5173/). Questa pagina dell'applicazione ha due frame: uno per l'interfaccia utente di Alice e l'altro per quella di Bill. I due frame non comunicano; si trovano sulla stessa pagina solo per comodità.
3
46. Come Alice, fai clic su **Generate a Stealth Meta-Address** (Genera un meta-indirizzo stealth). Questo mostrerà il nuovo indirizzo stealth e le chiavi private corrispondenti. Copia il meta-indirizzo stealth negli appunti.
5
67. Come Bill, incolla il nuovo meta-indirizzo stealth e fai clic su **Generate an address** (Genera un indirizzo). Questo ti dà l'indirizzo da finanziare per Alice.
7
88. Copia l'indirizzo e la chiave pubblica di Bill e incollali nell'area "Private key for address generated by Bill" (Chiave privata per l'indirizzo generato da Bill) dell'interfaccia utente di Alice. Una volta compilati questi campi, vedrai la chiave privata per accedere agli asset a quell'indirizzo.
9
109. Puoi usare [un calcolatore online](https://iancoleman.net/ethereum-private-key-to-address/) per assicurarti che la chiave privata corrisponda all'indirizzo.
11
12### Come funziona il programma \{#how-the-program-works\}
13
14#### Il componente WASM \{#wasm\}
15
16Il codice sorgente che viene compilato in WASM è scritto in [Rust](https://rust-lang.org/). Puoi vederlo in [`src/rust_wasm/src/lib.rs`](https://github.com/qbzzt/251022-stealth-addresses/blob/main/src/rust-wasm/src/lib.rs). Questo codice è principalmente un'interfaccia tra il codice JavaScript e [la libreria `eth-stealth-addresses`](https://github.com/kassandraoftroy/eth-stealth-addresses).
17
18**`Cargo.toml`**
19
20[`Cargo.toml`](https://doc.rust-lang.org/cargo/reference/manifest.html) in Rust è analogo a [`package.json`](https://docs.npmjs.com/cli/v9/configuring-npm/package-json) in JavaScript. Contiene informazioni sul pacchetto, dichiarazioni delle dipendenze, ecc.
21
22```toml
23[package]
24name = "rust-wasm"
25version = "0.1.0"
26edition = "2024"
27
28[dependencies]
29eth-stealth-addresses = "0.1.0"
30hex = "0.4.3"
31wasm-bindgen = "0.2.104"
32getrandom = { version = "0.2", features = ["js"] }
Mostra tutto

Il pacchetto getrandom (opens in a new tab) ha bisogno di generare valori casuali. Questo non può essere fatto con mezzi puramente algoritmici; richiede l'accesso a un processo fisico come fonte di entropia. Questa definizione specifica che otterremo quell'entropia chiedendola al browser in cui siamo in esecuzione.

1console_error_panic_hook = "0.1.7"

Questa libreria (opens in a new tab) ci fornisce messaggi di errore più significativi quando il codice WASM va in panico e non può continuare.

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

Il tipo di output richiesto per produrre codice WASM.

lib.rs

Questo è il codice Rust vero e proprio.

1use wasm_bindgen::prelude::*;

Le definizioni per creare un pacchetto WASM da Rust. Sono documentate qui (opens in a new tab).

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

Le funzioni di cui abbiamo bisogno dalla libreria eth-stealth-addresses (opens in a new tab).

1use hex::{decode,encode};

Rust utilizza tipicamente array (opens in a new tab) di byte ([u8; <size>]) per i valori. Ma in JavaScript, utilizziamo tipicamente stringhe esadecimali. La libreria hex (opens in a new tab) traduce per noi da una rappresentazione all'altra.

1#[wasm_bindgen]

Genera i binding WASM per poter chiamare questa funzione da JavaScript.

1pub fn wasm_generate_stealth_meta_address() -> String {

Il modo più semplice per restituire un oggetto con più campi è restituire una stringa JSON.

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

La funzione generate_stealth_meta_address (opens in a new tab) restituisce tre campi:

  • Il meta-indirizzo (Kpub e Vpub)
  • La chiave privata di visualizzazione (Vpriv)
  • La chiave privata di spesa (Kpriv)

La sintassi della tupla (opens in a new tab) ci permette di separare nuovamente quei valori.

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

Usa la macro format! (opens in a new tab) per generare la stringa codificata in JSON. Usa hex::encode (opens in a new tab) per cambiare gli array in stringhe esadecimali.

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

Questa funzione trasforma una stringa esadecimale (fornita da JavaScript) in un array di byte. La usiamo per analizzare i valori forniti dal codice JavaScript. Questa funzione è complicata a causa di come Rust gestisce array e vettori.

L'espressione <const N: usize> è chiamata generico (opens in a new tab). N è un parametro che controlla la lunghezza dell'array restituito. La funzione si chiama in realtà str_to_array::<n>, dove n è la lunghezza dell'array.

Il valore di ritorno è Option<[u8; N]>, il che significa che l'array restituito è opzionale (opens in a new tab). Questo è un pattern tipico in Rust per le funzioni che potrebbero fallire.

Ad esempio, se chiamiamo str_to_array::10("bad060a7"), la funzione dovrebbe restituire un array di dieci valori, ma l'input è di soli quattro byte. La funzione deve fallire, e lo fa restituendo None. Il valore di ritorno per str_to_array::4("bad060a7") sarebbe Some<[0xba, 0xd0, 0x60, 0xa7]>.

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

La funzione hex::decode (opens in a new tab) restituisce un Result<Vec<u8>, FromHexError>. Il tipo Result (opens in a new tab) può contenere un risultato positivo (Ok(value)) o un errore (Err(error)).

Il metodo .ok() trasforma il Result in un Option, il cui valore è il valore Ok() se ha successo o None in caso contrario. Infine, l'operatore punto interrogativo (opens in a new tab) interrompe le funzioni correnti e restituisce un None se l'Option è vuoto. Altrimenti, estrae il valore e lo restituisce (in questo caso, per assegnare un valore a vec).

Questo sembra un metodo stranamente contorto per gestire gli errori, ma Result e Option assicurano che tutti gli errori vengano gestiti, in un modo o nell'altro.

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

Se il numero di byte non è corretto, si tratta di un fallimento e restituiamo None.

1 // try_into consuma vec e tenta di creare [u8; N]
2 let array: [u8; N] = vec.try_into().ok()?;

Rust ha due tipi di array. Gli array (opens in a new tab) hanno una dimensione fissa. I vettori (opens in a new tab) possono crescere e ridursi. hex::decode restituisce un vettore, ma la libreria eth_stealth_addresses vuole ricevere array. .try_into() (opens in a new tab) converte un valore in un altro tipo, ad esempio, un vettore in un array.

1 Some(array)
2}

Rust non richiede l'uso della parola chiave return (opens in a new tab) quando si restituisce un valore alla fine di una funzione.

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

Questa funzione riceve un meta-indirizzo pubblico, che include sia Vpub che Kpub. Restituisce l'indirizzo stealth, la chiave pubblica da pubblicare (Rpub) e un valore di scansione di un byte che accelera l'identificazione di quali indirizzi pubblicati potrebbero appartenere ad Alice.

Il valore di scansione fa parte del segreto condiviso (S = GRprivVpriv). Questo valore è disponibile per Alice, e controllarlo è molto più veloce che controllare se f(Kpub+G*hash(S)) è uguale all'indirizzo pubblicato.

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

Usiamo la funzione generate_stealth_address (opens in a new tab) della libreria.

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

Prepara la stringa di output codificata in 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}
Mostra tutto

Questa funzione usa la funzione compute_stealth_key (opens in a new tab) della libreria per calcolare la chiave privata per prelevare dall'indirizzo (Rpriv). Questo calcolo richiede questi valori:

  • L'indirizzo (Address=f(Ppub))
  • La chiave pubblica generata da Bill (Rpub)
  • La chiave privata di visualizzazione (Vpriv)
  • La chiave privata di spesa (Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)] (opens in a new tab) specifica che la funzione viene eseguita quando il codice WASM viene inizializzato.

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

Questo codice specifica che l'output di panico venga inviato alla console JavaScript. Per vederlo in azione, usa l'applicazione e dai a Bill un meta-indirizzo non valido (basta cambiare una cifra esadecimale). Vedrai questo errore nella console 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

Seguito da un'analisi dello stack (stack trace). Quindi dai a Bill il meta-indirizzo valido e dai ad Alice un indirizzo non valido o una chiave pubblica non valida. Vedrai questo errore:

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

Di nuovo, seguito da un'analisi dello stack.

L'interfaccia utente

L'interfaccia utente è scritta usando React (opens in a new tab) e servita da Vite (opens in a new tab). Puoi imparare a conoscerli usando questo tutorial. Non c'è bisogno di WAGMI (opens in a new tab) qui perché non interagiamo direttamente con una blockchain o un portafoglio.

L'unica parte non ovvia dell'interfaccia utente è la connettività WASM. Ecco come funziona.

vite.config.js

Questo file contiene la configurazione di 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})

Abbiamo bisogno di due plugin Vite: react (opens in a new tab) e wasm (opens in a new tab).

App.jsx

Questo file è il componente principale dell'applicazione. È un contenitore che include due componenti: Alice e Bill, le interfacce utente per quegli utenti. La parte rilevante per WASM è il codice di inizializzazione.

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

Quando usiamo wasm-pack (opens in a new tab), crea due file che usiamo qui: un file wasm con il codice vero e proprio (qui, src/rust-wasm/pkg/rust_wasm_bg.wasm) e un file JavaScript con le definizioni per usarlo (qui, src/rust_wasm/pkg/rust_wasm.js). L'esportazione predefinita di quel file JavaScript è il codice che deve essere eseguito per avviare 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 )
Mostra tutto

L'hook useEffect (opens in a new tab) ti permette di specificare una funzione che viene eseguita quando le variabili di stato cambiano. Qui, l'elenco delle variabili di stato è vuoto ([]), quindi questa funzione viene eseguita solo una volta al caricamento della pagina.

La funzione dell'effetto deve restituire immediatamente. Per usare codice asincrono, come l'init di WASM (che deve caricare il file .wasm e quindi richiede tempo) definiamo una funzione async (opens in a new tab) interna e la eseguiamo senza un await.

Bill.jsx

Questa è l'interfaccia utente per Bill. Ha una singola azione, creare un indirizzo basato sul meta-indirizzo stealth fornito da Alice.

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

Oltre all'esportazione predefinita, il codice JavaScript generato da wasm-pack esporta una funzione per ogni funzione nel codice WASM.

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

Per chiamare le funzioni WASM, chiamiamo semplicemente la funzione esportata dal file JavaScript creato da wasm-pack.

Alice.jsx

Il codice in Alice.jsx è analogo, tranne per il fatto che Alice ha due azioni:

  • Generare un meta-indirizzo
  • Ottenere la chiave privata per un indirizzo pubblicato da Bill

Conclusione

Gli indirizzi stealth non sono una panacea; devono essere usati correttamente. Ma se usati correttamente, possono abilitare la privacy su una blockchain pubblica.

Vedi qui per altri miei lavori (opens in a new tab).

Ultimo aggiornamento della pagina: 14 novembre 2025

Questo tutorial è stato utile?