#plonk #zero-knowledge #smart-contracts #zk #near-protocol #cryptography

near_plonk_verifier

Plonk proof verifier implementation for Near Protocol smart Contracts

1 stable release

1.0.0 Jun 7, 2023

#6 in #near-protocol

MIT license

52KB
991 lines

near-plonk-verifier

Rust library to use verify plonk zero knowledge proofs inside a NEAR Protocol smart contract.

Use cases

Applying zero knowledge cryptography inside blockchain smart contracts has been one of the most widely praised uses of this new technology. In the Ethereum ecosystem, there are many applications using zero-knowledge proofs to ensure data privacy and computational efficiency in a permissionless blockchain context.

Developing this kind of applications became accessible to a normal (read not a cryptography expert) developer with libraries such as snark.js and circom, which simplify the construction of algorithms by abstracting away all cryptography implementation and allowing developers to only focus on business logic. This tooling, however, is only compatible with EVM based blockchains. For developers looking to build zk-based applications on the NEAR protocol the tool was not enough.

With this in mind, we developed this library as a generic proof verifier utilizing the plonk algorithm. This can be utilized together with snark.js and circom to generate a full fledged application running zk proofs.

You can use this library as a substitute for the Verifying from a Smart Contract section in the circom tutorial.

How to use it

To implement this Verifier in your Smart Contract you must first have setup your logical circuit and produced a trusted setup using snark.js. This library will allow you to verify if a proof is valid or not inside the Smart Contract. To do so you must:

  1. Initialize the Verifier in the Smart Contract's state by passing the setup values generated by snark.js to it
  2. Submit proofs generated by the prover binary (created by snark.js) to the smart contract

The verifier can be implemented with a simple import:

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::U128;
use near_sdk::{env, near_bindgen, PanicOnDefault, AccountId, BorshStorageKey};
use near_sdk::collections::{LookupSet};
use near_plonk_verifier::{self, Verifier, U256, G1Point, G2Point};

#[near_bindgen]
#[derive(PanicOnDefault, BorshDeserialize, BorshSerialize)]
pub struct Contract {
  pub verifier: Verifier,
}

impl Contract {
  #[init]
  pub fn new(
      power: U256,
      n_public: U256,
      q_m: G1Point,
      q_l: G1Point,
      q_r: G1Point,
      q_o: G1Point,
      q_c: G1Point,
      s_1: G1Point,
      s_2: G1Point,
      s_3: G1Point,
      k_1: U256,
      k_2: U256,
      x_2: G2Point,
      q: U256,
      qf: U256,
      w1: U256,
  ) -> Self {
    assert!(!env::state_exists(), "Already initialized");
    
    Self {
      verifier: Verifier::new(
        power
        n_public,
        q_m,
        q_l,
        q_r,
        q_o,
        q_c,
        s_1,
        s_2,
        s_3,
        k_1,
        k_2,
        x_2,
        q,
        qf,
        w1,
      )
    }
  }
}

The Verifier struct can be represented as a series of elliptic curve points and scalar values:

#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, Clone, Debug)]
#[serde(crate = "near_sdk::serde")]
pub struct G1Point {
    pub x: U256,
    pub y: U256,
}

#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, Clone)]
#[serde(crate = "near_sdk::serde")]
pub struct G2Point {
    pub x: [U256; 2],
    pub y: [U256; 2],
}

#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Verifier {
    pub struct Verifier {
    // n values
    power: U256,
    n: U256,
    n_public: U256,
    n_lagrange: U256,
    // Q values
    q_m: G1Point,
    q_l: G1Point,
    q_r: G1Point,
    q_o: G1Point,
    q_c: G1Point,
    // S values
    s_1: G1Point,
    s_2: G1Point,
    s_3: G1Point,
    // k values
    k_1: U256,
    k_2: U256,
    // X2 values
    x_2: G2Point,
    // Field size constants
    q: U256,
    qf: U256,
    // omega value
    w1: U256,
  }
}

To fill out this values, refer to the verification_key.json file generated by snarkjs.js, it will provide all the parameters to initialize the Verifier.

After initializing the verifier, it can be used to evaluate any proof in your circuit and check whether it is valid or not with the verify method.

pub fn verify(&self, input: Vec<U256>, proof: Proof) -> bool

#[derive(Serialize, Deserialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Proof {
    // public values
    public_values: Vec<U256>,
    // proof values
    a: G1Point,
    b: G1Point,
    c: G1Point,
    z: G1Point,
    t_1: G1Point,
    t_2: G1Point,
    t_3: G1Point,
    eval_a: U256,
    eval_b: U256,
    eval_c: U256,
    eval_s1: U256,
    eval_s2: U256,
    eval_zw: U256,
    eval_r: U256,
    wxi: G1Point,
    wxi_w: G1Point,
}

Proofs always follow the same structure and are generated by snark.js when running the prover algorithm. They have public_values added to it as a vec, in snark.js public values are generated as a separate json file containing a single array.

snark.js generates 2 files whenever it creates a proof:

  1. public -> contains an array of values that should be passed to public_values
  2. proof -> contains the rest of the params to be added to Proof

Supported near-sdk versions

near-plonk-verifier is built on top of near-sdk 4.0.0 and will be updated periodically to reflect updates on near-sdk. Previous near-sdk versions are not compatible with this library.

Dependencies

~6MB
~119K SLoC