Passer au contenu principal

Utilisation des adresses furtives

Adresse furtive
confidentialité
cryptographie
Rust
wasm
Intermédiaire
Ori Pomerantz
30 novembre 2025
17 minutes de lecture

Vous êtes Bill. Pour des raisons que nous n'aborderons pas, vous voulez faire un don à la campagne "Alice pour la Reine du Monde" et que Alice sache que vous avez fait un don afin qu'elle vous récompense si elle gagne. Malheureusement, sa victoire n'est pas garantie. Il existe une campagne concurrente, "Carol pour l'Impératrice du Système solaire". Si Carol gagne et qu'elle découvre que vous avez fait un don à Alice, vous aurez des ennuis. Vous ne pouvez donc pas simplement transférer 200 ETH de votre compte à celui d'Alice.

ERC-5564 (opens in a new tab) apporte la solution. Cet ERC explique comment utiliser les adresses furtives (opens in a new tab) pour un transfert anonyme.

Avertissement : La cryptographie derrière les adresses furtives est, à notre connaissance, solide. Cependant, il existe des attaques potentielles par canal auxiliaire. Ci-dessous, vous verrez ce que vous pouvez faire pour réduire ce risque.

Comment fonctionnent les adresses furtives

Cet article tentera d'expliquer les adresses furtives de deux manières. La première est comment les utiliser. Cette partie est suffisante pour comprendre le reste de l'article. Ensuite, il y a une explication des mathématiques sous-jacentes. Si la cryptographie vous intéresse, lisez également cette partie.

La version simple (comment utiliser les adresses furtives)

Alice crée deux clés privées et publie les clés publiques correspondantes (qui peuvent être combinées en une seule méta-adresse de double longueur). Bill crée également une clé privée et publie la clé publique correspondante.

En utilisant la clé publique d'une partie et la clé privée de l'autre, vous pouvez dériver un secret partagé connu uniquement d'Alice et de Bill (il ne peut pas être dérivé des seules clés publiques). À l'aide de ce secret partagé, Bill obtient l'adresse furtive et peut y envoyer des actifs.

Alice obtient également l'adresse à partir du secret partagé, mais comme elle connaît les clés privées des clés publiques qu'elle a publiées, elle peut également obtenir la clé privée qui lui permet de retirer des fonds de cette adresse.

Les mathématiques (pourquoi les adresses furtives fonctionnent de cette manière)

Les adresses furtives standard utilisent la cryptographie sur les courbes elliptiques (ECC) (opens in a new tab) pour obtenir de meilleures performances avec moins de bits de clé, tout en conservant le même niveau de sécurité. Mais pour la plupart, nous pouvons ignorer cela et prétendre que nous utilisons l'arithmétique ordinaire.

Il y a un nombre que tout le monde connaît, G. Vous pouvez multiplier par G. Mais en raison de la nature de l'ECC, il est pratiquement impossible de diviser par G. La façon dont la cryptographie à clé publique fonctionne généralement dans Ethereum est que vous pouvez utiliser une clé privée, Ppriv, pour signer des transactions qui sont ensuite vérifiées par une clé publique, Ppub = GPpriv.

Alice crée deux clés privées, Kpriv et Vpriv. Kpriv sera utilisée pour dépenser l'argent de l'adresse furtive, et Vpriv pour voir les adresses qui appartiennent à Alice. Alice publie ensuite les clés publiques : Kpub = GKpriv et Vpub = GVpriv

