DEV Community

Sufferer
Sufferer

Posted on • Edited on • Originally published at dev.to

Building your first ZKP program on Solana

👋 Introduction

Zero-knowledge proofs are a kind of security tool in cryptography. They let one person (the prover) show another person (the verifier) that something is true, without giving away any other details. Think of it like proving you know a secret without actually telling the secret. This tool is really useful for keeping things private and has been used in things like making payments without revealing identities, sending messages anonymously, and in secure systems where trust is important (bridge).

SOLANA UPGRADE 2023.10

Prior to Solana v1.16 it wasn't possible to verify cryptographic proofs on Solana efficiently. Due to the prevalence of complex computation behind the pairing algorithm, it was necessary to increase the functionality of Solana by adding syscalls to conduct proof verification. This functionality was added in the Solana v1.16 update and at the time of this writing is only available on testnet.

🦄 This tutorial will cover

  • The basics of zero-knowledge cryptography and specifically zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)
  • Initiating a trusted setup ceremony (using the Powers of Tau)
  • Writing and compiling a simple ZK circuit (using the Circom language)
  • Generating, deploying, and testing a Solana contract to verify a sample ZK-proof

🟥🟦 Explaining ZK-proofs with a color-focused example
Let's break down zero-knowledge proofs with an easy-to-understand scenario. Imagine you need to convince someone who is color-blind that different colors can be distinguished. We'll tackle this challenge interactively. Picture this: the color-blind person (acting as the verifier) selects two sheets of paper, one red 🟥 and one blue 🟦, which look identical to them.

The verifier then shows you (the prover) one of these papers and asks you to remember its color. Next, they hide the paper behind their back, possibly switching it with the other one, and ask you to identify if the color is the same or has changed. If you consistently identify the color correctly, it suggests you can differentiate colors (or you're just lucky, as there's a 50% chance of guessing right).

If this test is repeated 10 times and you're always correct, the verifier becomes about 99.90234% sure (calculated as 1 - (1/2)^10) that you're genuinely distinguishing the colors. After 30 repetitions, their confidence level rises to 99.99999990686774% (1 - (1/2)^30).

However, this method, while interactive, isn't practical for a decentralized application (DApp) that requires users to confirm data without multiple transactions. That's why non-interactive methods like Zk-SNARKs and Zk-STARKs are important.

In this guide, we'll focus on Zk-SNARKs. For those interested in Zk-STARKs, you can learn more on the Starkware. Additionally, a comparison between Zk-SNARKs and Zk-STARKs is available on the Panther Protocol blog.

🎯 Zk-SNARK: Zero-Knowledge Succinct Non-Interactive Argument of Knowledge
A Zk-SNARK is a non-interactive proof system where the prover can demonstrate to the verifier that a statement is true by simply submitting one proof. And the verifier is able to verify the proof in a very short time. Typically, dealing with a Zk-SNARK consists of three main phases:

  • Conducting a trusted setup using a multi-party computation (MPC) protocol to generate proving and verification keys (using Powers of TAU)
  • Generating a proof using a prover key, public input, and secret input (witness)
  • Verifying the proof

Let's set up our development environment and start coding!

⚙ Development environment setup
Let's begin the process by taking the following steps:

Create a new project called "simple-zk" using create-solana-dapp, after that, enter a name for your contract (e.g. simple-zk).

npx create-solana-dapp@latest simple-zk
Enter fullscreen mode Exit fullscreen mode
cd simple-zk
Enter fullscreen mode Exit fullscreen mode

Next we’ll clone the snarkjs repo inside simple-zk folder

git clone https://github.com/iden3/snarkjs
cd snarkjs
npm ci
cd ../simple-zk
Enter fullscreen mode Exit fullscreen mode

Then we’ll install the required libraries needed for ZkSNARKs

npm add --save-dev snarkjs ffjavascript
npm i -g circom
Enter fullscreen mode Exit fullscreen mode

Finally, we'll install the latest verifier on Solana implemented by Light protocol:

