Використання стелс-адрес
Ви — Білл. З причин, які ми не будемо розглядати, ви хочете зробити пожертву на кампанію "Аліса за королеву світу" і хочете, щоб Аліса знала, що ви зробили пожертву, щоб вона винагородила вас, якщо переможе. На жаль, її перемога не гарантована. Існує конкуруюча кампанія, "Керол за імператрицю Сонячної системи". Якщо Керол переможе і дізнається, що ви зробили пожертву Алісі, у вас будуть проблеми. Тож ви не можете просто переказати 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+G*hash(S) = Kpub+G*hash(S).
Ми маємо окремий ключ перегляду, щоб дозволити Алісі передати субпідряд компанії "Служби світового панування Дейва". Аліса готова повідомити Дейву публічні адреси та інформувати її, коли з’являться додаткові гроші, але вона не хоче, щоб він витрачав гроші її кампанії.
Оскільки для перегляду та витрачання використовуються окремі ключі, Аліса може дати Дейву 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) | |
| Адреса=f(Ppub) | — | Адреса=f(Ppub) | Адреса=f(Ppub) | Адреса=f(Ppub) |
| Ppriv = Kpriv+hash(S) | — | — | — |
Коли зі стелс-адресами щось іде не так
На блокчейні немає секретів. Хоча стелс-адреси можуть забезпечити вам конфіденційність, ця конфіденційність вразлива до аналізу трафіку. Щоб навести тривіальний приклад, уявіть, що Білл поповнює адресу і негайно надсилає транзакцію для публікації значення Rpub. Без Vpriv Аліси ми не можемо бути впевнені, що це стелс-адреса, але варто робити ставку саме на це. Потім ми бачимо іншу транзакцію, яка переказує всі ETH з цієї адреси на адресу фонду кампанії Аліси. Можливо, ми не зможемо цього довести, але, ймовірно, Білл щойно зробив пожертву на кампанію Аліси. Керол, безперечно, так би й подумала.
Біллу легко відокремити публікацію Rpub від поповнення стелс-адреси (робити це в різний час, з різних адрес). Однак цього недостатньо. Шаблон, який шукає Керол, полягає в тому, що Білл поповнює адресу, а потім фонд кампанії Аліси виводить з неї кошти.
Одне з рішень полягає в тому, щоб кампанія Аліси не виводила гроші напряму, а використовувала їх для оплати третій стороні. Якщо кампанія Аліси надсилає 10 ETH до "Служб світового панування Дейва", Керол знає лише, що Білл зробив пожертву одному з клієнтів Дейва. Якщо у Дейва достатньо клієнтів, Керол не зможе дізнатися, чи зробив Білл пожертву Алісі, яка з нею конкурує, чи Адаму, Альберту чи Ебігейл, до яких Керол байдужа. Аліса може включити хешоване значення в платіж, а потім надати Дейву прообраз, щоб довести, що це була її пожертва. Як варіант, як зазначалося вище, якщо Аліса дасть Дейву свій 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> називається дженеріком (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). Для цього обчислення потрібні такі значення:
- Адреса (Адреса=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('Помилка завантаження wasm:', err)12 alert("Помилка Wasm: " + 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 р.