Usando endereços secretos
Você é o Bill. Por razões que não vamos entrar em detalhes, você quer fazer uma doação para a campanha "Alice para Rainha do Mundo" e que a Alice saiba que você doou para que ela o recompense se ganhar. Infelizmente, a vitória dela não é garantida. Existe uma campanha concorrente, "Carol para Imperatriz do Sistema Solar". Se a Carol ganhar, e ela descobrir que você doou para a Alice, você terá problemas. Então você não pode simplesmente transferir 200 ETH da sua conta para a da Alice.
O ERC-5564 (opens in a new tab) tem a solução. Este ERC explica como usar endereços secretos (opens in a new tab) para transferência anônima.
Aviso: A criptografia por trás dos endereços secretos é, até onde sabemos, sólida. No entanto, existem potenciais ataques de canal lateral. Abaixo, você verá o que pode fazer para reduzir este risco.
Como os endereços secretos funcionam
Este artigo tentará explicar os endereços secretos de duas maneiras. A primeira é como usá-los. Esta parte é suficiente para entender o resto do artigo. Depois, há uma explicação da matemática por trás disso. Se você estiver interessado em criptografia, leia esta parte também.
A versão simples (como usar endereços secretos)
Alice cria duas chaves privadas e publica as chaves públicas correspondentes (que podem ser combinadas em um único meta-endereço de comprimento duplo). Bill também cria uma chave privada e publica a chave pública correspondente.
Usando a chave pública de uma parte e a chave privada da outra, você pode derivar um segredo compartilhado conhecido apenas por Alice e Bill (ele não pode ser derivado apenas das chaves públicas). Usando este segredo compartilhado, Bill obtém o endereço secreto e pode enviar ativos para ele.
Alice também obtém o endereço a partir do segredo compartilhado, mas como ela conhece as chaves privadas para as chaves públicas que publicou, ela também pode obter a chave privada que lhe permite sacar desse endereço.
A matemática (por que endereços secretos funcionam assim)
Endereços secretos padrão usam criptografia de curva elíptica (ECC) (opens in a new tab) para obter melhor desempenho com menos bits de chave, mantendo o mesmo nível de segurança. Mas, na maior parte, podemos ignorar isso e fingir que estamos usando aritmética regular.
Existe um número que todos conhecem, G. Você pode multiplicar por G. Mas, devido à natureza da ECC, é praticamente impossível dividir por G. A forma como a criptografia de chave pública geralmente funciona no Ethereum é que você pode usar uma chave privada, Ppriv, para assinar transações que são então verificadas por uma chave pública, Ppub = GPpriv.
Alice cria duas chaves privadas, Kpriv e Vpriv. Kpriv será usada para gastar dinheiro do endereço secreto, e Vpriv para visualizar os endereços que pertencem a Alice. Alice então publica as chaves públicas: Kpub = GKpriv e Vpub = GVpriv
Bill cria uma terceira chave privada, Rpriv, e publica Rpub = GRpriv em um registro central (Bill também poderia tê-la enviado para Alice, mas assumimos que Carol está ouvindo).
Bill calcula RprivVpub = GRprivVpriv, que ele espera que Alice também saiba (explicado abaixo). Este valor é chamado S, o segredo compartilhado. Isso dá a Bill uma chave pública, Ppub = Kpub+G*hash(S). A partir desta chave pública, ele pode calcular um endereço e enviar quaisquer recursos que ele queira para ele. No futuro, se Alice ganhar, Bill pode dizer a ela Rpriv para provar que os recursos vieram dele.
Alice calcula RpubVpriv = GRprivVpriv. Isso dá a ela o mesmo segredo compartilhado, S. Como ela conhece a chave privada, Kpriv, ela pode calcular Ppriv = Kpriv+hash(S). Esta chave permite que ela acesse ativos no endereço que resulta de Ppub = GPpriv = GKpriv+G*hash(S) = Kpub+G*hash(S).
Temos uma chave de visualização separada para permitir que Alice subcontrate os Serviços de Campanha de Dominação Mundial de Dave. Alice está disposta a deixar Dave saber os endereços públicos e informá-la quando mais dinheiro estiver disponível, mas ela não quer que ele gaste o dinheiro da campanha dela.
Como a visualização e o gasto usam chaves separadas, Alice pode dar a Dave Vpriv. Então Dave pode calcular S = RpubVpriv = GRprivVpriv e dessa forma obter as chaves públicas (Ppub = Kpub+G*hash(S)). Mas sem Kpriv Dave não consegue obter a chave privada.
Para resumir, estes são os valores conhecidos pelos diferentes participantes.
| Alice | Publicado | 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) | |
| Endereço=f(Ppub) | — | Endereço=f(Ppub) | Endereço=f(Ppub) | Endereço=f(Ppub) |
| Ppriv = Kpriv+hash(S) | — | — | — |
Quando os endereços secretos dão errado
Não há segredos na cadeia de blocos. Embora os endereços secretos possam fornecer privacidade, essa privacidade é suscetível à análise de tráfego. Para dar um exemplo trivial, imagine que Bill financia um endereço e imediatamente envia uma transação para publicar um valor Rpub. Sem o Vpriv de Alice, não podemos ter certeza de que este é um endereço secreto, mas essa é a aposta mais segura. Em seguida, vemos outra transação que transfere todo o ETH desse endereço para o endereço do fundo de campanha de Alice. Podemos não ser capazes de provar, mas é provável que Bill tenha acabado de doar para a campanha de Alice. Carol certamente pensaria assim.
É fácil para Bill separar a publicação de Rpub do financiamento do endereço secreto (fazê-los em momentos diferentes, a partir de endereços diferentes). No entanto, isso não é suficiente. O padrão que Carol procura é que Bill financie um endereço e, em seguida, o fundo de campanha de Alice saca dele.
Uma solução é a campanha de Alice não sacar o dinheiro diretamente, mas usá-lo para pagar um terceiro. Se a campanha de Alice envia 10 ETH para os Serviços de Campanha de Dominação Mundial de Dave, Carol só sabe que Bill doou para um dos clientes de Dave. Se Dave tiver clientes suficientes, Carol não seria capaz de saber se Bill doou para Alice, que compete com ela, ou para Adam, Albert ou Abigail, com quem Carol não se importa. Alice pode incluir um valor com hash no pagamento e, em seguida, fornecer a Dave a pré-imagem, para provar que foi sua doação. Alternativamente, como observado acima, se Alice der a Dave seu Vpriv, ele já saberá de quem veio o pagamento.
O principal problema com essa solução é que ela exige que Alice se preocupe com o sigilo quando esse sigilo beneficia Bill. Alice pode querer manter sua reputação para que o amigo de Bill, Bob, também doe para ela. Mas também é possível que ela não se importe em expor Bill, porque então ele ficará com medo do que acontecerá se Carol vencer. Bill pode acabar dando ainda mais apoio a Alice.
Usando várias camadas secretas
Em vez de confiar em Alice para preservar a privacidade de Bill, o próprio Bill pode fazê-lo. Ele pode gerar múltiplos meta-endereços para pessoas fictícias, Bob e Bella. Bill então envia ETH para Bob, e "Bob" (que na verdade é Bill) o envia para Bella. "Bella" (também Bill) o envia para Alice.
Carol ainda pode fazer análise de tráfego e ver o pipeline Bill-para-Bob-para-Bella-para-Alice. No entanto, se "Bob" e "Bella" também usarem ETH para outros fins, não parecerá que Bill transferiu nada para Alice, mesmo que Alice saque imediatamente do endereço secreto para seu endereço de campanha conhecido.
Escrevendo uma aplicação de endereço secreto
Este artigo explica uma aplicação de endereço secreto disponível no GitHub (opens in a new tab).
Ferramentas
Existe uma biblioteca de endereço secreto em typescript (opens in a new tab) que poderíamos usar. No entanto, operações criptográficas podem ser intensivas em CPU. Prefiro implementá-las em uma linguagem compilada, como Rust (opens in a new tab), e usar WASM (opens in a new tab) para executar o código no navegador.
Vamos usar Vite (opens in a new tab) e React (opens in a new tab). Estas são ferramentas padrão da indústria; se você não estiver familiarizado com elas, pode usar este tutorial. Para usar o Vite, precisamos do Node.
Veja os endereços secretos em ação
-
Instale as ferramentas necessárias: Rust (opens in a new tab) e Node (opens in a new tab).
-
Clone o repositório do GitHub.
1git clone https://github.com/qbzzt/251022-stealth-addresses.git2cd 251022-stealth-addresses -
Instale os pré-requisitos e compile o código Rust.
1cd src/rust-wasm2rustup target add wasm32-unknown-unknown3cargo install wasm-pack4wasm-pack build --target web -
Inicie o servidor web.
1cd ../..2npm install3npm run dev -
Acesse a aplicação (opens in a new tab). Esta página da aplicação tem dois frames: um para a interface de usuário de Alice e outro para a de Bill. Os dois frames não se comunicam; eles estão na mesma página apenas por conveniência.
-
Como Alice, clique em Gerar um Meta-endereço Secreto. Isso exibirá o novo endereço secreto e as chaves privadas correspondentes. Copie o meta-endereço secreto para a área de transferência.
-
Como Bill, cole o novo meta-endereço secreto e clique em Gerar um endereço. Isso lhe dá o endereço para financiar Alice.
-
Copie o endereço e a chave pública de Bill e cole-os na área "Chave privada para endereço gerado por Bill" da interface do usuário de Alice. Depois que esses campos forem preenchidos, você verá a chave privada para acessar os ativos nesse endereço.
-
Você pode usar uma calculadora online (opens in a new tab) para garantir que a chave privada corresponda ao endereço.
Como o programa funciona
O componente WASM
O código-fonte que compila para WASM é escrito em Rust (opens in a new tab). Você pode vê-lo em src/rust_wasm/src/lib.rs (opens in a new tab). Este código é principalmente uma interface entre o código JavaScript e a biblioteca eth-stealth-addresses (opens in a new tab).
Cargo.toml
Cargo.toml (opens in a new tab) em Rust é análogo a package.json (opens in a new tab) em JavaScript. Ele contém informações do pacote, declarações de dependência, etc.
1[package]2name = "rust-wasm"3version = "0.1.0"4edition = "2024"56[dependencies]7eth-stealth-addresses = "0.1.0"8hex = "0.4.3"9wasm-bindgen = "0.2.104"10getrandom = { version = "0.2", features = ["js"] }Exibir tudoO pacote getrandom (opens in a new tab) precisa gerar valores aleatórios. Isso não pode ser feito por meios puramente algorítmicos; requer acesso a um processo físico como fonte de entropia. Esta definição especifica que obteremos essa entropia perguntando ao navegador em que estamos executando.
1console_error_panic_hook = "0.1.7"Esta biblioteca (opens in a new tab) nos dá mensagens de erro mais significativas quando o código WASM entra em pânico e não pode continuar.
1[lib]2crate-type = ["cdylib", "rlib"]O tipo de saída necessário para produzir código WASM.
lib.rs
Este é o código Rust real.
1use wasm_bindgen::prelude::*;As definições para criar um pacote WASM a partir do Rust. Elas estão documentadas aqui (opens in a new tab).
1use eth_stealth_addresses::{2 generate_stealth_meta_address,3 generate_stealth_address,4 compute_stealth_key5};As funções que precisamos da biblioteca eth-stealth-addresses (opens in a new tab).
1use hex::{decode,encode};Rust normalmente usa arrays (opens in a new tab) de bytes ([u8; <size>]) para valores. Mas em JavaScript, normalmente usamos strings hexadecimais. A biblioteca hex (opens in a new tab) traduz para nós de uma representação para a outra.
1#[wasm_bindgen]Gere bindings WASM para poder chamar esta função a partir do JavaScript.
1pub fn wasm_generate_stealth_meta_address() -> String {A maneira mais fácil de retornar um objeto com vários campos é retornar uma string JSON.
1 let (address, spend_private_key, view_private_key) = 2 generate_stealth_meta_address();A generate_stealth_meta_address (opens in a new tab) retorna três campos:
- O meta-endereço (Kpub e Vpub)
- A chave privada de visualização (Vpriv)
- A chave privada de gastos (Kpriv)
A sintaxe de tupla (opens in a new tab) nos permite separar esses valores novamente.
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 a macro format! (opens in a new tab) para gerar a string codificada em JSON. Use hex::encode (opens in a new tab) para alterar os arrays para strings hexadecimais.
1fn str_to_array<const N: usize>(s: &str) -> Option<[u8; N]> {Esta função transforma uma string hexadecimal (fornecida pelo JavaScript) em um array de bytes. Nós a usamos para analisar valores fornecidos pelo código JavaScript. Esta função é complicada por causa de como o Rust lida com arrays e vetores.
A expressão <const N: usize> é chamada de genérica (opens in a new tab). N é um parâmetro que controla o comprimento do array retornado. A função é na verdade chamada str_to_array::<n>, onde n é o comprimento do array.
O valor de retorno é Option<[u8; N]>, o que significa que o array retornado é opcional (opens in a new tab). Este é um padrão típico em Rust para funções que podem falhar.
Por exemplo, se chamarmos str_to_array::10("bad060a7"), a função deveria retornar um array de dez valores, mas a entrada tem apenas quatro bytes. A função precisa falhar, e ela faz isso retornando None. O valor de retorno para str_to_array::4("bad060a7") seria Some<[0xba, 0xd0, 0x60, 0xa7]>.
1 // decode returns Result<Vec<u8>, _>2 let vec = decode(s).ok()?;A função hex::decode (opens in a new tab) retorna um Result<Vec<u8>, FromHexError>. O tipo Result (opens in a new tab) pode conter um resultado bem-sucedido (Ok(value)) ou um erro (Err(error)).
O método .ok() transforma o Result em um Option, cujo valor é o valor de Ok() se for bem-sucedido, ou None se não for. Finalmente, o operador de ponto de interrogação (opens in a new tab) aborta as funções atuais e retorna um None se o Option estiver vazio. Caso contrário, ele desempacota o valor e o retorna (neste caso, para atribuir um valor a vec).
Este parece ser um método estranhamente complicado para lidar com erros, mas Result e Option garantem que todos os erros sejam tratados, de uma forma ou de outra.
1 if vec.len() != N { return None; }Se o número de bytes estiver incorreto, isso é uma falha e retornamos None.
1 // try_into consome vec e tenta fazer [u8; N]2 let array: [u8; N] = vec.try_into().ok()?;Rust tem dois tipos de array. Arrays (opens in a new tab) têm um tamanho fixo. Vetores (opens in a new tab) podem crescer e encolher. hex::decode retorna um vetor, mas a biblioteca eth_stealth_addresses quer receber arrays. .try_into() (opens in a new tab) converte um valor em outro tipo, por exemplo, um vetor em um array.
1 Some(array)2}Rust não exige que você use a palavra-chave return (opens in a new tab) ao retornar um valor no final de uma função.
1#[wasm_bindgen]2pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {Esta função recebe um meta-endereço público, que inclui tanto Vpub quanto Kpub. Ele retorna o endereço secreto, a chave pública para publicar (Rpub) e um valor de varredura de um byte que acelera a identificação de quais endereços publicados podem pertencer a Alice.
O valor de varredura faz parte do segredo compartilhado (S = GRprivVpriv). Este valor está disponível para Alice, e verificá-lo é muito mais rápido do que verificar se f(Kpub+G*hash(S)) é igual ao endereço publicado.
1 let (address, r_pub, scan) = 2 generate_stealth_address(&str_to_array::<66>(stealth_address)?);Usamos a função generate_stealth_address (opens in a new tab) da biblioteca.
1 format!("{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}",2 encode(address),3 encode(r_pub),4 encode(&[scan])5 ).into()6}Prepare a string de saída codificada em 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}Exibir tudoEsta função usa a função compute_stealth_key (opens in a new tab) da biblioteca para calcular a chave privada para sacar do endereço (Rpriv). Este cálculo requer estes valores:
- O endereço (Endereço=f(Ppub))
- A chave pública gerada por Bill (Rpub)
- A chave privada de visualização (Vpriv)
- A chave privada de gastos (Kpriv)
1#[wasm_bindgen(start)]#[wasm_bindgen(start)] (opens in a new tab) especifica que a função é executada quando o código WASM é inicializado.
1pub fn main() {2 console_error_panic_hook::set_once();3}Este código especifica que a saída de pânico seja enviada para o console JavaScript. Para vê-lo em ação, use a aplicação e dê a Bill um meta-endereço inválido (apenas mude um dígito hexadecimal). Você verá este erro no console do 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` failed3 left: 04 right: 1Seguido por um rastreamento de pilha. Em seguida, dê a Bill o meta-endereço válido e a Alice um endereço inválido ou uma chave pública inválida. Você verá este erro:
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 addressNovamente, seguido por um rastreamento de pilha.
A interface do usuário
A interface do usuário é escrita usando React (opens in a new tab) e servida por Vite (opens in a new tab). Você pode aprender sobre eles usando este tutorial. Não há necessidade de WAGMI (opens in a new tab) aqui, porque não interagimos diretamente com uma cadeia de blocos ou uma carteira.
A única parte não óbvia da interface do usuário é a conectividade WASM. Funciona assim.
vite.config.js
Este arquivo contém a configuração do Vite (opens in a new tab).
1import { defineConfig } from 'vite'2import react from '@vitejs/plugin-react'3import wasm from "vite-plugin-wasm";45// https://vite.dev/config/6export default defineConfig({7 plugins: [react(), wasm()],8})Precisamos de dois plugins Vite: react (opens in a new tab) e wasm (opens in a new tab).
App.jsx
Este arquivo é o componente principal da aplicação. É um contêiner que inclui dois componentes: Alice e Bill, as interfaces de usuário para esses usuários. A parte relevante para o WASM é o código de inicialização.
1import init from './rust-wasm/pkg/rust_wasm.js'Quando usamos wasm-pack (opens in a new tab), ele cria dois arquivos que usamos aqui: um arquivo wasm com o código real (aqui, src/rust-wasm/pkg/rust_wasm_bg.wasm) e um arquivo JavaScript com as definições para usá-lo (aqui, src/rust_wasm/pkg/rust_wasm.js). A exportação padrão desse arquivo JavaScript é um código que precisa ser executado para iniciar o 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 }1516 loadWasm()17 }, []18 )Exibir tudoO hook useEffect (opens in a new tab) permite que você especifique uma função que é executada quando as variáveis de estado mudam. Aqui, a lista de variáveis de estado está vazia ([]), então esta função é executada apenas uma vez quando a página carrega.
A função de efeito deve retornar imediatamente. Para usar código assíncrono, como o init do WASM (que precisa carregar o arquivo .wasm e, portanto, leva tempo), definimos uma função interna async (opens in a new tab) e a executamos sem um await.
Bill.jsx
Esta é a interface de usuário para Bill. Ele tem uma única ação, criar um endereço com base no meta-endereço secreto fornecido por Alice.
1import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'Além da exportação padrão, o código JavaScript gerado pelo wasm-pack exporta uma função para cada função no código WASM.
1 <button onClick={() => {2 setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))3 }}>Para chamar funções WASM, basta chamar a função exportada pelo arquivo JavaScript criado pelo wasm-pack.
Alice.jsx
O código em Alice.jsx é análogo, exceto que Alice tem duas ações:
- Gerar um meta-endereço
- Obter a chave privada para um endereço publicado por Bill
Conclusão
Endereços secretos não são uma panaceia; eles devem ser usados corretamente. Mas, quando usados corretamente, eles podem permitir a privacidade em uma cadeia de blocos pública.
Veja aqui mais do meu trabalho (opens in a new tab).
Última atualização da página: 14 de novembro de 2025