Bằng chứng bảo mật-bảo mật quyền sở hữu đối với thẻ tham dự (PoA) bằng cách sử dụng zk-SNARKs

Jul 25 2022
Giới thiệu Vào thời điểm bạn tìm thấy bài viết này, bạn có thể đã xem qua nhiều bài viết hấp dẫn giải thích về zk-SNARK. Nếu bạn chưa có, hãy bỏ qua phần cuối, nơi tôi liệt kê những thứ quan trọng nhất cho người mới bắt đầu.

Giới thiệu

Vào thời điểm bạn tìm thấy bài viết này, bạn có thể đã xem qua nhiều bài báo hấp dẫn giải thích về zk-SNARK. Nếu bạn chưa có, hãy bỏ qua phần cuối, nơi tôi liệt kê những thứ quan trọng nhất cho người mới bắt đầu.

Trong khi thử nghiệm với zk-SNARK, tôi gặp phải một số trở ngại thực tế, mỗi trở ngại đòi hỏi hàng giờ đánh giá mã và tìm kiếm trên web. Với những điều sau đây, tôi muốn giúp bạn tiết kiệm thời gian đó, để bạn có thể trực tiếp tập trung vào phần thực hành. Do đó, phần sau là hướng dẫn thực tế nhằm giới thiệu nhanh cho bạn về zk-SNARK và các cách có thể áp dụng chúng. Tôi sẽ không giải thích zk-SNARK là gì và cách chúng hoạt động (và thành thật mà nói, tôi không thể làm điều đó một cách chi tiết chính xác), nhưng tôi sẽ cố gắng cung cấp cho bạn một số thông tin thực hành mà bạn có thể cần để tham khảo chủ đề. Trước khi chúng ta có thể bắt đầu, hãy đảm bảo bạn đã làm quen với SnarkJS , đây sẽ là thành phần chính của hướng dẫn sau.

Động lực

Trong phần sau, chúng tôi xây dựng Mã thông báo Chứng minh sự thu hút (PoA). Mã thông báo này có thể được phát hành bởi các nhà tổ chức sự kiện và sau đó được những người tham gia sự kiện yêu cầu cung cấp cho họ một thứ gì đó vô hình như một lời nhắc nhở. Phần thú vị là người dùng có thể yêu cầu PoA Token của họ theo các cách bảo vệ quyền riêng tư mà không cần phải tiết lộ danh tính của họ. Chìa khóa là công nghệ zk-SNARK. Tôi đã đọc về trường hợp sử dụng cụ thể này trong bài đăng trên blog của Vitalik trên Soulbound Tokens và trực tiếp nghĩ về cách triển khai khả thi.

Hướng dẫn này cũng có thể đóng vai trò giới thiệu cho đề xuất EIP gần đây của tôi về các ERC-721 tương thích với zk-SNARK.

Quy trình làm việc

Trước khi sự kiện diễn ra:
Đầu tiên, người tham dự đăng ký sự kiện và cung cấp cam kết của họ cho đơn vị tổ chức sự kiện. Cam kết c được tạo ra bằng cách lấy mã băm của bí mật của người phục vụ s và giá trị nullifier n , sao cho c = h (s, n) . Trong quá trình đăng ký, cả hai, bí mật và nullifier vẫn ở chế độ riêng tư.

Sau sự kiện:
Nhà phát hành PoA tạo cây merkle, off-chain: Kích thước của cây merkle phụ thuộc vào số lượng PoA Tokens mà người đó muốn tạo. Các lá của cây merkle chứa các cam kết nhận được từ những người tham dự trong quá trình đăng ký.
Nhà phát hành sau đó tạo một hợp đồng Token (mã bên dưới). Hợp đồng mã thông báo về cơ bản là ERC721 mở rộng, tuy nhiên, chúng tôi xóa chức năng chuyển giao. Đã tồn tại các EIP cho Mã liên kết tài khoản và Mã bảo mật vẫn đang trong giai đoạn soạn thảo, do đó chúng tôi sẽ sử dụng ERC721 đã được điều chỉnh .) vẫn đang trong giai đoạn soạn thảo, do đó chúng tôi sẽ sử dụng ERC721 đã được điều chỉnh. Ngay sau khi các mã thông báo không thể chuyển nhượng trở thành tiêu chuẩn, chúng tôi sẽ áp dụng hợp đồng.

