Ir al contenido principal

Uso de direcciones sigilosas

Dirección sigilosa
privacidad
criptografía
rust
wasm
Intermedio
Ori Pomerantz
30 de noviembre de 2025
16 minuto leído

Usted es Bill. Por razones que no detallaremos, usted quiere donar a la campaña «Alice para Reina del Mundo» y que Alice sepa que donó para que lo recompense si gana. Lamentablemente, su victoria no está garantizada. Existe una campaña rival, «Carol para Emperatriz del Sistema Solar». Si Carol gana y descubre que usted donó a Alice, estará en problemas. Así que no puede simplemente transferir 200 ETH de su cuenta a la de Alice.

ERC-5564opens in a new tab tiene la solución. Este ERC explica cómo usar direcciones sigilosasopens in a new tab para transferencias anónimas.

Advertencia: La criptografía detrás de las direcciones sigilosas es, hasta donde sabemos, sólida. Sin embargo, existen posibles ataques de canal lateral. A continuación, verá lo que puede hacer para reducir este riesgo.

Cómo funcionan las direcciones sigilosas

Este artículo intentará explicar las direcciones sigilosas de dos maneras. La primera es cómo usarlas. Esta parte es suficiente para comprender el resto del artículo. Luego, hay una explicación de las matemáticas detrás de esto. Si le interesa la criptografía, lea también esta parte.

La versión simple (cómo usar direcciones sigilosas)

Alice crea dos claves privadas y publica las claves públicas correspondientes (que se pueden combinar en una única metadirección de doble longitud). Bill también crea una clave privada y publica la clave pública correspondiente.

Usando la clave pública de una de las partes y la clave privada de la otra, se puede derivar un secreto compartido conocido solo por Alice y Bill (no se puede derivar solo de las claves públicas). Usando este secreto compartido, Bill obtiene la dirección sigilosa y puede enviar activos a ella.

Alice también obtiene la dirección a partir del secreto compartido, pero como conoce las claves privadas de las claves públicas que publicó, también puede obtener la clave privada que le permite retirar de esa dirección.

Las matemáticas (por qué las direcciones sigilosas funcionan así)

Las direcciones sigilosas estándar utilizan la criptografía de curva elíptica (ECC)opens in a new tab para obtener un mejor rendimiento con menos bits de clave, mientras mantienen el mismo nivel de seguridad. Pero en su mayor parte podemos ignorar eso y fingir que estamos usando aritmética regular.

Hay un número que todo el mundo conoce, G. Se puede multiplicar por G. Pero debido a la naturaleza de la ECC, es prácticamente imposible dividir por G. La forma en que la criptografía de clave pública funciona generalmente en Ethereum es que puede usar una clave privada, Ppriv, para firmar transacciones que luego son verificadas por una clave pública, Ppub = GPpriv.

Alice crea dos claves privadas, Kpriv y Vpriv. Kpriv se utilizará para gastar dinero de la dirección sigilosa y Vpriv para ver las direcciones que pertenecen a Alice. Alice luego publica las claves públicas: Kpub = GKpriv y Vpub = GVpriv

Bill crea una tercera clave privada, Rpriv, y publica Rpub = GRpriv en un registro central (Bill también podría habérsela enviado a Alice, pero asumimos que Carol está escuchando).

Bill calcula RprivVpub = GRprivVpriv, que espera que Alice también conozca (se explica a continuación). Este valor se llama S, el secreto compartido. Esto le da a Bill una clave pública, Ppub = Kpub+G*hash(S). A partir de esta clave pública, puede calcular una dirección y enviar los recursos que desee a ella. En el futuro, si Alice gana, Bill puede decirle Rpriv para demostrar que los recursos provinieron de él.

Alice calcula RpubVpriv = GRprivVpriv. Esto le da el mismo secreto compartido, S. Como conoce la clave privada, Kpriv, puede calcular Ppriv = Kpriv+hash(S). Esta clave le permite acceder a los activos en la dirección que resulta de Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S).

Tenemos una clave de visualización separada para permitir que Alice subcontrate a Dave's World Domination Campaign Services. Alice está dispuesta a que Dave conozca las direcciones públicas y le informe cuando haya más dinero disponible, pero no quiere que él gaste el dinero de su campaña.

Debido a que la visualización y el gasto usan claves separadas, Alice puede darle a Dave Vpriv. Entonces Dave puede calcular S = RpubVpriv = GRprivVpriv y de esa manera obtener las claves públicas (Ppub = Kpub+G*hash(S)). Pero sin Kpriv Dave no puede obtener la clave privada.

En resumen, estos son los valores conocidos por los diferentes participantes.

AlicePublicadoBillDave
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)
Dirección=f(Ppub)-Dirección=f(Ppub)Dirección=f(Ppub)Dirección=f(Ppub)
Ppriv = Kpriv+hash(S)---