git clone https://github.com/Lightprotocol/groth16-solana
cd groth16-solana
npm i
cd ../simple-zk
Enter fullscreen mode Exit fullscreen mode

Great! Now we are ready to start writing our first ZK project on Solana!

We currently have a main folder and two sub folders that make up our ZK project:

simple-zk folder: contains our native Solana template which will enable us to write our circuit and contracts and tests

  • snarkjs folder: contains the snarkjs repo that we cloned in step 2
  • groth16-solana folder: contains the verifying key export function we cloned in step 2

Circom circuit
First let's create a folder simple-zk/circuits and then create a file in it and add the following code to it:

template Multiplier() {
   signal private input a;
   signal private input b;
   //private input means that this input is not public and will not be revealed in the proof

   signal output c;

   c <== a*b;
 }

component main = Multiplier();
Enter fullscreen mode Exit fullscreen mode

Above we added a simple multiplier circuit. By using this circuit we can prove that we know two numbers that when multiplied together result in a specific number (c) without revealing the corresponding numbers (a and b) themselves.

To read more about the circom language consider having a look at this website.

Next we’ll create a folder for our build files and move the data there by conducting the following (while being in the simple-zk folder):

mkdir -p ./build/circuits
cd ./build/circuits
Enter fullscreen mode Exit fullscreen mode

💪 Creating a trusted setup with Powers of TAU
Now it's time to build a trusted setup. To carry out this process, we’ll make use of the Powers of Tau method (which probably takes a few minutes to complete). Let’s get into it:

echo 'prepare phase1'
node ../../../snarkjs/build/cli.cjs powersoftau new bn128 12 pot12_0000.ptau -v

echo 'contribute phase1 first'
node ../../../snarkjs/build/cli.cjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v -e="random text"

echo 'apply a random beacon'
node ../../../snarkjs/build/cli.cjs powersoftau beacon pot12_0001.ptau pot12_beacon.ptau 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n="Final Beacon"

echo 'prepare phase2'
node ../../../snarkjs/build/cli.cjs powersoftau prepare phase2 pot12_beacon.ptau pot12_final.ptau -v

echo 'Verify the final ptau'
node ../../../snarkjs/build/cli.cjs powersoftau verify pot12_final.ptau 
Enter fullscreen mode Exit fullscreen mode

After the process above is completed, it will create the pot12_final.ptau file in the build/circuits folder, which can be used for writing future related circuits.

CONSTRAINT SIZE
If a more complex circuit is written with more constraints, it is necessary to generate your PTAU setup using a larger parameter.

You can remove the unnecessary files:

rm pot12_0000.ptau pot12_0001.ptau pot12_0002.ptau pot12_beacon.ptau
Enter fullscreen mode Exit fullscreen mode

📜 Circuit compilation
Now let's compile the circuit by running the following command from the build/circuits folder:

circom ../../circuits/Multiplier.circom --r1cs --wasm --sym
Enter fullscreen mode Exit fullscreen mode

Now we have our circuit compiled to the build/circuits/Multiplier.sym, build/circuits/Multiplier.r1cs, and build/circuits/Multiplier.wasm files.

ALTBN-128 AND BLS12-381 CURVES
The altbn-128 and bls12-381 elliptic curves are currently supported by snarkjs. The altbn-128 curve is only supported on Ethereum. Furthermore, on Solana only the bn128 curve is supported.

Let's check the constraint size of our circuit by entering the following command:

node ../../../snarkjs/build/cli.cjs r1cs info Multiplier.r1cs
Enter fullscreen mode Exit fullscreen mode

Therefore, the correct result should be:

[INFO]  snarkJS: Curve: bn128
[INFO]  snarkJS: # of Wires: 4
[INFO]  snarkJS: # of Constraints: 1
[INFO]  snarkJS: # of Private Inputs: 2
[INFO]  snarkJS: # of Public Inputs: 0
[INFO]  snarkJS: # of Labels: 4
[INFO]  snarkJS: # of Outputs: 1
Enter fullscreen mode Exit fullscreen mode