Hợp đồng này phải cung cấp các khả năng sau:

  • Lưu trữ một gốc merkle bất biến trong một biến trạng thái
  • Tương tác với hợp đồng Người xác minh cuối cùng xác minh bằng chứng zk .
  • Merkle Root Hash - Đầu vào công khai
  • Nullifier Hash - Đầu vào công khai
  • Địa chỉ người nhận - Đầu vào công khai
  • Bí mật— Đầu vào riêng tư
  • Nullifier - Đầu vào riêng tư
  • PathElements - Đầu vào riêng tư
  • PathIndices— Đầu vào riêng tư

Để chứng minh sự phù hợp, câu tục ngữ phải có khả năng tạo ra bằng chứng merkle: Người dùng chứng minh khả năng tạo lại đường dẫn từ lá merkle đến gốc bằng cách sử dụng các đầu vào được cung cấp. Kỹ thuật này đã được áp dụng tại một số dự án khác, chẳng hạn như TornadoCash hoặc StealthDrop . Kiểm tra tài liệu để biết thêm chi tiết nhưng nói tóm lại, người dùng cung cấp lá của mình (cam kết) và đường dẫn đầy đủ đến gốc (PathElements - bao gồm cả băm). Sau đó, người dùng phải cho thuật toán biết liệu các phần tử đường dẫn đã cung cấp có được nối từ bên trái hoặc bên phải ở mỗi cấp trong cây merkle trước khi băm hay không. Điều này được thực hiện thông qua một mảng (PathIndices) chứa các số 0 và 1, cho biết hướng.

Lưu ý, bí mật của người dùng và trình nullifier vẫn ở chế độ riêng tư trong toàn bộ quá trình.

Cuối cùng, người phục vụ có thể nhập bằng chứng đã tạo vào hợp đồng Token đã thảo luận, có thể xác minh nó. Người dùng cũng nhập nullifier-băm và địa chỉ người nhận vào hợp đồng Mã thông báo.
Nếu bằng chứng hợp lệ và giá trị nullifier vẫn chưa được đổi, PoA sẽ được đúc đến địa chỉ do người dùng chỉ định . Địa chỉ này có thể không có bất kỳ kết nối nào với người tham gia thực tế. Hàm băm nullifier phải được cung cấp để ngăn người dùng đúc nhiều lần. Mọi băm nullifier đã đổi đều được hợp đồng lưu trữ.

Mặc dù Token PoA có thể không được chuyển nhượng, nhưng mọi người vẫn có thể chứng minh sự tham gia thông qua hợp đồng Token bất cứ lúc nào.

Mã số

Hợp đồng

Vì vậy, chúng ta hãy bắt đầu viết mã… Đầu tiên, chúng ta có hợp đồng Token của mình. Nói chung, chúng tôi lấy nó từ tiêu chuẩn ERC-721 và thêm 4 biến trạng thái công khai vào nó. Đầu tiên, _root chứa hàm băm gốc merkle của cây merkle đã tạo. _Verifier chứa địa chỉ của hợp đồng Người xác minh . Hợp đồng Người xác minh được tạo bằng cách sử dụng snarkjs . Hàm băm của các giá trị null đã được đổi được lưu trữ trong mảng _nullifierHashing . Biến _tokenId đại diện cho một bộ đếm ngày càng tăng.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.8;
import "./verifier.sol";
import "./ERC721.sol";
contract zkExtension is ERC721 {
    bytes32 public _root;
    Verifier public _verifier;
    mapping(bytes32 => bool) public _nullifierHashes;
    uint256 private _tokenId;
constructor(
        string memory name_, 
        string memory symbol_, 
        bytes32 root_,
        Verifier verifier_
        )
        ERC721(name_, symbol_)
    {   
      _root = root_;
      _verifier = verifier_;
    }
function root() public view virtual returns (bytes32) {
    return _root;
  }
// @notice mints PoA to address if nullifierHash is unknown
  // Returns true for valid proofs
  function verify(uint[2] memory a_,
                  uint[2][2] memory b_,
                  uint[2] memory c_,
                  uint[3] memory input_,
                  bytes32 root_,
                  bytes32 nullifierHash_,
                  address recipient_) public returns (bool valid) 
      {
        // Check if right tree
        require(root_ == _root, "Wrong root");
        if (_verifier.verifyProof(a_,b_,c_,input_)) {
            if (!_nullifierHashes[nullifierHash_]) {
              _nullifierHashes[nullifierHash_] = true;
              _mint(recipient_, _tokenId);
              _tokenId += 1;
            }
            return true;
        }
        return false;
      }
}

