Pairing operations over BLS12-381 are cheaper than BN254 on Garaga, so if possible, prefer using BLS12-381. The security of this curve is also much closer to 128 bits than BN254.
To deploy a groth16 verifier, you need the associated verifying key in .json format. Both BN254 and BLS12-381 are supported.
Snarkjs and Gnark jsons output are supported out of the box.
While Snarkjs jsons export are easy and working well, the gnark documentation is a bit outdated. Below we show a quick example on how to export the verifying key from your Gnark circuit .
Gnark export to json example
packagemainimport ("encoding/json""os""github.com/consensys/gnark-crypto/ecc""github.com/consensys/gnark/backend/groth16""github.com/consensys/gnark/frontend""github.com/consensys/gnark/frontend/cs/r1cs")// CubicCircuit defines a simple circuit// x**3 + x + 5 == ytypeCubicCircuitstruct {// struct tags on a variable is optional// default uses variable name and secret visibility. X frontend.Variable`gnark:"x"` Y frontend.Variable`gnark:",public"` B frontend.Variable A frontend.Variable`gnark:",public"`}// Define declares the circuit constraints// x**3 + x + 5 == yfunc (circuit *CubicCircuit) Define(api frontend.API) error { x3 := api.Mul(circuit.X, circuit.X, circuit.X) api.AssertIsEqual(circuit.Y, api.Add(x3, circuit.X, 5)) api.AssertIsEqual(circuit.B, circuit.A)returnnil}funcmain() {// compiles our circuit into a R1CSvar circuit CubicCircuit ccs, _ := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit)// groth16 zkSNARK: Setup pk, vk, _ := groth16.Setup(ccs)// witness definition assignment :=CubicCircuit{X: 3, Y: 35, A: 3, B: 3} witness, _ := frontend.NewWitness(&assignment, ecc.BN254.ScalarField()) publicWitness, _ := witness.Public()// groth16: Prove & Verify proof, _ := groth16.Prove(ccs, pk, witness) groth16.Verify(proof, vk, publicWitness) vk.ExportSolidity(os.Stdout)// Export to JSON: schema, _ := frontend.NewSchema(&circuit) pubWitnessJSON, _ := publicWitness.ToJSON(schema)SaveToJSON("gnark_vk_bn254.json", vk)SaveToJSON("gnark_proof_bn254.json", proof) os.WriteFile("gnark_public_bn254.json", pubWitnessJSON, 0644)}funcSaveToJSON(filePath string, v interface{}) error { jsonData, err := json.MarshalIndent(v, "", " ")if err !=nil {return err } err = os.WriteFile(filePath, jsonData, 0644)if err !=nil {return err }returnnil}
The curve identifier is automatically detected from the content of your verifying key.
Build your contract
The generated verifier contract does nothing besides verifying a proof! You must extend the template to add all the extra logic needed for your dapp, for example calling an external contract and sending it the public inputs.
The generated template is as follow. The main endpoint to call is verify_groth16_proof_[curve_name]
If the verification succeeds, it will call the internal function process_public_inputs.
This function is the starting point of your dapp logic.
groth16_verifier.cairo
use garaga::definitions::E12DMulQuotient;use garaga::groth16::{Groth16Proof, MPCheckHintBLS12_381};use super::groth16_verifier_constants::{N_PUBLIC_INPUTS, vk, ic, precomputed_lines};#[starknet::interface]traitIGroth16VerifierBLS12_381<TContractState> {fnverify_groth16_proof_bls12_381(ref self:TContractState, groth16_proof:Groth16Proof, mpcheck_hint:MPCheckHintBLS12_381, small_Q:E12DMulQuotient, msm_hint:Array<felt252>, ) ->bool;}#[starknet::contract]modGroth16VerifierBLS12_381 {use starknet::SyscallResultTrait;use garaga::definitions::{G1Point, G1G2Pair, E12DMulQuotient};use garaga::groth16::{ multi_pairing_check_bls12_381_3P_2F_with_extra_miller_loop_result, Groth16Proof,MPCheckHintBLS12_381 };use garaga::ec_ops::{G1PointTrait, G2PointTrait, ec_safe_add};use super::{N_PUBLIC_INPUTS, vk, ic, precomputed_lines};const ECIP_OPS_CLASS_HASH: felt252 =0x29aefd3c293b3d97a9caf77fac5f3c23a6ab8c7e70190ce8d7a12ac71ceac4c;use starknet::ContractAddress; #[storage]structStorage {} #[abi(embed_v0)]implIGroth16VerifierBLS12_381 of super::IGroth16VerifierBLS12_381<ContractState> {fnverify_groth16_proof_bls12_381(ref self:ContractState, groth16_proof:Groth16Proof, mpcheck_hint:MPCheckHintBLS12_381, small_Q:E12DMulQuotient, msm_hint:Array<felt252>, ) ->bool {// DO NOT EDIT THIS FUNCTION UNLESS YOU KNOW WHAT YOU ARE DOING.// ONLY EDIT THE process_public_inputs FUNCTION BELOW. groth16_proof.a.assert_on_curve(1); groth16_proof.b.assert_on_curve(1); groth16_proof.c.assert_on_curve(1);let ic = ic.span();let vk_x:G1Point=match ic.len() {0=>panic!("Malformed VK"),1=>*ic.at(0), _ => {// Start serialization with the hint array directly to avoid copying it.letmut msm_calldata:Array<felt252> = msm_hint;// Add the points from VK and public inputs to the proof.Serde::serialize(@ic.slice(1, N_PUBLIC_INPUTS), ref msm_calldata);Serde::serialize(@groth16_proof.public_inputs, ref msm_calldata);// Complete with the curve indentifier (1 for BLS12_381): msm_calldata.append(1);// Call the multi scalar multiplication endpoint on the Garaga ECIP ops contract// to obtain vk_x.letmut _vx_x_serialized = core::starknet::syscalls::library_call_syscall( ECIP_OPS_CLASS_HASH.try_into().unwrap(),selector!("msm_g1"), msm_calldata.span() ).unwrap_syscall();ec_safe_add( Serde::<G1Point>::deserialize(ref _vx_x_serialized).unwrap(), *ic.at(0), 1 ) } };// Perform the pairing check.let check =multi_pairing_check_bls12_381_3P_2F_with_extra_miller_loop_result(G1G2Pair { p: vk_x, q: vk.gamma_g2 },G1G2Pair { p: groth16_proof.c, q: vk.delta_g2 },G1G2Pair { p: groth16_proof.a.negate(1), q: groth16_proof.b }, vk.alpha_beta_miller_loop_result, precomputed_lines.span(), mpcheck_hint, small_Q );if check ==true { self.process_public_inputs( starknet::get_caller_address(), groth16_proof.public_inputs );returntrue; } else {returnfalse; } } } #[generate_trait]implInternalFunctions of InternalFunctionsTrait {fnprocess_public_inputs(ref self:ContractState, user:ContractAddress, public_inputs:Span<u256>, ) { // Process the public inputs with respect to the caller address (user).// Update the storage, emit events, call other contracts, etc. } }}
Declare and deploy your contract
When you are satisfied with your contract it's time to send it to Starknet!
The CLI provides utilities to simplify the process. Otherwise, it is a similar process than for every other contracts, please refer to Starknet documentation.
You will to create a file containing the following variables, depending on if you want to declare & deploy on Starknet Sepolia or Starknet Mainnet.
Create the following file and update its content.
Then, you can run the command garaga declare-project, which will build the contract and declare it to Starknet. If the class hash is already deployed, it will return it as well. Declaring the contract involves sending all its bytecode and it is quite an expensive operation. Make sure you dapp is properly tested before!
Usage: garaga declare-project [OPTIONS]
Declare your smart contract to Starknet. Obtain its class hash and a explorer link.
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --project-path DIRECTORY Path to the Cairo project holding the Scarb.toml file to declare │
│ [default: /home/felt/PycharmProjects/garaga-flow/my_project/] │
│ --env-file FILE Path to the environment file │
│ [default: /home/felt/PycharmProjects/garaga-flow/my_project/.secrets] │
│ --network [sepolia|mainnet] Starknet network [default: sepolia] │
│ --fee TEXT Fee token type [eth, strk] [default: eth] │
│ --help -h Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
This command will return the class hash, used in the next step.
Finally, to deploy the contract, a use garaga deploy-project :
Usage: garaga deploy-project [OPTIONS]
Deploy an instance of a smart contract class hash to Starknet. Obtain its address, the available endpoints and a explorer link.
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * --class-hash TEXT Contract class hash to deploy. Can be decimal or hex string [default: None] [required] │
│ --env-file FILE Path to the environment file containing rpc, address, private_key │
│ [default: /home/felt/PycharmProjects/garaga-flow/my_project/.secrets] │
│ --network [sepolia|mainnet] Starknet network [default: sepolia] │
│ --fee TEXT Fee token type [eth, strk] [default: strk] │
│ --help -h Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
The contract address will be prompted. Be sure to save it!