Now we can generate the reference zkey by executing the following:

node ../../../snarkjs/build/cli.cjs zkey new Multiplier.r1cs pot12_final.ptau Multiplier_0000.zkey
Enter fullscreen mode Exit fullscreen mode

Then we’ll add the below contribution to the zkey:

echo "some random text" | node ../../../snarkjs/build/cli.cjs zkey contribute Multiplier_0000.zkey Multiplier_final.zkey --name="1st Contributor" -v -e="more random text"
Enter fullscreen mode Exit fullscreen mode

Next, let's export the final zkey:

node ../../../snarkjs/build/cli.cjs zkey export verificationkey Multiplier_final.zkey verification_key.json
Enter fullscreen mode Exit fullscreen mode

Then we’ll remove the unnecessary files:

rm Multiplier_0000.zkey
Enter fullscreen mode Exit fullscreen mode

After conducting the above processes, the build/circuits folder should be displayed as follows:

build
└── circuits
├── Multiplier_final.zkey
├── Multiplier.r1cs
├── Multiplier.sym
├── Multiplier.wasm
├── pot12_final.ptau
└── verification_key.json

✅ Exporting the verifier contract
The final step in this section is to generate the Solana verifier contract which we’ll use in our ZK project.

cd ../../../groth16-solana 
npm run parse-vk ../circuits/build/circuits/verification_key.json
Enter fullscreen mode Exit fullscreen mode

Then the verifying_key.rs file is generated in the groth16-solana folder.

🚢 Verifier contract deployment​
Let's review the groth16-solana/verifying_key.rs file step-by-step because it contains the magic of ZK-SNARKs:

use groth16_solana::groth16::Groth16Verifyingkey;

pub const VERIFYINGKEY: Groth16Verifyingkey =  Groth16Verifyingkey {
    nr_pubinputs: 2,

    vk_alpha_g1: [
        46,198,28,80,85,219,64,95,16,86,37,55,105,174,107,82,67,212,66,53,189,244,65,129,153,249,14,192,208,23,189,255,
        46,213,140,248,251,76,224,235,78,79,47,37,11,253,131,73,220,6,86,57,31,37,150,116,245,62,83,76,46,234,4,132,
    ],

    vk_beta_g2: [
        40,239,114,219,169,186,198,208,56,242,155,131,18,151,60,17,8,209,95,232,155,207,165,191,9,240,203,222,208,254,251,118,
        23,244,194,167,148,204,162,27,134,119,235,184,191,212,15,40,60,80,108,153,207,223,171,38,222,36,166,12,84,109,176,64,
        14,48,121,113,255,10,248,201,22,70,211,61,239,180,243,240,193,117,248,132,92,108,103,68,96,104,143,249,30,26,84,65,
        6,82,118,185,31,130,171,49,7,175,0,68,128,209,81,253,68,111,106,183,12,127,60,70,105,211,9,21,170,72,70,58,
    ],

    vk_gamme_g2: [
        25,142,147,147,146,13,72,58,114,96,191,183,49,251,93,37,241,170,73,51,53,169,231,18,151,228,133,183,174,243,18,194,
        24,0,222,239,18,31,30,118,66,106,0,102,94,92,68,121,103,67,34,212,247,94,218,221,70,222,189,92,217,146,246,237,
        9,6,137,208,88,95,240,117,236,158,153,173,105,12,51,149,188,75,49,51,112,179,142,243,85,172,218,220,209,34,151,91,
        18,200,94,165,219,140,109,235,74,171,113,128,141,203,64,143,227,209,231,105,12,67,211,123,76,230,204,1,102,250,125,170,
    ],

    vk_delta_g2: [
        24,192,59,38,123,253,143,209,31,2,6,232,161,211,127,130,243,195,167,30,70,1,188,224,50,84,152,107,192,21,180,237,
        28,114,45,187,38,122,20,254,202,71,245,235,178,126,211,179,176,61,10,103,34,65,197,118,5,27,150,189,46,60,94,185,
        35,207,37,209,197,84,87,106,62,27,41,116,198,235,14,90,222,127,26,30,171,63,255,141,41,53,206,215,237,66,117,12,
        27,178,142,201,231,67,82,137,245,78,19,88,40,84,123,79,2,191,80,218,78,125,94,231,178,101,121,12,31,4,17,239,
    ],

    vk_ic: &[
        [
            26,117,81,38,33,231,103,28,33,207,192,9,40,163,239,213,143,75,215,83,82,99,239,43,187,29,215,94,171,232,109,97,
            1,34,199,15,152,146,18,109,191,206,154,14,65,233,113,21,117,171,39,197,154,75,176,199,151,75,117,63,170,66,13,98,
        ],
        [
            26,34,168,154,124,160,104,241,73,142,20,231,181,8,8,182,0,225,51,233,173,217,93,237,166,202,87,151,55,51,87,197,
            43,145,207,212,5,45,208,198,22,238,212,232,126,17,37,125,180,176,67,75,207,58,102,122,182,244,89,209,133,253,4,255,
        ],
    ]
};
Enter fullscreen mode Exit fullscreen mode