Chu trình

Hãy đi đến các mạch. Đầu tiên, chúng tôi sử dụng ngôn ngữ Circom (Circom 2.0) để định nghĩa mạch số học. Việc tạo ra các mạch là một quá trình rất mô-đun. Mỗi thao tác chúng tôi sử dụng đã được những người khác triển khai thành các mẫu Circom 2.0 . Tôi sẽ liên kết các triển khai tốt nhất ở cuối bài viết này. Mạch của chúng tôi khá đơn giản để triển khai, vì chúng tôi có thể sử dụng mẫu MerkleTreeChecker hiện có , đã được sử dụng tại TornadoCash và điều chỉnh nó theo yêu cầu của chúng tôi. Mạch được sử dụng để chứng minh rằng một chiếc lá nào đó có mặt trên cây merkle. Nếu vậy, điều này có nghĩa là chủ sở hữu của cam kết đã thực sự được đưa vào cây merkle của tổ chức phát hành PoA. Trong ví dụ này, các cấp độtham số được đặt thành 1, có nghĩa là cây merkle của chúng ta chỉ bao gồm 2 lá và gốc.

pragma circom 2.0.2;
include "../merkle/merkleTree.circom";
include "../merkle/commitmentHasher.circom";
template Main(levels) {
    signal input root;
    signal input nullifierHash;
    signal input recipient; 
    signal input nullifier;
    signal input secret;
    signal input pathElements[levels];
    signal input pathIndices[levels];
    
    component hasher = CommitmentHasher();
    hasher.nullifier <== nullifier;
    hasher.secret <== secret;
    hasher.nullifierHash === nullifierHash;
    
    component tree = MerkleTreeChecker(levels);
    tree.leaf <== hasher.commitment;
    tree.root <== root;
    for (var i = 0; i < levels; i++) {
        tree.pathElements[i] <== pathElements[i];
        tree.pathIndices[i] <== pathIndices[i];
    }
    signal recipientSquare;
    recipientSquare <== recipient * recipient;
}
component main {public [root,nullifierHash,recipient]} = Main(1);

Mẫu commitmentHasher trông giống như sau (nó giống với mẫu mà TornadoCash sử dụng):

pragma circom 2.0.2;
include "../../node_modules/circomlib/circuits/bitify.circom";
include "../../node_modules/circomlib/circuits/pedersen.circom";
template CommitmentHasher() {
    signal input nullifier;
    signal input secret;
    signal output commitment;
    signal output nullifierHash;
component commitmentHasher = Pedersen(496);
    component nullifierHasher = Pedersen(248);
    component nullifierBits = Num2Bits(248);
    component secretBits = Num2Bits(248);
    nullifierBits.in <== nullifier;
    secretBits.in <== secret;
    for (var i = 0; i < 248; i++) {
        nullifierHasher.in[i] <== nullifierBits.out[i];
        commitmentHasher.in[i] <== nullifierBits.out[i];
        commitmentHasher.in[i + 248] <== secretBits.out[i];
    }
commitment <== commitmentHasher.out[0];
    nullifierHash <== nullifierHasher.out[0];
}

Như đã hứa, đây sẽ là một hướng dẫn thực tế, do đó tôi cũng muốn chia sẻ một số dòng mã có thể được sử dụng để kiểm tra toàn bộ thiết lập:

Chúng tôi cần một số khuôn khổ cho phép chúng tôi tạo một cây merkle để thử nghiệm. Việc triển khai cây merkle phải hỗ trợ băm MIMC, vì chúng tôi cũng sử dụng băm MIMC trong mạch của mình. Chúng tôi sử dụng triển khai cây cố định của TornadoCash cho điều đó, mà tôi thấy là hoàn hảo cho ví dụ này.

Chúng tôi xây dựng nó bằng cách sử dụng Node v18.4.0:

