Приветствуем! Впервые на сайте Zcash?
The Zcash network is young, but evolving quickly! Sign up and we'll be in touch with monthly highlights on ecosystem growth, network development and how to get started with Zcash!

Язык

Транзакции между скрытыми адресами

Ariel Gabizon | Nov 29, 2016

В предыдущей статье, которая называется`'Структура транзакций Zcash' </blog/anatomy-of-zcash.html>`_, мы в общих чертах рассмотрели схему работы транзакций Zcash. Это сделано для того, чтобы у вас было примерное понимание о механизме защиты частной информации при проведении транзакций Zcash и о том, какое место в этом процессе занимает доказательство с нулевым разглашением. Эта статья полностью посвящена осуществлению транзакций между закрытыми адресами (которые также называют z-addrs).

Чтобы сфокусироваться на аспекте сохранения конфиденциальности, давайте пока отложим в сторону все, что связано с установлением согласованности при помощи доказательства работы и блокчейна и сконцентрируем внимание на конкретном узле, который правильно вычислил список непотраченных исходящих транзакций.

Для начала давайте вспомним, как этот список выглядит в блокчейне биткоина. Каждый неизрасходованный «выход» транзакции (UTXO) можно представить в виде 'банкноты', которая имеет некоторые характеристики: адрес/открытый ключ своего обладателя и количество BTC, которое ей соответствует. Для наглядности, давайте представим, что каждая такая банкнота равна 1 BTC, а каждый адрес содержит не более одной такой банкноты. Таким образом, в каждый данный момент времени база данных узла состоит из списка неизрасходованных банкнот, каждая из которых может быть описана адресом ее владельца. Например, база данных может выглядеть так.

\(\mathsf{Note}_1=\) \((\mathsf{PK}_1)\), \(\mathsf{Note}_2=\) \((\mathsf{PK}_2)\), \(\mathsf{Note}_3=\) \((\mathsf{PK}_3)\)

Предположим, \(\mathsf{PK}_1\) является адресом Алисы, и она хочет отправить 1 BTC на адрес Боба \(\mathsf{PK}_4.\) На все узлы она отправляет сообщение следующего содержания: "Переместить 1 BTC с адреса \(\mathsf{PK}_1\) на адрес \(\mathsf{PK}_4\)". Она подписывает сообщение закрытым ключом \(\mathsf{sk}_1\), который соответствует \(\mathsf{PK}_1,\) и таким образом убеждает узел, что она имеет право на перемещение денег с адреса \(\mathsf{PK}_1.\) Как только узел проверит достоверность подписи и наличие 1 BTC на адресе \(\mathsf{PK}_1,\) его база данных обновится и будет выглядеть следующим образом:

\(\mathsf{Note}_4=\) \((\mathsf{PK}_4)\), \(\mathsf{Note}_2=\) \((\mathsf{PK}_2)\), \(\mathsf{Note}_3=\) \((\mathsf{PK}_3)\)

Теперь давайте представим, что у каждой банкноты также есть рандомный 'серийный номер' (типа уникального индекса) \(r.\) Вскоре вы увидите, какую важную роль он играет в защите конфиденциальности. Таким образом, база данных может выглядеть так:

\(\mathsf{Note}_1=\) \((\) \(\mathsf{PK}_1\) \(,\) \(r_1)\), \(\mathsf{Note}_2=\) \((\) \(\mathsf{PK}_2\) \(,\) \(r_2)\), \(\mathsf{Note}_3=\) \((\) \(\mathsf{PK}_3\) \(,\) \(r_3)\)

Очевидно, что для сохранения конфиденциальности важно чтобы на узле хранились только "зашифрованные данные", или хэши, банкнот, а не сами банкноты.

\(\mathsf{H}_1=\) \(\mathbf{HASH}(\mathsf{Note}_1)\), \(\mathsf{H}_2=\) \(\mathbf{HASH}(\mathsf{Note}_2)\), \(\mathsf{H}_3=\) \(\mathbf{HASH}(\mathsf{Note}_3)\)

Не менее важно и то, что хэш банкноты остается в базе данных узла даже после того, как она израсходована. Таким образом, создается база данных не просто неизрасходованных банкнот, а всех банкнот, которые когда-либо существовали.

На этом этапе главная задача отличить израсходованные банкноты от неизрасходованных, при этом не нарушая конфиденциальности пользователей. И здесь самое время поговорить об обнуляторах. Это ни что иное, как список хеш-кодов серийных номеров уже израсходованных банкнот. На каждом узле кроме хешей банкнот хранится и обнулятор. Например, после того как банкнота \(\mathsf{Note}_2\) будет потрачена, в базе данных это будет выглядеть так:

Хеш банкноты Обнулятор
H1=| \(\mathbf{HASH}(\mathsf{Note}_1)\) \(\mathsf{nf}_1=\) \(\mathbf{HASH}(\mathsf{r}_2)\)
\(\mathsf{H}_2=\) \(\mathbf{HASH}(\mathsf{Note}_2)\)  
\(\mathsf{H}_3=\) \(\mathbf{HASH}(\mathsf{Note}_3)\)  