Cuando las direcciones sigilosas fallan

No hay secretos en la cadena de bloques. Aunque las direcciones sigilosas pueden proporcionarle privacidad, esa privacidad es susceptible al análisis de tráfico. Para poner un ejemplo trivial, imagine que Bill financia una dirección e inmediatamente envía una transacción para publicar un valor Rpub. Sin la clave Vpriv de Alice, no podemos estar seguros de que se trate de una dirección sigilosa, pero es la apuesta más segura. Luego, vemos otra transacción que transfiere todos los ETH de esa dirección a la dirección del fondo de campaña de Alice. Puede que no podamos probarlo, pero es probable que Bill acabe de donar a la campaña de Alice. Carol sin duda lo pensaría.

Para Bill es fácil separar la publicación de Rpub de la financiación de la dirección sigilosa (hacerlo en momentos diferentes, desde direcciones diferentes). Sin embargo, eso es insuficiente. El patrón que Carol busca es que Bill financie una dirección y luego el fondo de campaña de Alice retire de ella.

Una solución es que la campaña de Alice no retire el dinero directamente, sino que lo utilice para pagar a un tercero. Si la campaña de Alice envía 10 ETH a Dave's World Domination Campaign Services, Carol solo sabe que Bill donó a uno de los clientes de Dave. Si Dave tiene suficientes clientes, Carol no podría saber si Bill donó a Alice, que compite con ella, o a Adam, Albert o Abigail, que a Carol no le importan. Alice puede incluir un valor hasheado con el pago y luego proporcionar a Dave la preimagen para demostrar que fue su donación. Alternativamente, como se señaló anteriormente, si Alice le da a Dave su Vpriv, él ya sabe de quién provino el pago.

El principal problema con esta solución es que requiere que a Alice le importe el secreto cuando ese secreto beneficia a Bill. Alice puede querer mantener su reputación para que el amigo de Bill, Bob, también le done. Pero también es posible que no le importe exponer a Bill, porque entonces él tendrá miedo de lo que sucederá si Carol gana. Bill podría terminar brindando aún más apoyo a Alice.

Uso de múltiples capas sigilosas

En lugar de depender de Alice para preservar la privacidad de Bill, Bill puede hacerlo él mismo. Puede generar múltiples metadirecciones para personas ficticias, Bob y Bella. Luego, Bill envía ETH a Bob, y «Bob» (que en realidad es Bill) se lo envía a Bella. «Bella» (también Bill) se lo envía a Alice.

Carol aún puede hacer análisis de tráfico y ver la tubería de Bill a Bob, de Bob a Bella y de Bella a Alice. Sin embargo, si «Bob» y «Bella» también usan ETH para otros fines, no parecerá que Bill transfirió nada a Alice, incluso si Alice retira inmediatamente de la dirección sigilosa a su dirección de campaña conocida.

Escribir una aplicación de dirección sigilosa

Este artículo explica una aplicación de dirección sigilosa disponible en GitHubopens in a new tab.

Herramientas

Hay una librería de typescript para direcciones sigilosasopens in a new tab que podríamos usar. Sin embargo, las operaciones criptográficas pueden ser intensivas en CPU. Prefiero implementarlas en un lenguaje compilado, como Rustopens in a new tab, y usar WASMopens in a new tab para ejecutar el código en el navegador.

Vamos a usar Viteopens in a new tab y Reactopens in a new tab. Estas son herramientas estándar de la industria; si no está familiarizado con ellas, puede usar este tutorial. Para usar Vite, necesitamos Node.

Ver direcciones sigilosas en acción

  1. Instale las herramientas necesarias: Rustopens in a new tab y Nodeopens in a new tab.

  2. Clone el repositorio de GitHub.

    1git clone https://github.com/qbzzt/251022-stealth-addresses.git
    2cd 251022-stealth-addresses
  3. Instale los requisitos previos y compile el código de Rust.

    1cd src/rust-wasm
    2rustup target add wasm32-unknown-unknown
    3cargo install wasm-pack
    4wasm-pack build --target web
  4. Inicie el servidor web.

    1cd ../..
    2npm install
    3npm run dev
  5. Navegue hasta la aplicaciónopens in a new tab. Esta página de la aplicación tiene dos marcos: uno para la interfaz de usuario de Alice y el otro para la de Bill. Los dos marcos no se comunican; solo están en la misma página por conveniencia.

  6. Como Alice, haga clic en Generar una metadirección sigilosa. Esto mostrará la nueva dirección sigilosa y las claves privadas correspondientes. Copie la metadirección sigilosa al portapapeles.

  7. Como Bill, pegue la nueva metadirección sigilosa y haga clic en Generar una dirección. Esto le da la dirección para financiar a Alice.

  8. Copie la dirección y la clave pública de Bill y péguelas en el área «Clave privada para la dirección generada por Bill» de la interfaz de usuario de Alice. Una vez que esos campos estén llenos, verá la clave privada para acceder a los activos en esa dirección.

  9. Puede usar una calculadora en líneaopens in a new tab para asegurarse de que la clave privada corresponde a la dirección.

Cómo funciona el programa

El componente WASM

El código fuente que se compila en WASM está escrito en Rustopens in a new tab. Puede verlo en src/rust_wasm/src/lib.rsopens in a new tab. Este código es principalmente una interfaz entre el código de JavaScript y la librería eth-stealth-addressesopens in a new tab.

Cargo.toml

Cargo.tomlopens in a new tab en Rust es análogo a package.jsonopens in a new tab en JavaScript. Contiene información del paquete, declaraciones de dependencia, 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"] }
Mostrar todo

El paquete getrandomopens in a new tab necesita generar valores aleatorios. Eso no se puede hacer por medios puramente algorítmicos; requiere acceso a un proceso físico como fuente de entropía. Esta definición especifica que obtendremos esa entropía preguntándole al navegador en el que estamos ejecutando.

1console_error_panic_hook = "0.1.7"

Esta libreríaopens in a new tab nos da mensajes de error más significativos cuando el código WASM entra en pánico y no puede continuar.

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

El tipo de salida requerido para producir código WASM.

lib.rs

Este es el código real de Rust.

1use wasm_bindgen::prelude::*;

Las definiciones para crear un paquete WASM a partir de Rust. Están documentadas aquíopens in a new tab.

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

Las funciones que necesitamos de la librería eth-stealth-addressesopens in a new tab.

1use hex::{decode,encode};

Rust generalmente usa matricesopens in a new tab de bytes ([u8; <size>]) para los valores. Pero en JavaScript, generalmente usamos cadenas hexadecimales. La librería hexopens in a new tab traduce por nosotros de una representación a la otra.

1#[wasm_bindgen]

Generar enlaces WASM para poder llamar a esta función desde JavaScript.

1pub fn wasm_generate_stealth_meta_address() -> String {

La forma más fácil de devolver un objeto con múltiples campos es devolver una cadena JSON.

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

La función generate_stealth_meta_addressopens in a new tab devuelve tres campos:

  • La metadirección (Kpub y Vpub)
  • La clave privada de visualización (Vpriv)
  • La clave privada de gasto (Kpriv)

La sintaxis de tuplaopens in a new tab nos permite separar esos valores nuevamente.

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

Use la macro format!opens in a new tab para generar la cadena codificada en JSON. Use hex::encodeopens in a new tab para cambiar las matrices a cadenas hexadecimales.

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

Esta función convierte una cadena hexadecimal (proporcionada por JavaScript) en una matriz de bytes. La usamos para analizar los valores proporcionados por el código de JavaScript. Esta función es complicada por la forma en que Rust maneja las matrices y los vectores.

La expresión <const N: usize> se llama un genéricoopens in a new tab. N es un parámetro que controla la longitud de la matriz devuelta. La función en realidad se llama str_to_array::<n>, donde n es la longitud de la matriz.

El valor de retorno es Option<[u8; N]>, lo que significa que la matriz devuelta es opcionalopens in a new tab. Este es un patrón típico en Rust para funciones que pueden fallar.

Por ejemplo, si llamamos a str_to_array::10("bad060a7"), se supone que la función debe devolver una matriz de diez valores, pero la entrada es de solo cuatro bytes. La función necesita fallar, y lo hace devolviendo None. El valor de retorno para str_to_array::4("bad060a7") sería Some<[0xba, 0xd0, 0x60, 0xa7]>.

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

La función hex::decodeopens in a new tab devuelve un Result<Vec<u8>, FromHexError>. El tipo Resultopens in a new tab puede contener un resultado exitoso (Ok(value)) o un error (Err(error)).

El método .ok() convierte el Result en un Option, cuyo valor es el valor Ok() si tiene éxito o None si no lo tiene. Finalmente, el operador de signo de interrogaciónopens in a new tab aborta las funciones actuales y devuelve un None si el Option está vacío. De lo contrario, desenvuelve el valor y lo devuelve (en este caso, para asignar un valor a vec).

Este parece un método extrañamente complicado para manejar errores, pero Result y Option aseguran que todos los errores se manejen, de una forma u otra.

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

Si el número de bytes es incorrecto, es un fallo, y devolvemos None.

1 // try_into consumes vec and attempts to make [u8; N]
2 let array: [u8; N] = vec.try_into().ok()?;

Rust tiene dos tipos de matrices. Las matricesopens in a new tab tienen un tamaño fijo. Los vectoresopens in a new tab pueden crecer y encogerse. hex::decode devuelve un vector, pero la librería eth_stealth_addresses quiere recibir matrices. .try_into()opens in a new tab convierte un valor en otro tipo, por ejemplo, un vector en una matriz.

1 Some(array)
2}

