Технический › Кто получатель?

Одноразовые адреса

Stealth-адреса: как отправить деньги на адрес, не записывая его в блокчейн

Коротко

Отправитель берёт случайный r, публикует R = r·G и отправляет деньги на одноразовый ключ P = Hs(8·r·A ‖ i)·G + B. Получатель находит его, вычисляя то же самое через a: Hs(8·a·R ‖ i)·G + B. Тратит его приватным ключом x = Hs(8·a·R ‖ i) + b.

Задача

Мы хотим, чтобы:

  1. в блокчейне не было ни адреса получателя, ни чего-либо, что повторяется между платежами одному человеку;
  2. получатель мог сам найти свои выходы, не общаясь с отправителем;
  3. потратить выход мог только получатель.

Решение — обмен ключами Диффи–Хеллмана, встроенный в каждую транзакцию.

Шаг за шагом

Отправитель Получатель r (случайный) R = r·G знает адрес: A, B общий секрет: 8·r·A P = Hs(8rA ‖ i)·G + B в блокчейн: R и P (и больше ничего об адресе) a, b (свои ключи) видит R в транзакции общий секрет: 8·a·R = 8·a·r·G = 8·r·A проверяет: Hs(8aR ‖ i)·G + B == P ?

1. Отправитель выбирает случайный r

Для каждой транзакции — новый случайный r (transaction private key). Его публичная версия R = r·G (transaction public key) записывается в поле tx_extra транзакции.

2. Общий секрет

Отправитель вычисляет 8·r·A (A — публичный ключ просмотра из адреса). Получатель позже вычислит 8·a·R. Это одна и та же точка, потому что a·R = a·r·G = r·a·G = r·A. В коде Monero она называется key derivation.

Множитель 8 (кофактор) нужен, чтобы «очистить» точку от возможной компоненты малого порядка: даже если кто-то подсунет злонамеренную R, результат окажется в правильной подгруппе.

3. Одноразовый ключ

P = Hs( 8·r·A ‖ i )·G + B i — номер выхода в транзакции (varint), чтобы у двух выходов одной транзакции были разные ключи

Это и есть одноразовый адрес (output public key). Он выглядит как случайная точка: без a или r его не с чем сравнить.

4. Получатель сканирует

Для каждой новой транзакции и каждого выхода i кошелёк вычисляет Hs(8·a·R ‖ i)·G + B и сравнивает с P. Совпало — выход наш. Для этого нужен только ключ просмотра a и публичный B — поэтому view-only кошелёк может находить входящие.

5. Получатель тратит

Приватный ключ одноразового выхода:

x = Hs( 8·a·R ‖ i ) + b проверка: x·G = Hs(…)·G + b·G = Hs(…)·G + B = P ✓

Для этого нужен ключ траты b. Ключом x подписывается трата, и из него же вычисляется key image.

Сумма тоже шифруется общим секретом

Из того же общего секрета выводится маска для суммы. Сумма выхода (8 байт) хранится как amount ⊕ Keccak("amount" ‖ Hs(8rA ‖ i))[0..8], а «ослепляющий множитель» обязательства — как Hs("commitment_mask" ‖ Hs(8rA ‖ i)). Получатель вычисляет то же самое и расшифровывает сумму. Подробнее — в обязательствах Педерсена.

Слабые места

  • Сканирование дорогое: для каждого выхода каждой транзакции — умножение точки на скаляр. Отчасти лечится view tags.
  • Janus-атака: отправитель, подозревающий, что два подадреса принадлежат одному человеку, может собрать выход из частей разных подадресов и посмотреть, «заметит» ли получатель платёж. Защита в кошельках есть частично; полностью закрывает её Carrot.
  • Отправитель знает r и может доказать третьей стороне, что заплатил на определённый адрес (это иногда полезно — «payment proof»).