Usare gli indirizzi stealth
Tu sei Bill. Per ragioni in cui non ci addentreremo, vuoi donare alla campagna "Alice Regina del Mondo" e fare in modo che Alice sappia che hai donato, così ti darà una ricompensa 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 effettuare un trasferimento di 200 ETH dal tuo account a quello di Alice.
ERC-5564 (si apre in una nuova scheda) ha la soluzione. Questo ERC spiega come usare gli indirizzi stealth (si apre in una nuova scheda) per un trasferimento anonimo.
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 di lunghezza doppia). 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 usano la crittografia a curva ellittica (ECC) (si apre in una nuova scheda) 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 della 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 transazioni che vengono poi verificate da una chiave pubblica, Ppub = GPpriv.
Alice crea due chiavi private, Kpriv e Vpriv. Kpriv sarà usata per spendere denaro dall'indirizzo stealth, e Vpriv per visualizzare gli indirizzi che appartengono ad Alice. Alice poi pubblica 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 che risulta 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 è disponibile più denaro, ma non vuole che lui spenda i soldi della sua campagna.
Poiché la visualizzazione e la spesa usano 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 noti ai diversi partecipanti.
| Alice | Pubblicato | 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) |
| Indirizzo=f(Ppub) | - | Indirizzo=f(Ppub) | Indirizzo=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 che lo sia. 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 pattern 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 il denaro direttamente, ma lo 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é in tal caso lui avrà paura di cosa succederà se Carol vince. Bill potrebbe finire per fornire ad Alice ancora più supporto.
Usare livelli stealth multipli
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 poi invia ETH a Bob, e "Bob" (che in realtà è Bill) li invia a Bella. "Bella" (sempre Bill) li invia ad Alice.
Carol può ancora fare 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 (si apre in una nuova scheda).
Strumenti
C'è una libreria TypeScript per indirizzi stealth (si apre in una nuova scheda) che potremmo usare. Tuttavia, le operazioni crittografiche possono essere intensive per la CPU. Preferisco implementarle in un linguaggio compilato, come Rust (si apre in una nuova scheda), e usare WASM (si apre in una nuova scheda) per eseguire il codice nel browser.
Useremo Vite (si apre in una nuova scheda) e React (si apre in una nuova scheda). 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
-
Installa gli strumenti necessari: Rust (si apre in una nuova scheda) e Node (si apre in una nuova scheda).
-
Clona il repository GitHub.
git clone https://github.com/qbzzt/251022-stealth-addresses.git cd 251022-stealth-addresses -
Installa i prerequisiti e compila il codice Rust.
cd src/rust-wasm rustup target add wasm32-unknown-unknown cargo install wasm-pack wasm-pack build --target web -
Avvia il server web.
cd ../.. npm install npm run dev -
Naviga verso l'applicazione (si apre in una nuova scheda). 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à.
-
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.
-
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.
-
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 quei campi, vedrai la chiave privata per accedere agli asset a quell'indirizzo.
-
Puoi usare un calcolatore online (si apre in una nuova scheda) per assicurarti che la chiave privata corrisponda all'indirizzo.
Come funziona il programma
Il componente WASM
Il codice sorgente che viene compilato in WASM è scritto in Rust (si apre in una nuova scheda). Puoi vederlo in src/rust_wasm/src/lib.rs (si apre in una nuova scheda). Questo codice è principalmente un'interfaccia tra il codice JavaScript e la libreria eth-stealth-addresses (si apre in una nuova scheda).
Cargo.toml
Cargo.toml (si apre in una nuova scheda) in Rust è analogo a package.json (si apre in una nuova scheda) in JavaScript. Contiene informazioni sul pacchetto, dichiarazioni delle dipendenze, ecc.
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2024"
[dependencies]
eth-stealth-addresses = "0.1.0"
hex = "0.4.3"
wasm-bindgen = "0.2.104"
getrandom = { version = "0.2", features = ["js"] }
Il pacchetto getrandom (si apre in una nuova scheda) 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.
console_error_panic_hook = "0.1.7"
Questa libreria (si apre in una nuova scheda) ci fornisce messaggi di errore più significativi quando il codice WASM va in panico e non può continuare.
[lib]
crate-type = ["cdylib", "rlib"]
Il tipo di output richiesto per produrre codice WASM.
lib.rs
Questo è il codice Rust vero e proprio.
use wasm_bindgen::prelude::*;
Le definizioni per creare un pacchetto WASM da Rust. Sono documentate qui (si apre in una nuova scheda).
use eth_stealth_addresses::{
generate_stealth_meta_address,
generate_stealth_address,
compute_stealth_key
};
Le funzioni di cui abbiamo bisogno dalla libreria eth-stealth-addresses (si apre in una nuova scheda).
use hex::{decode,encode};
Rust usa tipicamente array (si apre in una nuova scheda) di byte ([u8; <size>]) per i valori. Ma in JavaScript, usiamo tipicamente stringhe esadecimali. La libreria hex (si apre in una nuova scheda) traduce per noi da una rappresentazione all'altra.
#[wasm_bindgen]
Genera i binding WASM per poter chiamare questa funzione da JavaScript.
pub fn wasm_generate_stealth_meta_address() -> String {
Il modo più semplice per restituire un oggetto con più campi è restituire una stringa JSON.
let (address, spend_private_key, view_private_key) =
generate_stealth_meta_address();
La funzione generate_stealth_meta_address (si apre in una nuova scheda) 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 (si apre in una nuova scheda) ci permette di separare di nuovo quei valori.
format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
encode(address),
encode(view_private_key),
encode(spend_private_key)
)
}
Usa la macro format! (si apre in una nuova scheda) per generare la stringa codificata in JSON. Usa hex::encode (si apre in una nuova scheda) per cambiare gli array in stringhe esadecimali.
fn 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 (si apre in una nuova scheda). N è un parametro che controlla la lunghezza dell'array restituito. La funzione è in realtà chiamata 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 (si apre in una nuova scheda). 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]>.
// decode restituisce Result<Vec<u8>, _>
let vec = decode(s).ok()?;
La funzione hex::decode (si apre in una nuova scheda) restituisce un Result<Vec<u8>, FromHexError>. Il tipo Result (si apre in una nuova scheda) può contenere un risultato di successo (Ok(value)) o un errore (Err(error)).
Il metodo .ok() trasforma il Result in un Option, il cui valore è il valore Ok() in caso di successo o None in caso contrario. Infine, l'operatore punto interrogativo (si apre in una nuova scheda) 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.
if vec.len() != N { return None; }
Se il numero di byte non è corretto, si tratta di un fallimento e restituiamo None.
// try_into consuma vec e tenta di creare [u8; N]
let array: [u8; N] = vec.try_into().ok()?;
Rust ha due tipi di array. Gli array (si apre in una nuova scheda) hanno una dimensione fissa. I vettori (si apre in una nuova scheda) possono crescere e ridursi. hex::decode restituisce un vettore, ma la libreria eth_stealth_addresses vuole ricevere array. .try_into() (si apre in una nuova scheda) converte un valore in un altro tipo, ad esempio, un vettore in un array.
Some(array)
}
Rust non richiede di usare la parola chiave return (si apre in una nuova scheda) quando si restituisce un valore alla fine di una funzione.
#[wasm_bindgen]
pub 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.
let (address, r_pub, scan) =
generate_stealth_address(&str_to_array::<66>(stealth_address)?);
Usiamo la funzione generate_stealth_address (si apre in una nuova scheda) della libreria.
format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
encode(address),
encode(r_pub),
encode(&[scan])
).into()
}
Prepara la stringa di output codificata in JSON.
#[wasm_bindgen]
pub fn wasm_compute_stealth_key(
address: &str,
bill_pub_key: &str,
view_private_key: &str,
spend_private_key: &str
) -> Option<String> {
.
.
.
}
Questa funzione usa la funzione compute_stealth_key (si apre in una nuova scheda) della libreria per calcolare la chiave privata per prelevare dall'indirizzo (Rpriv). Questo calcolo richiede questi valori:
- L'indirizzo (Indirizzo=f(Ppub))
- La chiave pubblica generata da Bill (Rpub)
- La chiave privata di visualizzazione (Vpriv)
- La chiave privata di spesa (Kpriv)
#[wasm_bindgen(start)]
#[wasm_bindgen(start)] (si apre in una nuova scheda) specifica che la funzione viene eseguita quando il codice WASM viene inizializzato.
pub fn main() {
console_error_panic_hook::set_once();
}
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:
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
Seguito da una traccia 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:
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
Di nuovo, seguito da una traccia dello stack.
L'interfaccia utente
L'interfaccia utente è scritta usando React (si apre in una nuova scheda) ed è servita da Vite (si apre in una nuova scheda). Puoi imparare a conoscerli usando questo tutorial. Non c'è bisogno di Wagmi (si apre in una nuova scheda) 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 (si apre in una nuova scheda).
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()],
})
Abbiamo bisogno di due plugin Vite: react (si apre in una nuova scheda) e wasm (si apre in una nuova scheda).
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.
import init from './rust-wasm/pkg/rust_wasm.js'
Quando usiamo wasm-pack (si apre in una nuova scheda), 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.
function App() {
.
.
.
useEffect(() => {
const loadWasm = async () => {
try {
await init();
setWasmReady(true)
} catch (err) {
console.error('Error loading wasm:', err)
alert("Wasm error: " + err)
}
}
loadWasm()
}, []
)
L'hook useEffect (si apre in una nuova scheda) 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 il init di WASM (che deve caricare il file .wasm e quindi richiede tempo) definiamo una funzione interna async (si apre in una nuova scheda) 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.
import { 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.
<button onClick={() => {
setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))
}}>
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 (si apre in una nuova scheda).