Bill crée une troisième clé privée, Rpriv, et publie Rpub = GRpriv dans un registre central (Bill aurait pu aussi l'envoyer à Alice, mais nous supposons que Carol écoute).

Bill calcule RprivVpub = GRprivVpriv, ce qu'il s'attend à ce qu'Alice sache aussi (expliqué ci-dessous). Cette valeur est appelée S, le secret partagé. Ceci donne à Bill une clé publique, Ppub = Kpub+G*hachage(S). À partir de cette clé publique, il peut calculer une adresse et y envoyer toutes les ressources qu'il souhaite. À l'avenir, si Alice gagne, Bill peut lui communiquer Rpriv pour prouver que les ressources proviennent de lui.

Alice calcule RpubVpriv = GRprivVpriv. Ceci lui donne le même secret partagé, S. Comme elle connaît la clé privée, Kpriv, elle peut calculer Ppriv = Kpriv+hachage(S). Cette clé lui permet d'accéder aux actifs dans l'adresse qui résulte de Ppub = GPpriv = GKpriv+Ghachage(S) = Kpub+Ghachage(S).

Nous avons une clé de visualisation distincte pour permettre à Alice de sous-traiter aux services de campagne de domination mondiale de Dave. Alice est prête à faire connaître à Dave les adresses publiques et à l'informer lorsque plus d'argent est disponible, mais elle ne veut pas qu'il dépense l'argent de sa campagne.

Parce que la visualisation et la dépense utilisent des clés distinctes, Alice peut donner à Dave Vpriv. Alors Dave peut calculer S = RpubVpriv = GRprivVpriv et de cette façon obtenir les clés publiques (Ppub = Kpub+G*hachage(S)). Mais sans Kpriv, Dave ne peut pas obtenir la clé privée.

Pour résumer, voici les valeurs connues des différents participants.

AlicePubliéBillDave
GGGG
Kpriv
VprivVpriv
Kpub = GKprivKpubKpubKpub
Vpub = GVprivVpubVpubVpub
Rpriv
RpubRpubRpub = GRprivRpub
S = RpubVpriv = GRprivVprivS = RprivVpub = GRprivVprivS = RpubVpriv = GRprivVpriv
Ppub = Kpub+G*hachage(S)Ppub = Kpub+G*hachage(S)Ppub = Kpub+G*hachage(S)
Adresse=f(Ppub)Adresse=f(Ppub)Adresse=f(Ppub)Adresse=f(Ppub)
Ppriv = Kpriv+hachage(S)

Quand les adresses furtives tournent mal

Il n'y a pas de secrets sur la blockchain. Bien que les adresses furtives puissent vous offrir une certaine confidentialité, cette confidentialité est sensible à l'analyse du trafic. Pour prendre un exemple trivial, imaginez que Bill finance une adresse et envoie immédiatement une transaction pour publier une valeur Rpub. Sans le Vpriv d'Alice, nous ne pouvons pas être sûrs qu'il s'agit d'une adresse furtive, mais c'est le pari à faire. Ensuite, nous voyons une autre transaction qui transfère tous les ETH de cette adresse vers l'adresse du fonds de campagne d'Alice. Nous ne pourrons peut-être pas le prouver, mais il est probable que Bill vient de faire un don à la campagne d'Alice. Carol le penserait certainement.

Il est facile pour Bill de séparer la publication de Rpub du financement de l'adresse furtive (en les faisant à des moments différents, à partir d'adresses différentes). Cependant, cela est insuffisant. Le schéma que Carol recherche est que Bill finance une adresse, et qu'ensuite le fonds de campagne d'Alice retire des fonds de celle-ci.

Une solution consiste à ce que la campagne d'Alice ne retire pas l'argent directement, mais l'utilise pour payer un tiers. Si la campagne d'Alice envoie 10 ETH aux services de campagne de domination mondiale de Dave, Carol sait seulement que Bill a fait un don à l'un des clients de Dave. Si Dave a suffisamment de clients, Carol ne serait pas en mesure de savoir si Bill a fait un don à Alice, qui est sa concurrente, ou à Adam, Albert ou Abigail dont Carol se moque. Alice peut inclure une valeur de hachage avec le paiement, puis fournir à Dave la pré-image, pour prouver que c'était bien son don. Alternativement, comme indiqué ci-dessus, si Alice donne à Dave son Vpriv, il sait déjà de qui provient le paiement.

Le principal problème de cette solution est qu'elle exige qu'Alice se soucie du secret alors que ce secret profite à Bill. Alice peut vouloir maintenir sa réputation afin que l'ami de Bill, Bob, fasse également un don. Mais il est aussi possible qu'elle n'hésite pas à dénoncer Bill, car il aura alors peur de ce qui arrivera si Carol gagne. Bill pourrait finir par apporter encore plus de soutien à Alice.

Utilisation de plusieurs couches furtives

Au lieu de compter sur Alice pour préserver la vie privée de Bill, Bill peut le faire lui-même. Il peut générer plusieurs méta-adresses pour des personnages de fiction, Bob et Bella. Bill envoie ensuite des ETH à Bob, et « Bob » (qui est en fait Bill) les envoie à Bella. « Bella » (également Bill) les envoie à Alice.

