Мы внедрили прототип анонимных транзакций на базе zkSNARK, чтобы обеспечить конфиденциальные транзакции в блокчейне Waves. В своей реализации мы используем доказательную систему Groth16 на кривой BN254 и DSL circom.
Объясняем, как это работает.
zkSNARKs
zkSNARK – это криптографический примитив, подтверждающий знание специального набора данных (свидетельства), соответствующего множеству следующих уравнений (системы ограничений):
⟨ai,w⟩⟨bi,w⟩+⟨ci,w⟩=0
при этом часть данных свидетельства приватная. Эта конструкция позволяет доказать, например, знание прообраза хэша без разглашения прообраза. Также она может быть использована в механизме приватных транзакций для модели UTXO (Unspent Transaction (TX) Output), где публикуют только хэши UTXO, а валидность транзакции доказывают внутри zkSNARK (доказательство собственности, доказательство сохранения суммы).
zkSNARK является неинтерактивной технологией нулевого разглашения, то есть не подразумевающей протокол взаимодействия между участниками, который реализуют, чтобы доказать знание. В технологии zkSNARK доказывающий конструируют доказательство и отправляет его проверяющему – дополнительных взаимодействий не требуется. Проверяющий может проверить правильность и корректность использования данных по доказательству, не прибегая к дополнительной информации. Изначально zkSNARK-и создали как протокол “конфиденциального вычисления”: при вычислении результата данные, участвующие в расчетах, не раскрываются.
При помощи технологии zkSNARK можно реализовать схему коммит-раскрытие: доказывающий вычисляет хэш (hash), отдает его проверяющему и делает специальное доказательство, что он знает прообраз хеша x. При подстановке значений x и hash в формулу, и передаче этой формулы и доказательства проверяющему, проверяющий может удостовериться, что доказывающий знает x. На этом строятся анонимные транзакции: мы знаем приватный ключ и какой-то конкретный вход (непотраченный UTXO) с конкретной суммой, который пользователь создал на смарт-контракте. Не раскрывая этих данных, пользователь может подтвердить с помощью смарт-контракта, что это его вход, что он может им распоряжаться и отдать кому-то в пользование.
Сейчас технология не используется повсеместно, потому что пруф генерируется несколько минут, что не очень удобно для пользователя.
Узнайте больше о программировании zkSNARK-ов в статье Виталика Бутерина "Quadratic Arithmetic Programs: from Zero to Hero" и в статье Iden3 про разработку схем на circom.
Аккаунтная модель
Для транзакций в Waves обычно используют ключи и адреса на базе кривой curve25519
. Эта кривая не является zkSNARK-friendly, поэтому для анонимных транзакций используем подгруппу скрученных кривых Эдвардса BabyJubJub
. Кроме того, мы используем публичные ключи в качестве адресов, поскольку при отправке нужно шифровать данные для получателя.
UTXO-модель
В нашей модели UTXO представлен набором из 3 параметров: баланс, публичный ключ владельца и уникальный секрет. В блокчейне представлены только хэши без дополнительного шифрования. Владелец представлен публичным ключом, и, как отмечалось ранее, мы используем публичные ключи не на кривой curve25519
, а на zkSNARK-friendly кривой BabyJubJub
. Id UTXO должен быть сгенерирован случайным образом, так как если пользователь укажет два одинаковых id, то сможет забрать (потратить) UTXO только по одному из них. При этом заблокированными окажутся только UTXO с соответствующим id для текущего пользователя, но не для остальных. В интересах пользователя выбирать id, используя генератор случайных чисел (на id выделяется 253 битах, поэтому получить коллизию сложно).
Чтобы потратить UTXO, необходимо опубликовать в цепи обнулитель (nullifier) – детерминированную функцию от UTXO, определенную как hash(secret, owner_privkey). Это значение детерминировано и уникально для каждого UTXO, его знает только владелец. Кроме владельца, никто не может связать UTXO с соответствующим ему обнулителем.
UTXO хранятся внутри хэш-мапы dApp, то есть в стейте контракта. В блокчейне UTXO представлены в зашифрованном виде. Чтобы забрать свои средства, пользователь должен просканировать блокчейн и попытаться расшифровать каждый UTXO.
Стейт dApp-а
В стейте dApp-а хранятся hash-map-ы, представляющие два множества:
- хэши UTXO
- обнулители
Таким образом, dApp может проверить существование UTXO анонимного множества и уникальность обнулителей. Этого достаточно, чтобы обрабатывать анонимные переводы с защитой от подделки новых ассетов и двойного расходования.
В dApp – 3 метода, соответствующие базовым типам транзакций:
- Депозит
- Трансфер
- Вывод
Для перевода и вывода средств используем собственные верификаторы, обеспечивающие взаимодействие dApp со специальными анонимными аккаунтами, основанными на кривой BabyJubJub. Депозиты обрабатываются с обычных аккаунтов Waves.
Комиссии
За пополнение счета комиссия взимается с curve25519
аккаунта. За переводы и снятие средств комиссия снимается с анонимного счета. На уровне dApp это выглядит так:
dApp оплачивает транзакцию сам, то есть с его баланса списывается нативный токен, который тратится на оплату комиссии
Между входами и выходами часть комиссии сжигается, чтобы аннулировать активы, соответствующие реальным активам на смарт-контракте
На уровне UTXO сжигаем некоторую сумму в качестве комиссии при обработке транзакции.
Транзакции
Депозит является простой операцией, каждый депозит добавляет новый элемент в UTXO.
Трансферы основываются на примитиве перевода 2-to-2.
Некоторые входы и выходы могут быть нулевыми. В качестве частичного примера такой конструкции может быть представлен любой вид простого перевода (join, split и другие переводы, за исключением атомик свопов).
Выводы работают как и другие транзакции, только вместо создания второго UTXO позволяют пользователю выводить свои UTXO из dApp. На входе также может быть два UTXO, на выходе UTXO с остатком средств и withdraw, которые публикуются в блокчейне.
При выводе или трансфере dApp проверяет, что соответствующие обнулители еще не встречались в его стейте.
При помощи zkSNARK мы можем посчитать, что сумма входов транзакции равна сумме выходов. При исполнении транзакции мы тратим её UTXO и еще какой-то нулевой UTXO, которого нет в merkle-дереве. Чтобы потратить UTXO, нужно доказать знание приватного ключа его владельца. Проверяем, что приватный ключ при умножении на генератор группы дает в результате публичный ключ. Соответственно, не зная приватный ключ транзакцию совершить нельзя.
Анонимное множество
Используем анонимное множество из 8 элементов. Целевой элемент приватно выбирается из множества, представленного в публичном входе zkSNARK. Этот метод позволяет обфусцировать граф транзакций (если существует возможность восстановления графа взаимодействия UTXO, то существует возможность деанонимизации транзакций).
В дальнейшем, простой сборщик 8 элементов можно заменить на сборщик Merkle-деревьев. Подход скрывает граф транзакций.
Чтобы создать валидную транзакцию, доказываем, что тратим какой-то UTXO из множества UTXO. Помещаем в приватные входы zkSNARK merkle proof-ы и данные UTXO, а в публичный вход – root hash. Таким образом, при помощи SNARK-а доказываем, что знаем UTXO.
Защита от двойного расходования
Чтобы защититься от двойного расходования, используем обнулители – детерминированные функции, не зависящие напрямую от хэша UTXO. Для вычисления обнулителя берем функцию от приватного ключа, доказано соответствующего публичному ключу, id и хэшу UTXO. Для каждой UTXO существует только один обнулитель.
Для каждой транзакции обнулитель потраченных UTXO выходов должен быть представлен в качестве публичного входа для zkSNARK. Внутри zkSNARK circuit также должно быть подтверждено, что он принадлежит к потраченным UTXO.
Если контракт dApp получает не уникальное значение обнулителя, транзакция отклоняется. Таким образом, гарантируется, что каждый UTXO тратится по одному разу.
После того, как потратили UTXO, обнулитель публикуется в списке потраченных обнулителей в стейте dApp. То есть в hash-map напротив данного обнулителя ставится "true". Перед публикацией обнулителя в стейте проверяем, что данный обнулитель еще не использован, а значит UTXO с таким id можно тратить.