In this tutorial, we will explore the implementation differences between zk-SNARKs in SnarkyJS and Solidity using Circom, SnarkJS, and Solidity.
SnarkyJS is a TypeScript library that makes it easy for developers to build powerful ZK applications without being cryptographers. Smart contracts written with SnarkyJS run in a user's web browser retaining privacy for the user's data inputs.
Circom is a programming language used to write programs called "circuits". These circuits take a set of inputs, describe constraints about them, and can have one, many, or no outputs.
Let's say we have a private key that should not be revealed to anyone, but its possession is vital. It could be a private key associated with an account or a symmetric key used to encrypt a file. We want to prove that we know the key without revealing it, using zero-knowledge proof (ZKP) zk-SNARKs. Specifically, we will use the Poseidon hash function.
Poseidon is a hash function designed to minimize the complexities for both the prover and verifier when generating and validating zero-knowledge proofs. Compared to competitor hash functions, such as Pedersen Hash, Poseidon uses up to 8x fewer constraints per message bit, making it significantly faster.
see also Installing the circom ecosystem docs
Download Rustup and install Rust
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Check version
rustup --version
On mac:
brew install node
or Download Nodejs here
On linux:
sudo apt install nodejs
sudo apt install npm
check version:
node -v
npm -v
To install from circom sources, clone the circom repository:
git clone https://github.com/iden3/circom.git
Enter the circom directory and use the cargo build to compile:
cargo build --release
then
cargo install --path circom
Check version
circom --version
npm install -g snarkjs
We will create a new directory called circom-snarkjs-solidity. Within this directory, we will create a package.json file and add the circomlib package as a dev dependency:
npm init
npm install --save-dev circomlib
or
yarn add -D circomlib
Create a new directory:
mkdir circom-snarkjs-solidity
Create a new circom file poseidon.circom, see also poseidon.circom
This file has 2 inputs and output the poseidon hash.
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/poseidon.circom";
template PoseidonHash() {
var nInputs = 2;
signal input inputs[nInputs];
signal output out;
component hasher = Poseidon(nInputs);
for (var i = 0; i < nInputs; i ++) {
hasher.inputs[i] <== inputs[i];
}
out <== hasher.out;
}
component main = PoseidonHash();
This is a code snippet written in the Circom programming language that defines a circuit that performs a hash function using the Poseidon algorithm. Let's break it down:
What is circom? Circom is a domain-specific language (DSL) used for the specification and implementation of arithmetic circuits. It is designed specifically for use in zero-knowledge proof (ZKP) systems and is commonly used in conjunction with zk-SNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge).
Circom allows developers to easily create and verify ZKP systems that are both efficient and secure. It is especially useful for creating circuits that involve complex mathematical computations, as it provides a convenient and intuitive way to express these computations in code. Overall, Circom simplifies the process of building ZKP systems and helps to ensure that they are correct and secure.
This line specifies the version of Circom being used.
pragma circom 2.0.0;
This line includes the Poseidon circuit implementation from the Circom library.
include "../node_modules/circomlib/circuits/poseidon.circom";
This defines a template for a circuit called "PoseidonHash" that takes two input signals and produces one output signal. The template defines a component called "hasher" that implements the Poseidon algorithm, and connects the inputs of the hasher component to the input signals of the template. The output signal of the hasher component is then connected to the output signal of the template.
template PoseidonHash() {
...
}
Now that we have written our circuit, the next step is to compile it using Circom. This will generate a JavaScript file that we can use to generate a proof using SnarkJS.
To compile the circuit, we can run the following command:
mkdir ./circom-snarkjs-solidity/compiled
circom ./circom-snarkjs-solidity/PoseidonHash.circom --r1cs --wasm --sym --c -o ./circom-snarkjs-solidity/compiled
or run
npm run compile:circuit
yarn compile:circuit
What is a witness? Before creating the proof, we need to calculate all the signals of the circuit that match all the constraints of the circuit. For that, we will use the Snarkjs that helps to do this job.
Computing the witness with Snarkjs Check the Snarkjs information:
snarkjs r1cs info ./circom-snarkjs-solidity/compiled/PoseidonHash.r1cs
As you can see above there are 2 private inputs on our circuit and 1 output with 240 contraints. The most of the constraints comes from poseidon.circom
include "../node_modules/circomlib/circuits/poseidon.circom";
Create your input file in json, this is your privatekey or your secret that you don't want to share
After compiling the circuit and running the witness calculator with an appropriate input, we will have a file with extension .wtns that contains all the computed signals and, a file with extension .r1cs that contains the constraints describing the circuit. Both files will be used to create our proof.
Now, we will use the snarkjs tool to generate and validate a proof for our input. In particular, using the multiplier2, we will prove that we are able to provide the two factors of the number 33. That is, we will show that we know two integers a and b such that when we multiply them, it results in the number 33.
We are going to use the Groth16 zk-SNARK protocol. To use this protocol, you will need to generate a trusted setup. Groth16 requires a per circuit trusted setup. In more detail, the trusted setup consists of 2 parts:
The powers of tau, which is independent of the circuit. The phase 2, which depends on the circuit. Next, we provide a very basic ceremony for creating the trusted setup and we also provide the basic commands to create and verify Groth16 proofs. Review the related Background section and check the snarkjs tutorial for further information.
First, we start a new "Powers of Tau" ceremony:
Powers of Tau // Explain power of Tau here
snarkjs powersoftau new bn128 10 ./circuits/powersOfTau28_hez_final_10.ptau
Then, we contribute to the ceremony:
cd circuits
snarkjs powersoftau contribute powersOfTau28_hez_final_10.ptau pot10_0001.ptau --name="First contribution" -v
Now, we have the contributions to the powers of tau in the file pot10_0001.ptau and we can proceed with the Phase 2.
Phase 2 The phase 2 is circuit-specific. Execute the following command to start the generation of this phase:
cd circuits
snarkjs powersoftau prepare phase2 pot10_0001.ptau pot10_final.ptau -v
Next, we generate a .zkey file that will contain the proving and verification keys together with all phase 2 contributions. Execute the following command to start a new zkey:
snarkjs groth16 setup ./circom-snarkjs-solidity/Poseidon/poseidonHashT3.r1cs pot10_final.ptau poseidonHashT3_0000.zkey
Contribute to the phase 2 of the ceremony:
snarkjs zkey contribute poseidonHashT3_0000.zkey poseidonHashT3_0001.zkey --name="1st Contributor Name" -v
Export the verification key:
snarkjs zkey export verificationkey poseidonHashT3_0001.zkey verification_key.json
Witness calculation: Once the witness is computed and the trusted setup is already executed, we can generate a zk-proof associated to the circuit and the witness:
snarkjs groth16 prove poseidonHashT3_0001.zkey witness.wtns proof.json public.json
To verify the proof, execute the following command:
snarkjs groth16 verify verification_key.json public.json proof.json
The command uses the files verification_key.json we exported earlier,proof.json and public.json to check if the proof is valid. If the proof is valid, the command outputs an OK.
A valid proof not only proves that we know a set of signals that satisfy the circuit, but also that the public inputs and outputs that we use match the ones described in the public.json file.
โ๐ It is also possible to generate a Solidity verifier that allows verifying proofs on Ethereum blockchain.
First, we need to generate the Solidity code using the command:
snarkjs zkey export solidityverifier poseidonHashT3_0001.zkey verifier.sol
This command takes validation key poseidonHashT3_0001.zkey and outputs Solidity code in a file named verifier.sol. You can take the code from this file and cut and paste it in Remix. You will see that the code contains two contracts: Pairing and Verifier. You only need to deploy the Verifier contract.
You may want to use first a testnet like Rinkeby, Kovan or Ropsten. You can also use the JavaScript VM, but in some browsers the verification takes long and the page may freeze.
The Verifier has a view function called verifyProof that returns TRUE if and only if the proof and the inputs are valid. To facilitate the call, you can use snarkJS to generate the parameters of the call by typing:
snarkjs generatecall
Cut and paste the output of the command to the parameters field of the verifyProof method in Remix. If everything works fine, this method should return TRUE. You can try to change just a single bit of the parameters, and you will see that the result is verifiable FALSE.
I have create scripts files to automate the above steps, just run the following command
chmod a+x ./scripts/compile-circuit.sh ./scripts/compile-poseidon-circuit.sh
./scripts/compile-poseidon-circuit.sh
Install dependencies:
yarn add -D ts-node typescript
yarn add -D chai @types/node @types/mocha @types/chai
yarn add -D circomlibjs circom_tester
Run circuit test
yarn test
npm install -g zkapp-cli
check version
zkapp --version
zk project snarkyjs
Choose none with the question "Create an accompanying UI project too?"
cd snarkyjs
Secret.ts:
import { Field, Poseidon, Struct } from 'snarkyjs';
export { Secret };
class Secret extends Struct({
secret: Field,
}) {
static from(secret: Field): Secret {
return new Secret({ secret });
}
static empty(): Secret {
return new Secret({ secret: Field(0) });
}
hash(): Field {
return Poseidon.hash(this.secret.toFields());
}
}
Add.ts
import { Field, SmartContract, state, State, method } from 'snarkyjs';
import { Secret } from './Secret';
export class Add extends SmartContract {
@state(Field) num = State<Field>();
@state(Secret) secret = State<Secret>();
init() {
super.init();
this.num.set(Field(1));
this.secret.set(Secret.empty());
}
@method update() {
const currentState = this.num.get();
this.num.assertEquals(currentState); // precondition that links this.num.get() to the actual on-chain state
const newState = currentState.add(2);
this.num.set(newState);
}
@method setSecret(secret: Field) {
this.secret.set(Secret.from(secret));
}
@method getSecret(): Field {
return this.secret.get().hash();
}
}
npm run build
npm run test
To conclude, in this tutorial we have explored the differences between using zk-SNARKs with SnarkyJS versus using Circom, SnarkJS, and Solidity. We have shown how to use Poseidon hash in a circuit written in Circom, and how to compile and generate the verification code for the circuit using SnarkJS. We have also seen how to use Solidity to interact with the generated verification code and deploy a smart contract on the Ethereum blockchain.
Circom provides a convenient way to write and verify arithmetic circuits for use in ZKP systems. It simplifies the process of building ZKP systems and helps to ensure their correctness and security. SnarkJS provides the tools needed to compile and generate the verification code for the circuit, which can then be used with Solidity to deploy a smart contract on the blockchain.
Overall, the choice between using SnarkyJS and Circom, SnarkJS, and Solidity depends on the specific requirements of the application. Developers may prefer SnarkyJS for its ease of use and compatibility with TypeScript, while others may prefer the flexibility and performance of using Circom, SnarkJS, and Solidity.