Rust no requiere que use la palabra clave returnopens in a new tab al devolver un valor al final de una función.

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

Esta función recibe una metadirección pública, que incluye tanto Vpub como Kpub. Devuelve la dirección sigilosa, la clave pública para publicar (Rpub) y un valor de escaneo de un byte que acelera la identificación de qué direcciones publicadas pueden pertenecer a Alice.

El valor de escaneo es parte del secreto compartido (S = GRprivVpriv). Este valor está disponible para Alice, y verificarlo es mucho más rápido que verificar si f(Kpub+G*hash(S)) es igual a la dirección publicada.

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

Usamos la función generate_stealth_addressopens in a new tab de la librería.

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

Preparar la cadena de salida codificada 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}
Mostrar todo

Esta función usa la función compute_stealth_keyopens in a new tab de la librería para calcular la clave privada para retirar de la dirección (Rpriv). Este cálculo requiere estos valores:

  • La dirección (Dirección=f(Ppub))
  • La clave pública generada por Bill (Rpub)
  • La clave privada de visualización (Vpriv)
  • La clave privada de gasto (Kpriv)
1#[wasm_bindgen(start)]

#[wasm_bindgen(start)]opens in a new tab especifica que la función se ejecuta cuando se inicializa el código WASM.

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

Este código especifica que la salida de pánico se envíe a la consola de JavaScript. Para verlo en acción, use la aplicación y dé a Bill una metadirección no válida (simplemente cambie un dígito hexadecimal). Verá este error en la consola de 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

Seguido de un seguimiento de la pila. Luego, dé a Bill la metadirección válida y a Alice una dirección no válida o una clave pública no válida. Verá este error:

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

De nuevo, seguido de un seguimiento de la pila.

La interfaz de usuario

La interfaz de usuario está escrita con Reactopens in a new tab y servida por Viteopens in a new tab. Puede aprender sobre ellos usando este tutorial. No hay necesidad de WAGMIopens in a new tab aquí porque no interactuamos directamente con una cadena de bloques o una billetera.

La única parte no obvia de la interfaz de usuario es la conectividad WASM. Así es como funciona.

vite.config.js

Este archivo contiene la configuración de Viteopens 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})

Necesitamos dos plugins de Vite: reactopens in a new tab y wasmopens in a new tab.

App.jsx

Este archivo es el componente principal de la aplicación. Es un contenedor que incluye dos componentes: Alice y Bill, las interfaces de usuario para esos usuarios. La parte relevante para WASM es el código de inicialización.

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

Cuando usamos wasm-packopens in a new tab, crea dos archivos que usamos aquí: un archivo wasm con el código real (aquí, src/rust-wasm/pkg/rust_wasm_bg.wasm) y un archivo JavaScript con las definiciones para usarlo (aquí, src/rust_wasm/pkg/rust_wasm.js). La exportación predeterminada de ese archivo JavaScript es código que necesita ejecutarse para iniciar 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 )
Mostrar todo

El hook useEffectopens in a new tab le permite especificar una función que se ejecuta cuando cambian las variables de estado. Aquí, la lista de variables de estado está vacía ([]), por lo que esta función se ejecuta solo una vez cuando la página se carga.

La función de efecto tiene que retornar inmediatamente. Para usar código asíncrono, como el init de WASM (que tiene que cargar el archivo .wasm y por lo tanto toma tiempo), definimos una función interna asyncopens in a new tab y la ejecutamos sin un await.

Bill.jsx

Esta es la interfaz de usuario para Bill. Tiene una sola acción, crear una dirección basada en la metadirección sigilosa proporcionada por Alice.

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

Además de la exportación predeterminada, el código JavaScript generado por wasm-pack exporta una función por cada función en el código WASM.

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

Para llamar a las funciones de WASM, simplemente llamamos a la función exportada por el archivo JavaScript creado por wasm-pack.

Alice.jsx

El código en Alice.jsx es análogo, excepto que Alice tiene dos acciones:

  • Generar una metadirección
  • Obtener la clave privada para una dirección publicada por Bill

Conclusión

Las direcciones sigilosas no son la panacea; tienen que ser usadas correctamente. Pero cuando se usan correctamente, pueden habilitar la privacidad en una cadena de bloques pública.

Vea aquí más de mi trabajoopens in a new tab.

Última actualización de la página: 14 de noviembre de 2025

¿Le ha resultado útil este tutorial?