Как происходит транзакция

Теперь предположим, что у Алисы есть \(\mathsf{Note}_1\) и она хочет отправить это Бобу, открытый ключ которого \(\mathsf{PK}_4.\) Хоть для Zcash это не является принципиально важным условием, но для наглядности мы представим, что Алиса и Боб используют частный канал. По сути, Алиса сделает свою банкноту недействительной, опубликовав ее обнулятор, и в то же самое время создаст новую действующую банкноту, которая будет принадлежать Бобу.

Если быть точнее, она выполняет следующие действия.

  1. Она выбирает новый рандомный серийный номер \(r_4\) и определяет новую банкноту как \(\mathsf{Note}_4=\) \((\) \(\mathsf{PK}_4\) \(,\) \(r_4).\)
  2. Она лично посылает \(\mathsf{Note}_4\) Бобу.
  3. Далее на все узлы она посылает обнулятор \(\mathsf{Note}_1,\) \(\mathsf{nf}_2=\) \(\mathbf{HASH}(\mathsf{r}_1)\)
  4. И затем она отправляет хеш новой банкноты \(\mathsf{H}_4=\) \(\mathbf{HASH}(\mathsf{Note}_4)\) на все узлы.

Теперь, когда узел получил \(\mathsf{nf}_2\) и \(\mathsf{H}_4,\) он начинает проверять, была ли банкнота соответствующая \(\mathsf{nf}_2\) уже потрачена. Это делается очень просто: узел проверяет, существует ли соответствующий обнулятор. Если он не существует, то узел добавляет \(\mathsf{nf}_2\) в список обнуляторов и добавляет \(\mathsf{H}_4\) в список хешированных банкнот, тем самым подтверждая транзакцию между Алисой и Бобом.

Хеш банкноты Обнулятор
\(\mathsf{H}_1=\) \(\mathbf{HASH}(\mathsf{Note}_1)\) \(\mathsf{nf}_1=\) \(\mathbf{HASH}(\mathsf{r}_2)\)
\(\mathsf{H}_2=\) \(\mathbf{HASH}(\mathsf{Note}_2)\) \(\mathsf{nf}_2=\) \(\mathbf{HASH}(\mathsf{r}_1)\)
\(\mathsf{H}_3=\) \(\mathbf{HASH}(\mathsf{Note}_3)\)  
\(\mathsf{H}_4=\) \(\mathbf{HASH}(\mathsf{Note}_4)\)  

...но секундочку, мы же проверили и узнали, что \(\mathsf{Note}_1\) не была потрачена ранее... но мы не проверили, принадлежит ли она Алисе. На самом деле, мы не проверили, является ли она вообще 'настоящей', а точнее, содержится ли ее хеш в хеш-таблице узла. Чтобы решить эту проблему простым способом, Алисе нужно вместо хеша опубликовать \(\mathsf{Note}_1,\) но тогда она не сможет сохранить конфиденциальность.

И тут на помощь спешит алгоритм доказательства с нулевым разглашением:

В дополнение к перечисленным действиям Алисе нужно опубликовать \(\pi\) доказательство, убеждающее узлы, что кем бы ни были опубликованы данные об этой транзакции \(\mathsf{PK}_1,\) \(\mathsf{sk}_1,\) и \(r_1\) при них

  1. Хеш-код банкноты \(\mathsf{Note}_1=\) (\(\mathsf{PK}_1,\) \(r_1)\) находится в списке хешированных банкнот.
  2. \(\mathsf{sk}_1\) является закрытым ключом, соответствующим \(\mathsf{PK}_1\) (таким образом тот, кто знает его, является полноправным обладателем \(\mathsf{Note}_1)\).
  3. Хеш \(r_1\) является \(\mathsf{nf}_2\) (следовательно, если \(\mathsf{nf}_2\) - что, как мы знаем, является обнулятором \(\mathsf{Note}_1\) - не содержится в списке обнуляторов, \(\mathsf{Note}_1\) остается неизрасходованной).

По своим свойствам доказательство с нулевым разглашением обеспечивает, что \(\pi\) не раскроет никакой информации о \(\mathsf{PK}_1,\) \(\mathsf{sk}_1,\) или \(r_1\).

Основные моменты, где мы упростили содержание и опустили детали

Хотелось бы отметить, что мы очень сильно упростили описание этого процесса, и рекомендуем ознакомиться со спецификацией к протоколу, где все подробно описано.

Вот основные моменты, которые мы опустили:

  1. Хешированные банкноты должны храниться не просто в виде списка, а в виде дерева Меркла. Это играет огромную роль для эффективной работы доказательств с нулевым разглашением. Более того, необходимо хранить  скрытое вычисление и связывающее обязательство банкноты, а не просто ее хеш-код.
  2. Обнулятор должен быть вычислен более сложным путем, нежели описанным нами, чтобы обеспечить конфиденциальность получателя по отношению к отправителю.
  3. Мы не описали, как отбросить необходимость в частном канале между отправителем и получателем.