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

Бумажное голосование (как и выборы) — очень дорогой способ голосования. У меня нет точных данных, но это стоит миллиарды долларов, и все время витает в воздухе, что кто-то его обманул.

Система на основе блокчейна стоит лишь часть этой цены, и она очень безопасна, потому что блокчейн является общедоступным, и каждый может отслеживать все в нем.

Единственное, что не отслеживается, так это то, кто за какую партию голосовал благодаря доказательству с нулевым разглашением (более подробное объяснение читайте в моей предыдущей статье). Таким образом, голосование на основе блокчейна кажется святым Граалем голосования.

В моей предыдущей статье я обещал простое доказательство концепции. Сейчас он завершен и доступен на GitHub. Я покажу вам, как это работает в этой статье.

Метод, который я использую, такой же, как и у Tornado Cash. Tornado Cash — это решение для конфиденциальности Ethereum и ERC20, не связанное с тюремным заключением, основанное на zkSNARK.

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

Это также можно использовать для анонимного голосования, потому что каждый должен проголосовать только один раз, но никто не должен знать, какой голос принадлежит какому избирателю. (У меня есть полная статья на эту тему.)

Мое приложение для голосования — это децентрализованное приложение Ethereum. Это статическая страница с JavaScript без какого-либо бэкэнда (бэкэнд — это смарт-контракт на Ethereum). Из-за этого сложно атаковать систему, потому что в блокчейне нет единой точки отказа.

Если вы хотите протестировать его локально, самый простой способ — запустить локальный блокчейн:

npx hardhat node

и настроить учетные записи в MetaMask. Мы будем использовать первые 2 аккаунта.

На первом этапе клонируйте репозиторий, разверните смарт-контракты и запустите приложение:

git clone https://github.com/TheBojda/zktree-vote
cd zktree-vote
npm i
npm run prepare (optional, npm i should run it)
npm run deploy
npm start

Приложение dApp использует MetaMask для связи с блокчейном, поэтому, если вы используете его из браузера на настольном компьютере, необходимо установить MetaMask, а если вы используете его со своего мобильного телефона, откройте его во встроенном браузере приложения MetaMask.

При открытии http://localhost:1234/ вы увидите меню приложения:

Нажмите на регистрацию, чтобы проголосовать, чтобы открыть страницу регистрации.

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

Валидация избирателя может осуществляться лично на месте голосования, онлайн с использованием системы видеоконференцсвязи и т. д. Валидатор может использовать для этого «Инструмент Валидатора»:

Чтобы упростить задачу, я добавил на страницу QR-ридер, поэтому отправить обязательство валидатору можно, отсканировав QR-код избирателя, или же голосующий может скопировать его и отправить в чат (онлайн-проверка).

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

Это гарантирует, что один избиратель будет зарегистрирован только один раз с одним обязательством, что означает, что он может проголосовать только один раз.

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

На последней странице вы можете проверить результаты голосования в режиме реального времени.

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

После этого короткого вступления давайте посмотрим на код.

Для разработки этого dApp для голосования я использовал свою библиотеку zk-merkle-tree. Это библиотека JavaScript, которая использует метод Tornado Cash на основе zkSNARK и скрывает всю сложность доказательств с нулевым разглашением.

Я планирую написать полноценную статью об этой библиотеке, поэтому в этой статье я не буду писать о ней более подробно.

Смарт-контракт системы голосования выглядит так:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "zk-merkle-tree/contracts/ZKTree.sol";

contract ZKTreeVote is ZKTree {
    address public owner;
    mapping(address => bool) public validators;
    mapping(uint256 => bool) uniqueHashes;
    uint numOptions;
    mapping(uint => uint) optionCounter;

    constructor(
        uint32 _levels,
        IHasher _hasher,
        IVerifier _verifier,
        uint _numOptions
    ) ZKTree(_levels, _hasher, _verifier) {
        owner = msg.sender;
        numOptions = _numOptions;
        for (uint i = 0; i <= numOptions; i++) optionCounter[i] = 0;
    }

    function registerValidator(address _validator) external {
        require(msg.sender == owner, "Only owner can add validator!");
        validators[_validator] = true;
    }

    function registerCommitment(
        uint256 _uniqueHash,
        uint256 _commitment
    ) external {
        require(validators[msg.sender], "Only validator can commit!");
        require(
            !uniqueHashes[_uniqueHash],
            "This unique hash is already used!"
        );
        _commit(bytes32(_commitment));
        uniqueHashes[_uniqueHash] = true;
    }

    function vote(
        uint _option,
        uint256 _nullifier,
        uint256 _root,
        uint[2] memory _proof_a,
        uint[2][2] memory _proof_b,
        uint[2] memory _proof_c
    ) external {
        require(_option <= numOptions, "Invalid option!");
        _nullify(
            bytes32(_nullifier),
            bytes32(_root),
            _proof_a,
            _proof_b,
            _proof_c
        );
        optionCounter[_option] = optionCounter[_option] + 1;
    }

    function getOptionCounter(uint _option) external view returns (uint) {
        return optionCounter[_option];
    }
}

Смарт-контракт унаследован от ZKTree (из библиотеки zk-merkle-tree) и использует его методы _commit и _nullify. Метод _commit сохраняет обязательство, а метод _nullify сохраняет нуллификатор и проверяет для него доказательство с нулевым разглашением.