Carol peut toujours faire une analyse du trafic et voir le pipeline Bill-Bob-Bella-Alice. Cependant, si « Bob » et « Bella » utilisent également des ETH à d'autres fins, il n'apparaîtra pas que Bill ait transféré quoi que ce soit à Alice, même si Alice retire immédiatement de l'adresse furtive vers l'adresse connue de sa campagne.

Écrire une application d'adresse furtive

Cet article explique une application d'adresse furtive disponible sur GitHub (opens in a new tab).

Outils

Il existe une bibliothèque d'adresses furtives typescript (opens in a new tab) que nous pourrions utiliser. Cependant, les opérations cryptographiques peuvent être gourmandes en ressources de l'unité centrale. Je préfère les implémenter dans un langage compilé, tel que Rust (opens in a new tab), et utiliser WASM (opens in a new tab) pour exécuter le code dans le navigateur.

Nous allons utiliser Vite (opens in a new tab) et React (opens in a new tab). Ce sont des outils standard de l'industrie ; si vous ne les connaissez pas, vous pouvez utiliser ce tutoriel. Pour utiliser Vite, nous avons besoin de Node.

Voir les adresses furtives en action

  1. Installez les outils nécessaires : Rust (opens in a new tab) et Node (opens in a new tab).

  2. Clonez le dépôt GitHub.

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
  3. Installez les prérequis et compilez le code Rust.

    1cd src/rust-wasm
    2rustup target add wasm32-unknown-unknown
    3cargo install wasm-pack
    4wasm-pack build --target web
  4. Démarrez le serveur web.

    1cd ../..
    2npm install
    3npm run dev
  5. Accédez à l'application (opens in a new tab). Cette page d'application comporte deux cadres : l'un pour l'interface utilisateur d'Alice et l'autre pour celle de Bill. Les deux cadres ne communiquent pas ; ils ne sont sur la même page que pour des raisons de commodité.

  6. En tant qu'Alice, cliquez sur Generate a Stealth Meta-Address. Cela affichera la nouvelle adresse furtive et les clés privées correspondantes. Copiez la méta-adresse furtive dans le presse-papiers.

  7. En tant que Bill, collez la nouvelle méta-adresse furtive et cliquez sur Generate an address. Cela vous donne l'adresse à financer pour Alice.

  8. Copiez l'adresse et la clé publique de Bill et collez-les dans la zone "Clé privée pour l'adresse générée par Bill" de l'interface utilisateur d'Alice. Une fois ces champs remplis, vous verrez la clé privée pour accéder aux actifs à cette adresse.

  9. Vous pouvez utiliser un calculateur en ligne (opens in a new tab) pour vous assurer que la clé privée correspond à l'adresse.

Comment le programme fonctionne

Le composant WASM

Le code source qui se compile en WASM est écrit en Rust (opens in a new tab). Vous pouvez le voir dans src/rust_wasm/src/lib.rs (opens in a new tab). Ce code est principalement une interface entre le code JavaScript et la bibliothèque eth-stealth-addresses (opens in a new tab).

Cargo.toml

Cargo.toml (opens in a new tab) en Rust est analogue à package.json (opens in a new tab) en JavaScript. Il contient les informations sur le paquet, les déclarations de dépendance, etc.

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"] }
Afficher tout

Le paquet getrandom (opens in a new tab) doit générer des valeurs aléatoires. Cela ne peut pas être fait par des moyens purement algorithmiques ; cela nécessite l'accès à un processus physique comme source d'entropie. Cette définition spécifie que nous obtiendrons cette entropie en interrogeant le navigateur dans lequel nous nous exécutons.

1console_error_panic_hook = "0.1.7"

Cette bibliothèque (opens in a new tab) nous donne des messages d'erreur plus significatifs lorsque le code WASM panique et ne peut pas continuer.

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

Le type de sortie requis pour produire du code WASM.

lib.rs

Ceci est le vrai code Rust.

1use wasm_bindgen::prelude::*;

Les définitions pour créer un paquet WASM à partir de Rust. Elles sont documentées ici (opens in a new tab).

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

Les fonctions dont nous avons besoin de la bibliothèque eth-stealth-addresses (opens in a new tab).

1use hex::{decode,encode};

Rust utilise généralement des tableaux (opens in a new tab) d'octets ([u8; <taille>]) pour les valeurs. Mais en JavaScript, nous utilisons généralement des chaînes de caractères hexadécimales. La bibliothèque hex (opens in a new tab) traduit pour nous d'une représentation à l'autre.

1#[wasm_bindgen]

Générer des liaisons WASM pour pouvoir appeler cette fonction depuis JavaScript.

1pub fn wasm_generate_stealth_meta_address() -> String {

La manière la plus simple de renvoyer un objet avec plusieurs champs est de renvoyer une chaîne JSON.

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

La generate_stealth_meta_address (opens in a new tab) renvoie trois champs :

  • La méta-adresse (Kpub et Vpub)
  • La clé privée de visualisation (Vpriv)
  • La clé privée de dépense (Kpriv)

La syntaxe tuple (opens in a new tab) nous permet de séparer à nouveau ces valeurs.

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

Utilisez la macro format! (opens in a new tab) pour générer la chaîne codée en JSON. Utilisez hex::encode (opens in a new tab) pour transformer les tableaux en chaînes hexadécimales.

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

Cette fonction transforme une chaîne hexadécimale (fournie par JavaScript) en un tableau d'octets. Nous l'utilisons pour analyser les valeurs fournies par le code JavaScript. Cette fonction est compliquée en raison de la façon dont Rust gère les tableaux et les vecteurs.

L'expression <const N: usize> est appelée une générique (opens in a new tab). N est un paramètre qui contrôle la longueur du tableau retourné. La fonction est en fait appelée str_to_array::<n>, où n est la longueur du tableau.

La valeur de retour est Option<[u8; N]>, ce qui signifie que le tableau retourné est facultatif (opens in a new tab). C'est un modèle typique en Rust pour les fonctions qui peuvent échouer.

Par exemple, si nous appelons str_to_array::10("bad060a7"), la fonction est censée renvoyer un tableau de dix valeurs, mais l'entrée n'est que de quatre octets. La fonction doit échouer, et elle le fait en renvoyant None. La valeur de retour pour str_to_array::4("bad060a7") serait Some<[0xba, 0xd0, 0x60, 0xa7]>.

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

La fonction hex::decode (opens in a new tab) renvoie un Result<Vec<u8>, FromHexError>. Le type Result (opens in a new tab) peut contenir soit un résultat réussi (Ok(valeur)) soit une erreur (Err(erreur)).

La méthode .ok() transforme le Result en une Option, dont la valeur est soit la valeur Ok() en cas de succès, soit None dans le cas contraire. Enfin, l'opérateur point d'interrogation (opens in a new tab) interrompt les fonctions actuelles et renvoie un None si l'Option est vide. Sinon, il déballe la valeur et la renvoie (dans ce cas, pour assigner une valeur à vec).

Cela ressemble à une méthode étrangement alambiquée pour gérer les erreurs, mais Result et Option garantissent que toutes les erreurs sont gérées, d'une manière ou d'une autre.

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

Si le nombre d'octets est incorrect, c'est un échec, et nous retournons None.

1 // try_into consomme vec et tente de créer [u8; N]
2 let array: [u8; N] = vec.try_into().ok()?;

Rust a deux types de tableaux. Les tableaux (opens in a new tab) ont une taille fixe. Les vecteurs (opens in a new tab) peuvent s'agrandir et se réduire. hex::decode renvoie un vecteur, mais la bibliothèque eth_stealth_addresses veut recevoir des tableaux. .try_into() (opens in a new tab) convertit une valeur en un autre type, par exemple, un vecteur en un tableau.

1 Some(array)
2}

Rust ne vous oblige pas à utiliser le mot-clé return (opens in a new tab) lorsque vous retournez une valeur à la fin d'une fonction.

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

Cette fonction reçoit une méta-adresse publique, qui inclut à la fois Vpub et Kpub. Elle renvoie l'adresse furtive, la clé publique à publier (Rpub), et une valeur d'analyse d'un octet qui accélère l'identification des adresses publiées qui peuvent appartenir à Alice.

