Використання прихованих адрес
Ви — Білл. З причин, у які ми не будемо заглиблюватися, ви хочете зробити пожертву на кампанію «Алісу в королеви світу» і хочете, щоб Аліса знала про вашу пожертву, щоб вона могла дати вам винагороду в разі перемоги. На жаль, її перемога не гарантована. Існує конкуруюча кампанія — «Керол в імператриці Сонячної системи». Якщо Керол переможе і дізнається, що ви пожертвували Алісі, у вас будуть неприємності. Тому ви не можете просто переказати 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. Загалом криптографія з відкритим ключем в Етеріумі працює так: ви можете використовувати приватний ключ, 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) |
| Address=f(Ppub) | - | Address=f(Ppub) | Address=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.
git clone https://github.com/qbzzt/251022-stealth-addresses.git cd 251022-stealth-addresses -
Встановіть необхідні компоненти та скомпілюйте код Rust.
cd src/rust-wasm rustup target add wasm32-unknown-unknown cargo install wasm-pack wasm-pack build --target web -
Запустіть вебсервер.
cd ../.. npm install npm 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. Він містить інформацію про пакет, оголошення залежностей тощо.
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2024"
[dependencies]
eth-stealth-addresses = "0.1.0"
hex = "0.4.3"
wasm-bindgen = "0.2.104"
getrandom = { version = "0.2", features = ["js"] }
Пакету getrandom (opens in a new tab) потрібно генерувати випадкові значення. Це неможливо зробити суто алгоритмічними засобами; це вимагає доступу до фізичного процесу як джерела ентропії. Це визначення вказує, що ми отримаємо цю ентропію, звернувшись до браузера, у якому ми працюємо.
console_error_panic_hook = "0.1.7"
Ця бібліотека (opens in a new tab) дає нам більш змістовні повідомлення про помилки, коли код WASM панікує і не може продовжувати роботу.
[lib]
crate-type = ["cdylib", "rlib"]
Тип виводу, необхідний для створення коду WASM.
lib.rs
Це власне код Rust.
use wasm_bindgen::prelude::*;
Визначення для створення пакета WASM з Rust. Вони задокументовані тут (opens in a new tab).
use eth_stealth_addresses::{
generate_stealth_meta_address,
generate_stealth_address,
compute_stealth_key
};
Функції, які нам потрібні з бібліотеки eth-stealth-addresses (opens in a new tab).
use hex::{decode,encode};
Rust зазвичай використовує байтові масиви (opens in a new tab) ([u8; <size>]) для значень. Але в JavaScript ми зазвичай використовуємо шістнадцяткові рядки. Бібліотека hex (opens in a new tab) перетворює для нас одне представлення в інше.
#[wasm_bindgen]
Генерує прив'язки WASM, щоб мати можливість викликати цю функцію з JavaScript.
pub fn wasm_generate_stealth_meta_address() -> String {
Найпростіший спосіб повернути об'єкт із кількома полями — повернути рядок JSON.
let (address, spend_private_key, view_private_key) =
generate_stealth_meta_address();
generate_stealth_meta_address (opens in a new tab) повертає три поля:
- Мета-адресу (Kpub та Vpub)
- Приватний ключ для перегляду (Vpriv)
- Приватний ключ для витрачання (Kpriv)
Синтаксис кортежу (opens in a new tab) дозволяє нам знову розділити ці значення.
format!("{{\"address\":\"{}\",\"view_private_key\":\"{}\",\"spend_private_key\":\"{}\"}}",
encode(address),
encode(view_private_key),
encode(spend_private_key)
)
}
Використовуйте макрос format! (opens in a new tab) для генерації рядка у форматі JSON. Використовуйте hex::encode (opens in a new tab) для перетворення масивів у шістнадцяткові рядки.
fn 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]>.
// decode повертає Result<Vec<u8>, _>
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 гарантують, що всі помилки будуть оброблені так чи інакше.
if vec.len() != N { return None; }
Якщо кількість байтів неправильна, це помилка, і ми повертаємо None.
// try_into споживає vec і намагається створити [u8; N]
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) перетворює значення в інший тип, наприклад, вектор у масив.
Some(array)
}
Rust не вимагає використання ключового слова return (opens in a new tab) під час повернення значення в кінці функції.
#[wasm_bindgen]
pub fn wasm_generate_stealth_address(stealth_address: &str) -> Option<String> {
Ця функція отримує відкриту мета-адресу, яка включає як Vpub, так і Kpub. Вона повертає приховану адресу, відкритий ключ для публікації (Rpub) та однобайтове значення сканування, яке прискорює ідентифікацію того, які з опублікованих адрес можуть належати Алісі.
Значення сканування є частиною спільного секрету (S = GRprivVpriv). Це значення доступне Алісі, і його перевірка набагато швидша, ніж перевірка того, чи дорівнює f(Kpub+G*hash(S)) опублікованій адресі.
let (address, r_pub, scan) =
generate_stealth_address(&str_to_array::<66>(stealth_address)?);
Ми використовуємо generate_stealth_address (opens in a new tab) з бібліотеки.
format!("{{\"address\":\"{}\",\"rPub\":\"{}\",\"scan\":\"{}\"}}",
encode(address),
encode(r_pub),
encode(&[scan])
).into()
}
Підготуйте вихідний рядок у форматі JSON.
#[wasm_bindgen]
pub fn wasm_compute_stealth_key(
address: &str,
bill_pub_key: &str,
view_private_key: &str,
spend_private_key: &str
) -> Option<String> {
.
.
.
}
Ця функція використовує compute_stealth_key (opens in a new tab) з бібліотеки для обчислення приватного ключа для зняття коштів з адреси (Rpriv). Це обчислення вимагає таких значень:
- Адреса (Address=f(Ppub))
- Відкритий ключ, згенерований Біллом (Rpub)
- Приватний ключ для перегляду (Vpriv)
- Приватний ключ для витрачання (Kpriv)
#[wasm_bindgen(start)]
#[wasm_bindgen(start)] (opens in a new tab) вказує, що функція виконується під час ініціалізації коду WASM.
pub fn main() {
console_error_panic_hook::set_once();
}
Цей код вказує, що вивід паніки має надсилатися до консолі JavaScript. Щоб побачити це в дії, скористайтеся застосунком і дайте Біллу недійсну мета-адресу (просто змініть одну шістнадцяткову цифру). Ви побачите цю помилку в консолі JavaScript:
rust_wasm.js:236 panicked at /home/ori/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/subtle-2.6.1/src/lib.rs:701:9:
assertion `left == right` failed
left: 0
right: 1
За яким слідує трасування стека. Потім дайте Біллу дійсну мета-адресу, а Алісі — або недійсну адресу, або недійсний відкритий ключ. Ви побачите цю помилку:
rust_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:
keys 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).
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import wasm from "vite-plugin-wasm";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), wasm()],
})
Нам потрібні два плагіни Vite: react (opens in a new tab) та wasm (opens in a new tab).
App.jsx
Цей файл є головним компонентом застосунку. Це контейнер, який включає два компоненти: Alice та Bill, інтерфейси для цих користувачів. Відповідною частиною для WASM є код ініціалізації.
import 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.
function App() {
.
.
.
useEffect(() => {
const loadWasm = async () => {
try {
await init();
setWasmReady(true)
} catch (err) {
console.error('Error loading wasm:', err)
alert("Wasm error: " + err)
}
}
loadWasm()
}, []
)
Хук useEffect (opens in a new tab) дозволяє вказати функцію, яка виконується під час зміни змінних стану. Тут список змінних стану порожній ([]), тому ця функція виконується лише один раз під час завантаження сторінки.
Функція ефекту має повернути результат негайно. Щоб використовувати асинхронний код, такий як init у WASM (який має завантажити файл .wasm і тому потребує часу), ми визначаємо внутрішню функцію async (opens in a new tab) і запускаємо її без await.
Bill.jsx
Це інтерфейс користувача для Білла. Він має єдину дію — створення адреси на основі прихованої мета-адреси, наданої Алісою.
import { wasm_generate_stealth_address } from './rust-wasm/pkg/rust_wasm.js'
Окрім експорту за замовчуванням, код JavaScript, згенерований wasm-pack, експортує функцію для кожної функції в коді WASM.
<button onClick={() => {
setPublicAddress(JSON.parse(wasm_generate_stealth_address(stealthMetaAddress)))
}}>
Щоб викликати функції WASM, ми просто викликаємо функцію, експортовану файлом JavaScript, створеним wasm-pack.
Alice.jsx
Код у Alice.jsx аналогічний, за винятком того, що Аліса має дві дії:
- Згенерувати мета-адресу
- Отримати приватний ключ для адреси, опублікованої Біллом
Висновок
Приховані адреси не є панацеєю; їх потрібно використовувати правильно. Але за умови правильного використання вони можуть забезпечити приватність у публічному блокчейні.