Использование скрытых адресов
Вы — Билл. По причинам, которые мы не будем рассматривать, вы хотите сделать пожертвование в кампанию "Алиса — королева мира" и хотите, чтобы Алиса знала о вашем пожертвовании, чтобы она вознаградила вас в случае своей победы. К сожалению, ее победа не гарантирована. Существует конкурирующая кампания "Кэрол — императрица Солнечной системы". Если Кэрол победит и узнает, что вы пожертвовали Алисе, у вас будут проблемы. Поэтому вы не можете просто перевести 200 ETH со своего аккаунта на аккаунт Алисы.
Решение предлагает ERC-5564 (opens in a new tab). Этот ERC объясняет, как использовать скрытые адреса (opens in a new tab) для анонимных переводов.
Предупреждение. Криптография, лежащая в основе скрытых адресов, насколько нам известно, надежна. Однако существуют потенциальные атаки по побочным каналам. Ниже вы увидите, что можно сделать, чтобы снизить этот риск.
Как работают скрытые адреса
В этой статье мы попытаемся объяснить скрытые адреса двумя способами. Первый — как их использовать. Этой части достаточно, чтобы понять остальную часть статьи. Затем следует объяснение математических основ. Если вы интересуетесь криптографией, прочтите и эту часть.
Простая версия (как использовать скрытые адреса)
Алиса создает два приватных ключа и публикует соответствующие публичные ключи (которые можно объединить в один метаадрес двойной длины). Билл также создает приватный ключ и публикует соответствующий публичный ключ.
Используя публичный ключ одной стороны и приватный ключ другой, можно получить общий секрет, известный только Алисе и Биллу (его нельзя получить только из публичных ключей). Используя этот общий секрет, Билл получает скрытый адрес и может отправлять на него активы.
Алиса также получает адрес из общего секрета, но поскольку она знает приватные ключи к опубликованным ею публичным ключам, она также может получить приватный ключ, который позволяет ей снимать средства с этого адреса.
Математика (почему скрытые адреса работают именно так)
Стандартные скрытые адреса используют криптографию на эллиптических кривых (ECC) (opens in a new tab) для повышения производительности с меньшим количеством битов ключа при сохранении того же уровня безопасности. Но по большей части мы можем это игнорировать и делать вид, что используем обычную арифметику.
Существует число, известное всем, — G. Вы можете умножать на G. Но из-за природы ECC практически невозможно делить на G. В Ethereum криптография с публичным ключом обычно работает так: вы можете использовать приватный ключ Ppriv для подписи транзакций, которые затем проверяются публичным ключом Ppub = GPpriv.
Алиса создает два приватных ключа: Kpriv и Vpriv. Kpriv будет использоваться для траты денег со скрытого адреса, а Vpriv — для просмотра адресов, принадлежащих Алисе. Затем Алиса публикует публичные ключи: Kpub = GKpriv и Vpub = GVpriv
Билл создает третий приватный ключ Rpriv и публикует Rpub = GRpriv в центральном реестре (Билл мог бы отправить его Алисе, но мы предполагаем, что Кэрол слушает).
Билл вычисляет RprivVpub = GRprivVpriv, который, как он ожидает, Алиса также знает (объяснено ниже). Это значение называется S, общий секрет. Это дает Биллу публичный ключ Ppub = Kpub+G*hash(S). Из этого публичного ключа он может вычислить адрес и отправить на него любые ресурсы. В будущем, если Алиса победит, Билл может сообщить ей Rpriv, чтобы доказать, что ресурсы поступили от него.
Алиса вычисляет RpubVpriv = GRprivVpriv. Это дает ей тот же общий секрет, S. Поскольку она знает приватный ключ Kpriv, она может вычислить Ppriv = Kpriv+hash(S). Этот ключ позволяет ей получить доступ к активам на адресе, который является результатом Ppub = GPpriv = GKpriv+Ghash(S) = Kpub+Ghash(S).
У нас есть отдельный ключ просмотра, чтобы Алиса могла передать подряд Dave's World Domination Campaign Services. Алиса готова сообщить Дэйву публичные адреса и информировать ее о поступлении новых денег, но она не хочет, чтобы он тратил деньги ее кампании.
Поскольку для просмотра и траты используются разные ключи, Алиса может дать Дэйву Vpriv. Тогда Дэйв может вычислить S = RpubVpriv = GRprivVpriv и таким образом получить публичные ключи (Ppub = Kpub+G*hash(S)). Но без Kpriv Дэйв не сможет получить приватный ключ.
Таким образом, это значения, известные разным участникам.
| Алиса | Опубликовано | Билл | Дэйв | |
|---|---|---|---|---|
| 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) | |
| Address=f(Ppub) | – | Address=f(Ppub) | Address=f(Ppub) | Address=f(Ppub) |
| Ppriv = Kpriv+hash(S) | – | – | – |
Когда скрытые адреса работают не так, как надо
В блокчейне нет секретов. Хотя скрытые адреса могут обеспечить вам конфиденциальность, эта конфиденциальность уязвима для анализа трафика. В качестве простого примера представьте, что Билл пополняет адрес и сразу же отправляет транзакцию для публикации значения Rpub. Без Vpriv Алисы мы не можем быть уверены, что это скрытый адрес, но стоит сделать ставку именно на это. Затем мы видим еще одну транзакцию, которая переводит все ETH с этого адреса на адрес фонда кампании Алисы. Возможно, мы не сможем этого доказать, но, скорее всего, Билл только что сделал пожертвование в кампанию Алисы. Кэрол наверняка так и подумает.
Биллу легко отделить публикацию Rpub от финансирования скрытого адреса (делать это в разное время, с разных адресов). Однако этого недостаточно. Кэрол ищет закономерность: Билл пополняет адрес, а затем фонд кампании Алисы снимает с него средства.
Одно из решений состоит в том, чтобы кампания Алисы не снимала деньги напрямую, а использовала их для оплаты третьей стороне. Если кампания Алисы отправит 10 ETH в Dave's World Domination Campaign Services, Кэрол будет знать только то, что Билл сделал пожертвование одному из клиентов Дэйва. Если у Дэйва достаточно клиентов, Кэрол не сможет узнать, пожертвовал ли Билл Алисе, которая с ней конкурирует, или Адаму, Альберту или Эбигейл, до которых Кэрол нет дела. Алиса может включить в платеж хэшированное значение, а затем предоставить Дэйву прообраз, чтобы доказать, что это было ее пожертвование. В качестве альтернативы, как отмечалось выше, если Алиса даст Дэйву свой Vpriv, он уже будет знать, от кого пришел платеж.
Основная проблема этого решения в том, что оно требует от Алисы заботиться о секретности, когда эта секретность выгодна Биллу. Алиса может захотеть сохранить свою репутацию, чтобы друг Билла, Боб, тоже сделал ей пожертвование. Но также возможно, что она не прочь разоблачить Билла, потому что тогда он будет бояться того, что произойдет, если Кэрол победит. Билл может в итоге оказать Алисе еще большую поддержку.
Использование нескольких скрытых слоев
Вместо того чтобы полагаться на Алису в сохранении конфиденциальности Билла, Билл может сделать это сам. Он может сгенерировать несколько метаадресов для вымышленных людей, Боба и Беллы. Затем Билл отправляет ETH Бобу, а "Боб" (который на самом деле Билл) отправляет их Белле. "Белла" (тоже Билл) отправляет их Алисе.
Кэрол все еще может анализировать трафик и видеть цепочку от Билла к Бобу, от Боба к Белле, от Беллы к Алисе. Однако, если "Боб" и "Белла" также используют ETH для других целей, не будет выглядеть так, будто Билл что-то перевел Алисе, даже если Алиса немедленно снимет средства со скрытого адреса на известный адрес своей кампании.
Написание приложения для скрытых адресов
В этой статье объясняется приложение для скрытых адресов, доступное на GitHub (opens in a new tab).
Инструменты
Существует библиотека скрытых адресов на typescript (opens in a new tab), которую мы могли бы использовать. Однако криптографические операции могут быть ресурсоемкими для процессора. Я предпочитаю реализовывать их на компилируемом языке, таком как Rust (opens in a new tab), и использовать WASM (opens in a new tab) для запуска кода в браузере.
Мы будем использовать Vite (opens in a new tab) и React (opens in a new tab). Это стандартные отраслевые инструменты; если вы с ними не знакомы, можете воспользоваться этим руководством. Для использования Vite нам нужен Node.
Скрытые адреса в действии
-
Установите необходимые инструменты: Rust (opens in a new tab) и Node (opens in a new tab).
-
Клонируйте репозиторий GitHub.
1git clone https://github.com/qbzzt/251022-stealth-addresses.git2cd 251022-stealth-addresses -
Установите необходимые компоненты и скомпилируйте код Rust.
1cd src/rust-wasm2rustup target add wasm32-unknown-unknown3cargo install wasm-pack4wasm-pack build --target web -
Запустите веб-сервер.
1cd ../..2npm install3npm run dev -
Перейдите в приложение (opens in a new tab). Эта страница приложения имеет два фрейма: один для пользовательского интерфейса Алисы, а другой — для Билла. Два фрейма не взаимодействуют; они находятся на одной странице только для удобства.
-
От имени Алисы нажмите Generate a Stealth Meta-Address. Будет отображен новый скрытый адрес и соответствующие ему приватные ключи. Скопируйте скрытый метаадрес в буфер обмена.
-
От имени Билла вставьте новый скрытый метаадрес и нажмите Generate an address. Вы получите адрес для пополнения счета Алисы.
-
Скопируйте адрес и публичный ключ Билла и вставьте их в область "Private key for address generated by Bill" пользовательского интерфейса Алисы. После заполнения этих полей вы увидите приватный ключ для доступа к активам по этому адресу.
-
Вы можете использовать онлайн-калькулятор (opens in a new tab), чтобы убедиться, что приватный ключ соответствует адресу.
Как работает программа
Компонент WASM
Исходный код, который компилируется в WASM, написан на Rust (opens in a new tab). Вы можете увидеть его в src/rust_wasm/src/lib.rs (opens in a new tab). Этот код в основном является интерфейсом между кодом JavaScript и библиотекой eth-stealth-addresses (opens in a new tab).
Cargo.toml
Cargo.toml (opens in a new tab) в Rust является аналогом package.json (opens in a new tab) в JavaScript. Он содержит информацию о пакете, объявления зависимостей и т. д.
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"] }Показать всеПакет getrandom (opens in a new tab) необходим для генерации случайных значений. Это невозможно сделать чисто алгоритмическими средствами; для этого требуется доступ к физическому процессу как источнику энтропии. Это определение указывает, что мы будем получать эту энтропию, запрашивая ее у браузера, в котором мы работаем.
1console_error_panic_hook = "0.1.7"Эта библиотека (opens in a new tab) выдает более содержательные сообщения об ошибках, когда код WASM паникует и не может продолжать работу.
1[lib]2crate-type = ["cdylib", "rlib"]Тип вывода, необходимый для создания кода WASM.
lib.rs
Это собственно код на Rust.
1use wasm_bindgen::prelude::*;Определения для создания пакета WASM из Rust. Они задокументированы здесь (opens in a new tab).
1use eth_stealth_addresses::{2 generate_stealth_meta_address,3 generate_stealth_address,4 compute_stealth_key5};Функции, которые нам нужны из библиотеки eth-stealth-addresses (opens in a new tab).
1use hex::{decode,encode};Rust обычно использует для значений байтовые массивы (opens in a new tab) ([u8; <size>]). Но в JavaScript мы обычно используем шестнадцатеричные строки. Библиотека hex (opens in a new tab) переводит для нас значения из одного представления в другое.
1#[wasm_bindgen]Сгенерируйте привязки WASM, чтобы можно было вызывать эту функцию из JavaScript.
1pub fn wasm_generate_stealth_meta_address() -> String {Самый простой способ вернуть объект с несколькими полями — вернуть строку JSON.
1 let (address, spend_private_key, view_private_key) = 2 generate_stealth_meta_address();generate_stealth_meta_address (opens in a new tab) возвращает три поля:
- Метаадрес (Kpub и Vpub)
- Приватный ключ просмотра (Vpriv)
- Приватный ключ для траты средств (Kpriv)
Синтаксис кортежа (opens in a new tab) позволяет нам снова разделить эти значения.
1 format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",2 encode(address),3 encode(view_private_key),4 encode(spend_private_key)5 )6}Используйте макрос format! (opens in a new tab) для генерации строки в формате JSON. Используйте hex::encode (opens in a new tab) для преобразования массивов в шестнадцатеричные строки.
1fn str_to_array<const N: usize>(s: &str) -> Option<[u8; N]> {Эта функция преобразует шестнадцатеричную строку (предоставленную JavaScript) в байтовый массив. Мы используем ее для разбора значений, предоставленных кодом JavaScript. Эта функция сложна из-за того, как Rust обрабатывает массивы и векторы.
Выражение <const N: usize> называется обобщением (generic) (opens in a new tab). N — это параметр, который управляет длиной возвращаемого массива. Функция на самом деле вызывается как str_to_array::<n>, где n — длина массива.
Возвращаемое значение — Option<[u8; N]>, что означает, что возвращаемый массив является необязательным (opens in a new tab). Это типичный шаблон в Rust для функций, которые могут завершиться сбоем.
Например, если мы вызовем str_to_array::10("bad060a7"), функция должна вернуть массив из десяти значений, но на вход подается только четыре байта. Функция должна завершиться сбоем, и она делает это, возвращая None. Возвращаемым значением для str_to_array::4("bad060a7") будет Some<[0xba, 0xd0, 0x60, 0xa7]>.
1 // decode возвращает Result<Vec<u8>, _>2 let vec = decode(s).ok()?;Функция hex::decode (opens in a new tab) возвращает Result<Vec<u8>, FromHexError>. Тип Result (opens in a new tab) может содержать либо успешный результат (Ok(value)), либо ошибку (Err(error)).
Метод .ok() превращает Result в Option, значением которого является либо значение Ok(), если успешно, либо None, если нет. Наконец, оператор вопросительного знака (opens in a new tab) прерывает текущие функции и возвращает None, если Option пуст. В противном случае он разворачивает значение и возвращает его (в данном случае для присвоения значения vec).
Это выглядит как странно запутанный метод обработки ошибок, но Result и Option гарантируют, что все ошибки будут обработаны тем или иным способом.
1 if vec.len() != N { return None; }Если количество байтов неверно, это сбой, и мы возвращаем None.
1 // try_into поглощает vec и пытается создать [u8; N]2 let array: [u8; N] = vec.try_into().ok()?;В Rust есть два типа массивов. Массивы (opens in a new tab) имеют фиксированный размер. Векторы (opens in a new tab) могут расти и сжиматься. hex::decode возвращает вектор, но библиотека eth_stealth_addresses хочет получать массивы. .try_into() (opens in a new tab) преобразует значение в другой тип, например, вектор в массив.
1 Some(array)2}Rust не требует использования ключевого слова return (opens in a new tab) при возврате значения в конце функции.
1#[wasm_bindgen]2pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {Эта функция получает публичный метаадрес, который включает в себя как Vpub, так и Kpub. Она возвращает скрытый адрес, публичный ключ для публикации (Rpub) и однобайтовое значение сканирования, которое ускоряет идентификацию того, какие опубликованные адреса могут принадлежать Алисе.
Значение сканирования является частью общего секрета (S = GRprivVpriv). Это значение доступно Алисе, и его проверка намного быстрее, чем проверка того, равно ли f(Kpub+G*hash(S)) опубликованному адресу.
1 let (address, r_pub, scan) = 2 generate_stealth_address(&str_to_array::<66>(stealth_address)?);Мы используем generate_stealth_address (opens in a new tab) из библиотеки.
1 format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",2 encode(address),3 encode(r_pub),4 encode(&[scan])5 ).into()6}Подготовьте выходную строку в формате 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}Показать всеЭта функция использует compute_stealth_key (opens in a new tab) из библиотеки для вычисления приватного ключа для снятия средств с адреса (Rpriv). Для этого вычисления требуются следующие значения:
- Адрес (Address=f(Ppub))
- Публичный ключ, сгенерированный Биллом (Rpub)
- Приватный ключ просмотра (Vpriv)
- Приватный ключ для траты средств (Kpriv)
1#[wasm_bindgen(start)]#[wasm_bindgen(start)] (opens in a new tab) указывает, что функция выполняется при инициализации кода WASM.
1pub fn main() {2 console_error_panic_hook::set_once();3}Этот код указывает, что вывод паники должен быть отправлен в консоль JavaScript. Чтобы увидеть это в действии, используйте приложение и дайте Биллу неверный метаадрес (просто измените одну шестнадцатеричную цифру). Вы увидите эту ошибку в консоли 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: 1За этим последует трассировка стека. Затем дайте Биллу действительный метаадрес, а Алисе — либо неверный адрес, либо неверный публичный ключ. Вы увидите эту ошибку:
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Снова последует трассировка стека.
Пользовательский интерфейс
Пользовательский интерфейс написан с использованием React (opens in a new tab) и обслуживается Vite (opens in a new tab). Вы можете узнать о них из этого руководства. Здесь нет необходимости в WAGMI (opens in a new tab), потому что мы не взаимодействуем напрямую с блокчейном или кошельком.
Единственная неочевидная часть пользовательского интерфейса — это подключение WASM. Вот как это работает.
vite.config.js
Этот файл содержит конфигурацию 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})Нам нужны два плагина Vite: react (opens in a new tab) и wasm (opens in a new tab).
App.jsx
Этот файл является основным компонентом приложения. Это контейнер, который включает два компонента: Alice и Bill — пользовательские интерфейсы для этих пользователей. Актуальная часть для WASM — это код инициализации.
1import init from './rust-wasm/pkg/rust_wasm.js'Когда мы используем wasm-pack (opens in a new tab), он создает два файла, которые мы здесь используем: файл wasm с фактическим кодом (здесь src/rust-wasm/pkg/rust_wasm_bg.wasm) и файл JavaScript с определениями для его использования (здесь src/rust_wasm/pkg/rust_wasm.js). Экспорт по умолчанию этого файла JavaScript — это код, который необходимо запустить для инициации 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 )Показать всеХук useEffect (opens in a new tab) позволяет указать функцию, которая выполняется при изменении переменных состояния. Здесь список переменных состояния пуст ([]), поэтому эта функция выполняется только один раз при загрузке страницы.
Функция эффекта должна возвращаться немедленно. Чтобы использовать асинхронный код, такой как init WASM (который должен загрузить файл .wasm и, следовательно, занимает время), мы определяем внутреннюю функцию async (opens in a new tab) и запускаем ее без await.
Bill.jsx
Это пользовательский интерфейс для Билла. У него есть одно действие: создание адреса на основе скрытого метаадреса, предоставленного Алисой.
1import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'В дополнение к экспорту по умолчанию, код JavaScript, сгенерированный wasm-pack, экспортирует функцию для каждой функции в коде WASM.
1 <button onClick={() => {2 setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))3 }}>Для вызова функций WASM мы просто вызываем функцию, экспортируемую файлом JavaScript, созданным wasm-pack.
Alice.jsx
Код в Alice.jsx аналогичен, за исключением того, что у Алисы есть два действия:
- Сгенерировать метаадрес
- Получить приватный ключ для адреса, опубликованного Биллом
Заключение
Скрытые адреса не панацея; их нужно использовать правильно. Но при правильном использовании они могут обеспечить конфиденциальность в публичном блокчейне.
Больше моих работ смотрите здесь (opens in a new tab).
Последнее обновление страницы: 14 ноября 2025 г.