La valeur de l'analyse fait partie du secret partagé (S = GRprivVpriv). Cette valeur est disponible pour Alice, et sa vérification est beaucoup plus rapide que de vérifier si f(Kpub+G*hachage(S)) est égal à l'adresse publiée.

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

Nous utilisons la fonction generate_stealth_address (opens in a new tab) de la bibliothèque.

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

Préparer la chaîne de sortie codée en 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}
Afficher tout

Cette fonction utilise la fonction compute_stealth_key (opens in a new tab) de la bibliothèque pour calculer la clé privée permettant de retirer des fonds de l'adresse (Rpriv). Ce calcul nécessite ces valeurs :

  • L'adresse (Adresse=f(Ppub))
  • La clé publique générée par Bill (Rpub)
  • La clé privée de visualisation (Vpriv)
  • La clé privée de dépense (Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)] (opens in a new tab) spécifie que la fonction est exécutée lorsque le code WASM est initialisé.

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

Ce code spécifie que la sortie de la panique soit envoyée à la console JavaScript. Pour le voir en action, utilisez l'application et donnez à Bill une méta-adresse invalide (il suffit de changer un chiffre hexadécimal). Vous verrez cette erreur dans la 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

Suivi d'une trace de la pile d'exécution. Donnez ensuite à Bill la méta-adresse valide, et à Alice une adresse ou une clé publique invalide. Vous verrez cette erreur :

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

Encore une fois, suivi d'une trace de pile.

L'interface utilisateur

L'interface utilisateur est écrite en utilisant React (opens in a new tab) et servie par Vite (opens in a new tab). Vous pouvez en apprendre davantage sur eux en utilisant ce tutoriel. Il n'y a pas besoin de WAGMI (opens in a new tab) ici car nous n'interagissons pas directement avec une blockchain ou un portefeuille.

La seule partie non évidente de l'interface utilisateur est la connectivité WASM. Voici comment ça marche.

vite.config.js

Ce fichier contient la configuration de 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})

Nous avons besoin de deux plugins Vite : react (opens in a new tab) et wasm (opens in a new tab).

App.jsx

Ce fichier est le composant principal de l'application. C'est un conteneur qui inclut deux composants : Alice et Bill, les interfaces utilisateur pour ces utilisateurs. La partie pertinente pour WASM est le code d'initialisation.

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

Lorsque nous utilisons wasm-pack (opens in a new tab), il crée deux fichiers que nous utilisons ici : un fichier wasm avec le code réel (ici, src/rust-wasm/pkg/rust_wasm_bg.wasm) et un fichier JavaScript avec les définitions pour l'utiliser (ici, src/rust_wasm/pkg/rust_wasm.js). L'exportation par défaut de ce fichier JavaScript est le code qui doit être exécuté pour initialiser 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 )
Afficher tout

Le hook useEffect (opens in a new tab) vous permet de spécifier une fonction qui s'exécute lorsque les variables d'état changent. Ici, la liste des variables d'état est vide ([]), donc cette fonction n'est exécutée qu'une seule fois lorsque la page se charge.

La fonction d'effet doit retourner immédiatement. Pour utiliser du code asynchrone, tel que le init de WASM (qui doit charger le fichier .wasm et prend donc du temps), nous définissons une fonction interne async (opens in a new tab) et l'exécutons sans await.

Bill.jsx

C'est l'interface utilisateur pour Bill. Elle a une seule action, créer une adresse basée sur la méta-adresse furtive fournie par Alice.

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

En plus de l'exportation par défaut, le code JavaScript généré par wasm-pack exporte une fonction pour chaque fonction dans le code WASM.

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

Pour appeler les fonctions WASM, il suffit d'appeler la fonction exportée par le fichier JavaScript créé par wasm-pack.

Alice.jsx

Le code dans Alice.jsx est analogue, sauf qu'Alice a deux actions :

  • Générer une méta-adresse
  • Obtenir la clé privée pour une adresse publiée par Bill

Conclusion

Les adresses furtives ne sont pas la panacée ; elles doivent être utilisées correctement. Mais lorsqu'elles sont utilisées correctement, elles peuvent permettre la confidentialité sur une blockchain publique.

Voir ici pour plus de mon travail (opens in a new tab).

Dernière mise à jour de la page : 14 novembre 2025

Ce tutoriel vous a été utile ?