Владелец может добавить валидаторов, вызвав метод registerValidator. Только валидаторы могут отправить обязательство смарт-контракту после проверки личности избирателя.

Последний метод — это getOptionCounter, который вы можете использовать для запроса результатов голосования в режиме реального времени.

Вот и все. Благодаря zk-merkle-tree контракт на голосование очень прост, вся сложность скрыта за библиотекой.

Само dApp представляет собой одностраничное приложение vue.js. Обязательство и обнулитель генерируются в компоненте VoterRegistration с помощью generateCommitment из zk-merkle-tree и сохраняются в локальном хранилище.

this.commitment = JSON.parse(
  localStorage.getItem("zktree-vote-commitment")
);
if (!this.commitment) {
  this.commitment = await generateCommitment();
  localStorage.setItem(
    "zktree-vote-commitment",
    JSON.stringify(this.commitment)
  );
}

Компонент ValidatorTool используется валидатором для отправки обязательства в блокчейн. Он считывает адрес контракта из Contracts.json (созданного в процессе развертывания) и отправляет обязательство с уникальным хэшем в контракт с правом голоса.

const abi = [
  "function registerCommitment(uint256 _uniqueHash, uint256 _commitment)",
];
const provider = new ethers.providers.Web3Provider(
  (window as any).ethereum
);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
const contracts = await (await fetch("contracts.json")).json();
const contract = new ethers.Contract(contracts.zktreevote, abi, signer);
try {
  await contract.registerCommitment(this.uniqueHash, this.commitment);
} catch (e) {
  alert(e.reason);
}

Компонент Vote используется избирателем для голосования. Он генерирует доказательство ZK, используя метод calculateMerkleRootAndZKProof из zk-merkle-tree, и отправляет его в цепочку блоков с помощью нуллификатора.

const commitment = JSON.parse(
  localStorage.getItem("zktree-vote-commitment")
);

const abi = [
  "function vote(uint _option,uint256 _nullifier,uint256 _root, 
   uint[2] memory _proof_a,uint[2][2] memory _proof_b,
   uint[2] memory _proof_c)",
];
const provider = new ethers.providers.Web3Provider(
  (window as any).ethereum
);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
const contracts = await (await fetch("contracts.json")).json();
const contract = new ethers.Contract(contracts.zktreevote, abi, signer);
const cd = await calculateMerkleRootAndZKProof(
  contracts.zktreevote,
  signer,
  TREE_LEVELS,
  commitment,
  "verifier.zkey"
);
try {
  await contract.vote(
    this.option,
    cd.nullifierHash,
    cd.root,
    cd.proof_a,
    cd.proof_b,
    cd.proof_c
  );
} catch (e) {
  alert(e.reason);
}

Как видите, код действительно простой, потому что вся сложность ZKP скрыта библиотекой zk-merkle-tree. На основе этого кода вы можете легко построить собственную систему голосования.

Возможные улучшения:

  • Белый список избирателей. Вы можете построить дерево Меркла из уникальных хэшей избирателей перед голосованием и проверить наличие избирателя в нем, когда валидатор отправит обязательство. Это предотвращает использование поддельных идентификаторов.
  • Улучшенное управление валидаторами. Если есть тысячи валидаторов, дерево Меркла — лучший способ их пакетной регистрации. Если избиратели и валидаторы разделены на округа, то можно сгенерировать отдельные деревья Меркла для округов и назначить этим деревьям валидаторов.
  • Улучшенный метод проверки. Единственный способ обмануть в этой системе — использовать поддельное имя для голосования. Вот почему проверка очень важна. Есть и другие методы предотвращения вредоносных валидаторов. Например, система может случайным образом выбрать 2 или 3 валидатора для одного избирателя. Вероятность того, что все они вредоносные, очень мала. Или он может записывать процесс проверки, а другие валидаторы случайным образом проверяют записанные видео. Мошенничество при голосовании является преступлением, поэтому существует очень высокий риск того, что валидатор окажется злонамеренным.
  • Интеграция систем видеоконференцсвязи, таких как Jitsi Meet. Отправка обязательства и весь процесс проверки могут быть очень простыми, если система видеоконференцсвязи интегрирована. Избиратели могут заполнить форму со своими данными и отправить ее до проверки. Валидатор должен только проверить удостоверение личности через камеру, и если все в порядке, он может отправить коммит и уникальный хэш в блокчейн одним нажатием кнопки.
  • Поддержка цифрового удостоверения личности. Если у избирателя есть цифровое удостоверение личности, которое можно использовать для цифровой подписи обязательства, то весь процесс проверки можно пропустить. Этот метод очень дешев, потому что не требует человеческих ресурсов, поэтому его можно использовать часто, а избиратели могут быть более вовлечены в процесс принятия решений.

Демократия — самый важный аспект блокчейна, а Web3 и анонимное голосование на основе блокчейна — его святой Грааль.

Я надеюсь, что эта короткая статья и библиотека zk-merkle-tree могут стать хорошей отправной точкой для других, чтобы создать свои собственные системы, которые когда-нибудь будут использоваться в реальном мире.

ОБНОВЛЕНИЕ: Вы можете прочитать вводную статью о библиотеке zk-merkle-tree здесь: