Магия доказательств с нулевым разглашением через исходный код Tornado Cash
27 февраля 2022 г.Согласно [Википедии] (https://en.wikipedia.org/wiki/Zero-knowledge_proof), определение доказательства с нулевым разглашением (ZKP) выглядит следующим образом:
… доказательство с нулевым разглашением или протокол с нулевым разглашением — это метод, с помощью которого одна сторона (доказывающая сторона) может доказать другой стороне (проверяющей стороне), что данное утверждение истинно, в то время как доказывающая сторона избегает передачи какой-либо дополнительной информации, кроме того факта, что утверждение действительно верно. Суть доказательств с нулевым разглашением заключается в том, что доказать, что кто-то обладает знанием определенной информации, тривиально, просто раскрыв ее; задача состоит в том, чтобы доказать такое владение, не раскрывая ни самой информации, ни какой-либо дополнительной информации.
Технология ZKP может широко использоваться во многих различных областях, таких как анонимное голосование или анонимный перевод денег, которые трудно решить в общедоступной базе данных, такой как блокчейн.
[Tornado Cash] (https://tornado.cash/) — это миксер монет, который вы можете использовать для анонимизации своих транзакций Ethereum. Из-за логики блокчейна каждая транзакция является общедоступной. Если у вас есть немного ETH на вашем счету, вы не можете перевести его анонимно, потому что любой может следить за историей ваших транзакций в блокчейне. Смесители монет, такие как Tornado Cash, могут решить эту проблему конфиденциальности, разорвав цепочку между источником и адресом назначения с помощью ZKP.
Если вы хотите анонимизировать одну из своих транзакций, вам необходимо внести небольшую сумму ETH (или токена ERC20) в контракт Tornado Cash (например: 1 ETH). Через некоторое время вы можете снять этот 1 ETH с другого счета. Хитрость в том, что никто не может создать связь между счетом вкладчика и счетом снятия. Если сотни аккаунтов вносят 1 ETH с одной стороны, а другие сотни аккаунтов снимают 1 ETH с другой стороны, то никто не сможет проследить путь, по которому движутся деньги. Техническая проблема заключается в том, что транзакции смарт-контрактов также являются общедоступными, как и любые другие транзакции в сети Ethereum. Это тот момент, когда ZKP будет актуален.
Когда вы вносите свой 1 ETH на контракт, вы должны предоставить «обязательство». Это обязательство хранится в смарт-контракте. Когда вы снимаете 1 ETH на другой стороне, вы должны предоставить «нуллификатор» и доказательство с нулевым разглашением. Нульификатор — это уникальный идентификатор, который связан с обязательством, и ZKP подтверждает связь, но никто не знает, какой нуллификатор назначен какому обязательству (кроме владельца счета ввода/вывода).
Опять же: мы можем доказать, что одно из обязательств назначено нашему обнулителю, не раскрывая нашего обязательства.
Обнулители отслеживаются смарт-контрактом, поэтому мы можем вывести только один депонированный ETH с одним обнулителем.
Звучит легко? Это не! :) Давайте углубимся в технологию. Но прежде всего мы должны понять еще одну хитрую вещь, [дерево Меркла] (https://en.wikipedia.org/wiki/Merkle_tree).
Деревья Меркла — это хеш-деревья, где листья — это элементы, а каждый узел — это хэш дочерних узлов. Корнем дерева является корень Меркла, представляющий весь набор элементов. Если вы добавите, удалите или измените любой элемент (лист) в дереве, корень Меркла изменится. Корень Меркла — это уникальный идентификатор набора элементов. Но как мы можем его использовать?
Есть еще одна вещь, называемая доказательством Меркла. Если у меня есть корень Меркла, вы можете прислать мне доказательство Меркла, доказывающее, что элемент находится в множестве, представленном корнем. На рисунке ниже показано, как это работает. Если вы хотите доказать мне, что HK входит в набор, вы должны отправить мне хэши HL, HIJ, HMNOP и HABCDEFGH. Используя эти хэши, я могу вычислить корень Меркла. Если рут такой же как у меня рут то HK в наборе. Где мы можем его использовать?
Простой пример — белый список. Представьте себе смарт-контракт с методом, который могут вызывать только пользователи из белого списка. Проблема в том, что в белом списке 1000 учетных записей. Как вы можете хранить их в смарт-контракте? Самый простой способ — сохранить каждую учетную запись в сопоставлении, но это очень дорого. Более дешевое решение — построить дерево Меркла и сохранить только корень Меркла (1 хэш против 1000 — неплохо). Если кто-то хочет вызвать метод, он должен предоставить доказательство Меркла (в данном случае это список из 10 хэшей), которое можно легко проверить с помощью смарт-контракта.
Еще раз: дерево Меркла используется для представления набора элементов с одним хэшем (корень Меркла). Существование элемента может быть доказано доказательством Меркла.
Следующее, что нам нужно понять, это само доказательство с нулевым разглашением. С ZKP вы можете доказать, что вы что-то знаете, не раскрывая то, что вы знаете. Для генерации ЗКП нужна схема. Схема — это что-то вроде небольшой программы, которая имеет общие входы и выходы, а также частные входы. Эти частные входные данные являются знаниями, которые вы не раскрываете для проверки, поэтому это называется доказательством с нулевым разглашением. С ZKP мы можем доказать, что выход может быть сгенерирован из входов с данной схемой.
Простая схема выглядит так:
```javascript
прагма цирком 2.0.0;
включить "node_modules/circomlib/схемы/bitify.circom";
включить "node_modules/circomlib/схемы/pedersen.circom";
шаблон Основной () {
обнулитель входного сигнала;
вывод сигнала nullifierHash;
компонент nullifierHasher = Pedersen(248);
компонент nullifierBits = Num2Bits(248);
nullifierBits.in <== nullifier;
для (вар я = 0; я < 248; я ++) {
nullifierHasher.in[i] <== nullifierBits.out[i];
nullifierHash <== nullifierHasher.out[0];
компонент main = Main();
Используя эту схему, мы можем доказать, что знаем источник данного хеша. Эта схема имеет один вход (обнулитель) и один выход (хеш-обнулитель). Доступность входов по умолчанию закрыта, а выходы всегда общедоступны. Эта схема использует 2 библиотеки из пакета Circomlib. Circomlib — это набор полезных схем. Первая библиотека — это bitlify, которая содержит методы обработки битов, а вторая — pedersen, которая содержит хэш-код Педерсена. Хеширование Педерсена — это метод хеширования, который можно эффективно использовать в схемах ZKP. В теле шаблона Main заполняем хэш и вычисляем хеш. (Для получения дополнительной информации о языке circom см. [документацию circom] (https://docs.circom.io/))
Для создания доказательства с нулевым разглашением вам понадобится ключ проверки. Это наиболее чувствительная часть ZKP, потому что, используя исходные данные, которые используются для генерации ключа проверки, любой может создать поддельные доказательства. Эти исходные данные называются «токсичными отходами», которые необходимо выбросить. Из-за этого существует «церемония» генерации ключа прувинга. В церемонии участвует много участников, и каждый участник вносит свой вклад в ключ испытания. Только одного невредоносного члена достаточно, чтобы сгенерировать действительный ключ подтверждения. Используя частные входы, общедоступные входы и ключ подтверждения, система ZKP может запустить схему и сгенерировать доказательство и выходные данные.
Существует ключ проверки для ключа подтверждения, который можно использовать для проверки. Система проверки использует общедоступные входные данные, выходные данные и ключ проверки для проверки доказательства.
Snarkjs — это полнофункциональный инструмент для генерации ключа подтверждения и ключа проверки с помощью церемонии, создания доказательства и его проверки. Он также может генерировать смарт-контракт для проверки, который может использоваться любым другим контрактом для проверки доказательства с нулевым разглашением. Для получения дополнительной информации см. [документацию по snarkjs] (https://github.com/iden3/snarkjs).
Теперь у нас есть все, чтобы понять, как работает Tornado Cash (TC). Когда вы вносите 1 ETH по контракту TC, вы должны предоставить хэш обязательства. Этот хэш обязательства будет храниться в дереве Меркла. Когда вы снимаете этот 1 ETH с другой учетной записи, вы должны предоставить 2 доказательства с нулевым разглашением. Первый доказывает, что дерево Меркель содержит ваши обязательства. Это доказательство является доказательством доказательства Меркла с нулевым разглашением. Но этого недостаточно, потому что вам должно быть разрешено снять этот 1 ETH только один раз. Из-за этого вы должны предоставить нуллификатор, уникальный для обязательства. Контракт хранит этот нуллификатор, это гарантирует, что вы не сможете снять внесенные деньги более одного раза.
Уникальность обнулителя обеспечивается методом генерации обязательств. Обязательство генерируется из нуллификатора и секрета путем хеширования. Если вы измените нуллификатор, изменится и обязательство, поэтому один нуллификатор можно использовать только для одного обязательства. Из-за одностороннего характера хеширования невозможно связать коммит и обнулитель, но мы можем сгенерировать для него ZKP.
После теории давайте посмотрим, как выглядит схема вывода TC:
```javascript
включить "../node_modules/circomlib/схемы/bitify.circom";
включить "../node_modules/circomlib/схемы/pedersen.circom";
включить "merkleTree.circom";
// вычисляет Педерсена (нуллификатор + секрет)
шаблон CommitmentHasher() {
обнулитель входного сигнала;
секрет ввода сигнала;
выходной сигнал фиксации;
вывод сигнала nullifierHash;
обязательство компонентаHasher = Pedersen(496);
компонент nullifierHasher = Pedersen(248);
компонент nullifierBits = Num2Bits(248);
компонент secretBits = Num2Bits(248);
nullifierBits.in <== nullifier;
secretBits.in <== секрет;
для (вар я = 0; я < 248; я ++) {
nullifierHasher.in[i] <== nullifierBits.out[i];
commitHasher.in[i] <== nullifierBits.out[i];
commitHasher.in[i + 248] <== secretBits.out[i];
обязательство <== обязательствоHasher.out[0];
nullifierHash <== nullifierHasher.out[0];
// Проверяет, что обязательство, соответствующее заданному секрету и аннулятору, включено в дерево депозитов Меркла
шаблон Снятие(уровни) {
корень входного сигнала;
входной сигнал nullifierHash;
сигнальный закрытый входной обнулитель;
секретный входной сигнал;
частные входные элементы сигнала [уровни];
закрытый вход сигнала pathIndices[levels];
хешировщик компонентов = CommitmentHasher();
hasher.nullifier <== nullifier;
hasher.secret <== секрет;
hasher.nullifierHash === nullifierHash;
дерево компонентов = MerkleTreeChecker (уровни);
tree.leaf <== hasher.commitment;
дерево.корень <== корень;
for (var i = 0; i < уровней; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndices[i] <== pathIndices[i];
основной компонент = Снять (20);
Первый шаблон — это CommitmentHasher. У него есть два входа: нуллификатор и секрет, которые представляют собой два случайных 248-битных числа. Шаблон вычисляет хэш нуллификатора и хэш фиксации, который является хэшем нуллификатора и секрета, как я писал ранее.
Второй шаблон — это сам вывод. У него есть 2 общедоступных входа: корень Merkle и nullifierHash. Корень Merkle необходим для проверки доказательства Merkle, а nullifierHash необходим смарт-контракту для его хранения. Частными входными параметрами являются нуллификатор, секрет, а также pathElements и pathIndices доказательства Меркла. Схема проверяет нуллификатор, генерируя коммит из него и из секрета, а также проверяет данное доказательство Меркла. Если все в порядке, будет сгенерировано доказательство с нулевым разглашением, которое может быть проверено смарт-контрактом TC.
Вы можете найти смарт-контракты в [папке контрактов] (https://github.com/tornadocash/tornado-core/tree/master/contracts) в репозитории. Верификатор генерируется из схемы. Он используется контрактом Tornado для проверки ZKP для данного хэша нуллификатора и корня Merkle.
Самый простой способ использовать контракт — интерфейс командной строки. Он написан на JavaScript, и его исходный код относительно прост. Вы можете легко найти, где параметры и ZKP генерируются и используются для вызова смарт-контракта.
Доказательство с нулевым разглашением является относительно новым в криптомире. Математика, стоящая за этим, действительно сложна и трудна для понимания, но такие инструменты, как snarkjs и circom, упрощают ее использование. Надеюсь, эта статья поможет вам разобраться в этой «волшебной» технологии, и вы сможете использовать ZKP в своем следующем проекте.
Удачного кодирования…
Оригинал