The below lines are the new altbn (altbn-128) that allows pairing checks to be conducted on the Solana Blockchain.

    pub fn prepare_inputs(&mut self) -> Result<(), Groth16Error> {
        let mut prepared_public_inputs = self.verifyingkey.vk_ic[0];

        for (i, input) in self.public_inputs.iter().enumerate() {
            let mul_res = alt_bn128_multiplication(
                &[&self.verifyingkey.vk_ic[i + 1][..], &input[..]].concat(),
            )
            .map_err(|_| Groth16Error::PreparingInputsG1MulFailed)?;
            prepared_public_inputs =
                alt_bn128_addition(&[&mul_res[..], &prepared_public_inputs[..]].concat())
                    .map_err(|_| Groth16Error::PreparingInputsG1AdditionFailed)?[..]
                    .try_into()
                    .map_err(|_| Groth16Error::PreparingInputsG1AdditionFailed)?;
        }

        self.prepared_public_inputs = prepared_public_inputs;

        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Next there are several simple util functions that are used to load the proof data sent to the contract:

And the last part is the groth16Verify function which is required to check the validity of the proof sent to the contract.

    pub fn verify(&mut self) -> Result<bool, Groth16Error> {
        self.prepare_inputs()?;

        let pairing_input = [
            self.proof_a.as_slice(),
            self.proof_b.as_slice(),
            self.prepared_public_inputs.as_slice(),
            self.verifyingkey.vk_gamme_g2.as_slice(),
            self.proof_c.as_slice(),
            self.verifyingkey.vk_delta_g2.as_slice(),
            self.verifyingkey.vk_alpha_g1.as_slice(),
            self.verifyingkey.vk_beta_g2.as_slice(),
        ]
        .concat();

        let pairing_res = alt_bn128_pairing(pairing_input.as_slice())
            .map_err(|_| Groth16Error::ProofVerificationFailed)?;

        if pairing_res[31] != 1 {
            return Err(Groth16Error::ProofVerificationFailed);
        }
        Ok(true)
    }
Enter fullscreen mode Exit fullscreen mode

🧑‍💻 Writing tests for the verifier

First, we’ll need to create a test file and import several packages that we will use in the test:

import * as snarkjs from "snarkjs";
import path from "path";
import {buildBn12381, utils} from "ffjavascript";
const {unstringifyBigInts} = utils;
Enter fullscreen mode Exit fullscreen mode

If you run the test, the result will be a TypeScript error, because we don't have a declaration file for the module 'snarkjs' & ffjavascript. This can be addressed by editing the tsconfig.json file in the root of the simple-zk folder. We'll need to change the strict option to false in that file
We'll also need to import the circuit.wasm and circuit_final.zkey files which will be used to generate the proof to send to the contract.

import * as web3 from "@solana/web3.js";
import * as snarkjs from "snarkjs";
import path from "path";
import { buildBn128, utils } from "ffjavascript";
import { expect } from "chai";
const { unstringifyBigInts } = utils;

const wasmPath = path.join(__dirname, "../../../circuits", "Multiplier.wasm");
const zkeyPath = path.join(__dirname, "../../../circuits", "Multiplier_final.zkey");

const PROGRAM_ID = new web3.PublicKey("8bg6eH9sKpjNNMzWxnqKUa94rTSHTeWRNXrHy5HeP6an");
const ACCOUNT_TO_QUERY = new web3.PublicKey("511wHA3Q7KV4rNL2WN3LY1t9DCNn5nYVujTU3RgQuHZS");
const connection = new web3.Connection(web3.clusterApiUrl('devnet'), "confirmed");
const wallet = web3.Keypair.generate();

describe('Groth16 Verifier', () => {
  it('should verify a valid proof', async () => {
    // Request an airdrop to fund the wallet
    const airdropSignature = await connection.requestAirdrop(
      wallet.publicKey,
      web3.LAMPORTS_PER_SOL * 1 // Request 1 SOL
    );
    await connection.confirmTransaction(airdropSignature);

    // Generate proof using snarkjs
    let input = { "a": 3, "b": 4 };
    let { proof, publicSignals } = await snarkjs.groth16.fullProve(input, wasmPath, zkeyPath);

    console.log(publicSignals)
    console.log(proof)
    let curve = await buildBn128();
    let proofProc = unstringifyBigInts(proof);
    publicSignals = unstringifyBigInts(publicSignals);

    let pi_a = g1Uncompressed(curve, proofProc.pi_a);
    let pi_a_0_u8_array = Array.from(pi_a);
    console.log(pi_a_0_u8_array);

    // pi_a = reverseEndianness(pi_a)
    // pi_a = negateG1(curve, pi_a); // Negate pi_a
    // pi_a = reverseEndianness(pi_a); // Reverse endianness of negated pi_a

    const pi_b = g2Uncompressed(curve, proofProc.pi_b);
    let pi_b_0_u8_array = Array.from(pi_b);
    console.log(pi_b_0_u8_array.slice(0, 64));
    console.log(pi_b_0_u8_array.slice(64, 128));

    const pi_c = g1Uncompressed(curve, proofProc.pi_c);
    let pi_c_0_u8_array = Array.from(pi_c);
    console.log(pi_c_0_u8_array);

  // Assuming publicSignals has only one element
    const publicSignalsBuffer = to32ByteBuffer(BigInt(publicSignals[0]));
    let public_signal_0_u8_array = Array.from(publicSignalsBuffer);
    console.log(public_signal_0_u8_array);

   /* Only uncomment this when you have deployed the solana program on version 1.17
    const transaction = new web3.Transaction();

    transaction.add(web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }));

    transaction.add(web3.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 2 }));

    // Define the accounts involved in the transaction
    const accounts = [
      {
        pubkey: wallet.publicKey, // The public key of the account sending the proof
        isSigner: true,
        isWritable: true
      },
      {
        pubkey: ACCOUNT_TO_QUERY,
        isSigner: false,
        isWritable: true
      },
    ];

  const serializedData = Buffer.concat([
    pi_a,
    pi_b,
    pi_c,
    publicSignalsBuffer
  ]);

    // Create the instruction
    const instruction = new web3.TransactionInstruction({
      keys: accounts,
      programId: PROGRAM_ID,
      data: serializedData // The data containing the proof and public inputs
    });


    // Add the instruction to the transaction
    transaction.add(instruction);

    // Sign and send the transaction
    const signature = await web3.sendAndConfirmTransaction(
      connection,
      transaction,
      [wallet], // Array of signers, in this case, just the wallet
      {
        skipPreflight: true
      }
    );

    console.log("Transaction signature", signature);

    // Send and confirm transaction
    await web3.sendAndConfirmTransaction(
      connection,
      transaction,
      [wallet]
    );

    // Fetch and assert the result
    // const ctxRes = await getRes(PROGRAM_ID);
    // expect(ctxRes).not.to.equal(0); // Replace with your expected result
  });
*/
});
Enter fullscreen mode Exit fullscreen mode

To carry out the next step it is necessary to define the g1Compressed, g2Compressed, and to32ByteByffer functions. They will be used to convert the cryptographic proof to the format that the contract expects.

function g1Uncompressed(curve, p1Raw) {
  let p1 = curve.G1.fromObject(p1Raw);

  let buff = new Uint8Array(64); // 64 bytes for G1 uncompressed
  curve.G1.toRprUncompressed(buff, 0, p1);

  return Buffer.from(buff);
}

function g2Uncompressed(curve, p2Raw) {
  let p2 = curve.G2.fromObject(p2Raw);

  let buff = new Uint8Array(128); // 128 bytes for G2 uncompressed
  curve.G2.toRprUncompressed(buff, 0, p2);

  return Buffer.from(buff);
}

function to32ByteBuffer(bigInt) {
  const hexString = bigInt.toString(16).padStart(64, '0'); 
  const buffer = Buffer.from(hexString, "hex");
  return buffer; 
}
Enter fullscreen mode Exit fullscreen mode

Now we can send the cryptographic proof to the contract.
But for the incompatible version of Solana right now (released in v1.17), we will write a test in our rust program using our generated proof data.

Find the logs of proof buffer data

// other codes...
console.log(pi_a_0_u8_array)

// other codes...

console.log(pi_b_0_u8_array.slice(0, 64));
console.log(pi_b_0_u8_array.slice(64, 128));

// other codes ...

console.log(pi_c_0_u8_array)

// other codes ...
console.log(publicSignal_0_u8_array)
Enter fullscreen mode Exit fullscreen mode

Remember these data, you will need to use them in the rust program soon.

Now let's take a look at our groth16.rs. Navigate to #config(tests). Then add our own test variables:

// other codes...

pub const VERIFYING_KEY_2: Groth16Verifyingkey; // using your verifying key here

// other codes ...

pub const PROOF_2: [u8; 256]; //... concat pi_a_0_u8_array, pi_b_0_u8_array.slice(0, 64), pi_b_0_u8_array.slice(64, 128), pi_c_0_u8_array in your console.log before

// other codes ...
pub const PUBLIC_INPUTS_2: [[u8; 32]; 1]; // use publicSignal_0_u8_array in your console.log step before
Enter fullscreen mode Exit fullscreen mode

Finally, write custom test function with our own data

    #[test]
    fn proof_2_verification_should_succeed() {
        let proof_a: G1 = G1::deserialize_with_mode(
            &*[&change_endianness(&PROOF_2[0..64]), &[0u8][..]].concat(),
            Compress::No,
            Validate::Yes,
        )
        .unwrap();
        let mut proof_a_neg = [0u8; 65];
        proof_a
            .neg()
            .x
            .serialize_with_mode(&mut proof_a_neg[..32], Compress::No)
            .unwrap();
        proof_a
            .neg()
            .y
            .serialize_with_mode(&mut proof_a_neg[32..], Compress::No)
            .unwrap();

        let proof_a = change_endianness(&proof_a_neg[..64]).try_into().unwrap();
        let proof_b = PROOF_2[64..192].try_into().unwrap();
        let proof_c = PROOF_2[192..256].try_into().unwrap();

        let mut verifier =
            Groth16Verifier::new(&proof_a, &proof_b, &proof_c, &PUBLIC_INPUTS_2, &VERIFYING_KEY_2)
                .unwrap();
        verifier.verify().unwrap();
    }
Enter fullscreen mode Exit fullscreen mode

Are you ready to verify your first proof on Solana blockchain? To start off this process, let's run the rust test by inputting the following:

cargo test
Enter fullscreen mode Exit fullscreen mode

The result should be as follows:

    Finished test [unoptimized + debuginfo] target(s) in 0.87s
     Running unittests src/lib.rs (target/debug/deps/solanazk-fcce2a686ac6f227)

running 4 tests
test groth16::tests::proof_2_verification_should_succeed ... ok
test groth16::tests::proof_verification_should_succeed ... ok
test groth16::tests::wrong_proof_verification_should_not_succeed ... ok
test groth16::tests::proof_verification_with_compressed_inputs_should_succeed ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.43s

   Doc-tests solanazk

running 1 test
test src/groth16.rs - groth16 (line 1) ... ignored

test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Enter fullscreen mode Exit fullscreen mode

Now you are ready to deploy your program to devnet/testnet when Solana v1.17 comes out (with verification taking less than 200_000 compute units). The Typescript test will fail for now. But when it comes, just replace the PROGRAM_ID with your newly deployed one then you will be alright.

In order to check the repo that contains the code from this tutorial, click on this link.

🏁 Conclusion
In this tutorial you learned the following skills:

  • The intricacies of zero-knowledge and specifically ZK-SNARKs
  • Writing and compiling Circom circuiting ( Increased familiarity with MPC and the Powers of TAU, which were used to generate verification keys for a circuit )
  • Became familiar with a Snarkjs library to export a Solana verifier for a circuit
  • Became familiar with native Solana for verifier deployment and test writing

Note: The above examples taught us how to build a simple ZK use case. That said, there are a wide range of highly complex ZK-focused use cases that can be implemented in a wide range of industries. Some of these include:

private voting systems 🗳
private lottery systems 🎰
private auction systems 🤝
private identity/oracle 👤
private poker game ♠️
private transaction layer 💸 (Elusiv)
private machine learning 🤖
private Zk-Rollups 🗞️
private stablecoin 🏦
private ZK light client for trustless bridging 🌉

If you have any questions or have noticed an error - feel free to write to the author - @zeref101

📌 References
Solana v1.16 Upgrade
groth16-solana
SnarkJS
create-solana-dapp CLI
Circom
Tutorial Repo
ZK Comparision
zkSTARK

📖 See Also

Solana ZK Tour (Dec 2023)
State of Privacy on Solana (July 2023)
Confidential Token on Solana
Solana ZK Light Client for a Trustless Bridge
Succinct's vision on ZK
Private Solana Program 🎭 (Light Protocol)
Another Circom Verifier
Otter Cash: Riptide Hackathon Winner

📬 About the author
Zeref on Telegram or Github

Top comments (4)

Collapse
 
ppoliani profile image
Pavlos Polianidis • Edited

@zeref101 quick question. I've got a circuit with the following public inputs

  signal output out1[5];
  signal output address;
Enter fullscreen mode Exit fullscreen mode

When I create the proof.json it ends up having the following values:


[
"50",
"50",
"30",
"10",
"45",
"12737771764621907568098507625979260347390739857011548125897812973048106564980"
]

Enter fullscreen mode Exit fullscreen mode

out1 is basically an array with 5 bytes e.g. values between 0 and 255. The address output though is a poseidon hash. As I understand it's a big value in the Circom curve's Field set.

How do I pass this into the on-chain program? Do I call it this way?

const publicSignalsBuffer = publicSignals.map((i) => to32ByteBuffer(BigInt(i)))

Update

I can't get this example verity the ZKP successfully. It feels like the JS code is missing some transformation on the proof_a similar to what the rust code does here github.com/Lightprotocol/groth16-s...

For anyone interested, I have written a Rust crate and bundled it into wasm to use it directly in JS. Check it out here github.com/Apocentre/solana-zk-exa...

Collapse
 
ppoliani profile image
Pavlos Polianidis

This is an awesome article. Thanks for this tutorial. May I ask you if you can share the code for to32ByteBuffer?

Collapse
 
ppoliani profile image
Pavlos Polianidis • Edited

Ended up doing something like this:

const to32ByteBuffer = (num) => {
  const result = new Uint8Array(32);
  let i = 0;

  while (num > 0n) {
    result[i] = Number(num % 256n);
    num = num / 256n;
    i += 1;
  }

    return result;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sufferer profile image
Sufferer

Thanks, Pavlos! Good catch on the missing code. I've updated the post. I'm not sure about your solution, but if it works, then great! 🙌