const assert = require('assert')
const crypto = require('crypto')
const ethers = require('ethers')
const { mimcHash } = require('./mimc');
const circomlib = require('circomlib')
const MerkleTree = require('fixed-merkle-tree')
const leBuff2int = require("ffjavascript").utils.leBuff2int;
/** Generate random number of specified byte length */
const rbigint = nbytes => leBuff2int(crypto.randomBytes(nbytes))
/** Compute pedersen hash */
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
const mimcsponge = circomlib.mimcsponge
/** BigNumber to hex string of specified length */
function toHex(number, length = 32) {
  const str = number instanceof Buffer ? number.toString('hex') : BigInt(number).toString(16)
  return '0x' + str.padStart(length * 2, '0')
}
/**
 * Create deposit object from secret and nullifier
 */
function createDeposit({ nullifier, secret }) {
  const preimage = Buffer.concat([nullifier.leInt2Buff(31), secret.leInt2Buff(31)])
  const commitment = pedersenHash(preimage)
  const commitmentHex = toHex(commitment)
  const nullifierHash = pedersenHash(nullifier.leInt2Buff(31))
  const nullifierHex = toHex(nullifierHash).toString()
  return { nullifier, secret, preimage, commitment, commitmentHex, nullifierHash, nullifierHex }
}
console.log(rbigint(31));
note = createDeposit({
        nullifier: 70468531690246127597324659426162022323359627919521679359003215289346912273n,
        secret: 60468531690246127597324659426162022323359627919521679359003215289346912273n,
      })
console.log(note)
const my_commitment = note.commitment
const my_recipient = "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
// Merkle tree
const merkleTreeLevels = 1;
const merkleTreeCommitments = [my_commitment,my_commitment];
const getPath = address => {
  const merkleTree = new MerkleTree(merkleTreeLevels, 
                                    merkleTreeCommitments, 
                                  { hashFunction: mimcHash(123) });
  let index = merkleTreeCommitments.findIndex(leaf => leaf === address);
  if(index < 0) return null;
  const { pathElements, pathIndices } = merkleTree.path(index);
  return [merkleTree.root(), pathElements, pathIndices];
};
function Uint8Array_to_bigint(x) {
  var ret = 0n;
  for (var idx = 0; idx < x.length; idx++) {
    ret = ret * 256n;
    ret = ret + BigInt(x[idx]);
  }
  return ret;
}
const fromHexString = hexString => new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
const intToHex = intString => ethers.BigNumber.from(intString).toHexString();
const hexStringTobigInt = hexString => {
  return Uint8Array_to_bigint(fromHexString(hexString));
};
function generateProofInputs(secret) {
  const val = getPath(secret);
  if (!val) return null;
  const [root, pathElements, pathIndices] = val;
  console.log("Hex root:", intToHex(root.toString()));
  const input = {
    root: root.toString(),
    nullifierHash: note.nullifierHash.toString(),
    pathElements: pathElements.map(x => x.toString()),
    pathIndices: pathIndices,
    secret: note.secret.toString(),
    nullifier: note.nullifier.toString(),
    recipient: hexStringTobigInt(my_recipient).toString()
  };
return input;
}
var commitment = my_commitment;
var inputs = generateProofInputs(commitment);
console.log("Inputs:",JSON.stringify(inputs));

Để cung cấp trực tiếp cho bạn một input.json mẫu để chơi xung quanh, hãy xem Github của tôi . Ở đó, bạn cũng sẽ tìm thấy mã của toàn bộ dự án.

Cuối cùng, như đã hứa, hãy để tôi cung cấp cho bạn một số tài nguyên có giá trị nhất mà tôi đã sử dụng để tìm hiểu về zkSNARKs:

  1. https://consensys.net/blog/developers/introduction-to-zk-snarks/
  2. https://docs.tornado.cash/general/how-does-tornado.cash-work
  3. https://docs.tornado.cash/tornado-cash-classic/circuits
  4. https://blog.iden3.io/first-zk-proof.html
  5. https://vitalik.ca/general/2021/01/26/snarks.html
  6. https://vitalik.ca/general/2022/06/15/using_snarks.html
  7. https://github.com/nalinbhardwaj/stealthdrop
  8. https:///@imolfar/why-and-how-zk-snark-works-1-introduction-the-medium-of-a-proof-d946e931160
  1. https://github.com/iden3/snarkjs
  2. https://github.com/iden3/circom
  3. https://github.com/iden3/circomlib
  4. https://github.com/0xPARC/circom-ecdsa

© Copyright 2021 - 2022 | vngogo.com | All Rights Reserved