Использование скрытых адресов
Вы — Билл. По причинам, в которые мы не будем вдаваться, вы хотите сделать пожертвование на кампанию «Алису в королевы мира» и хотите, чтобы Алиса знала о вашем пожертвовании, чтобы она могла дать вам вознаграждение в случае победы. К сожалению, ее победа не гарантирована. Существует конкурирующая кампания — «Кэрол в императрицы Солнечной системы». Если Кэрол победит и узнает, что вы пожертвовали Алисе, у вас будут неприятности. Поэтому вы не можете просто перевести 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 аналогичен, за исключением того, что у Алисы есть два действия:
- Сгенерировать мета-адрес
- Получить приватный ключ для адреса, опубликованного Биллом
Заключение
Скрытые адреса — не панацея; их нужно использовать правильно. Но при правильном использовании они могут обеспечить приватность в публичном блокчейне.
Здесь вы можете найти больше моих работ (opens in a new tab).