Plonky2
For users of Plonky2.
This tutorial corresponds to the v0.2.2
version of Plonky2.
Introductionβ
We will walk through the process of how to create a circuit using the Plonky2 library that proves a correct Merkle tree inclusion proof. The complete and finished code for the circuit can be found here along with other example projects in our sindri-resources repository. You can also check out the Plonky2 overview from Polymer Labs for a general overview of how to use Plonky2. An overview of the role that Merkle trees and Merkle inclusion proofs serve in the blockchain space can be found here.
Merkle Tree Circuitβ
The Merkle tree circuit implementation is based on the sample Plonky2 circuits provided by Hashcloak. Using this implementation requires the following steps:
- Importing the leaves for the leaf node layer of the Merkle tree and selecting the index of the leaf for which we want to prove inclusion. Along with the leaf nodes, this leaf index and corresponding Merkle path will be included as public inputs to the circuit.
- Constructing a full Merkle tree from the leaf nodes.
We use the
merkle_tree.rs
file to generate the hashes for every node in the Merkle tree and also to generate a Merkle path for the leaf whose inclusion is being proved. This file is based on the on Hashcloak's implementation. - Importing the node hashes and the the Merkle path indices as public inputs to the circuit. The Merkle root hash is the last public input to the circuit and is comprised of four 64-bit words.
- Proving the inclusion of the leaf in the Merkle tree using the Plonky2 Merkle verification circuit.
For this tutorial, we will use a 1024 leaf Merkle tree and prove membership for the first leaf. Note that the input values are all modulo the Goldilocks 64-bit prime, which defines the prime field for Plonky2.
Circuit Configurationβ
The circuit is organized in a directory containing the following files:
βββ πcircuit
β βββ πCargo.toml
β βββ πrust-toolchain
β βββ πsindri.json
β βββ πsrc
β βββ πlib.rs
β βββ πmerkle_tree.rs
The src/lib.rs
contains a prove method for the circuit function.
The Plonky2 circuit construction needs to be defined inside of this prove method.
The prove method is responsible for constructing the arithmetic circuit, loading any input data, configuring the partial witness, and proving the circuit.
Helper methods can be defined in the lib.rs
or in a separate module and imported into the lib.rs
file.
For instance, the merkle_tree.rs module contains methods to generate the root hash of a merkle tree along with along with a merkle path and required hashes to verify a Merkle proof.
The helper function verify_merkle_proof_circuit creates the arithmetic circuit for verifying a Merkle tree proof using the Plonky2 CircuitBuilder struct.
For detailed descriptions of the other files, you can click through the different tabs here.
- sindri.json
- Cargo.toml
- Circuit Definition
For any circuit using a Sindri supported framework, you will need to include a sindri.json
file defining certain metadata about your circuit.
There are a few key pieces of information here that are required:
- The
packageName
andplonky2Version
must be included and match the values in yourCargo.toml
file. We only support Plonky2 versionsv0.2
,v0.2.1
, andv0.2.2
. - The
provingScheme
andcircuitType
must both be set toplonky2
. - The
structName
must be the path to the circuit struct in yourlib.rs
file which implements theprove
method. To meet the requirements of the interface, thestructName
in your sindri.json must match the name of your circuit struct.
The package name in your Cargo.toml
file should match the name in the sindri.json
file.
[package]
name = "merkle_tree"
version = "0.1.0"
edition = "2021"
[dependencies]
itertools = "0.13.0"
num = "0.4.3"
plonky2 = "0.2.2"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
Your core circuit logic must be defined in the prove method of your circuit struct. This prove method must include the following components:
- Code to import input data and format it to Goldilocks Field field elements.
- Creating a circuit scaffold using the Plonky2 CircuitBuilder struct.
You can optionally define a helper method that builds the circuit.
- Instantiating the Plonky2 partial witness and populating it with public inputs.
- Invoking the prove() method on the CircuitBuilder struct. The prove method takes in the partial witness as its sole input.
- Return an instance of your circuit struct.
Circuit explanationβ
Sindri APIβ
We'll walk through the steps to upload your Plonky2 circuit to Sindri, compile it, generate proofs, and verify proofs. We will use a Rust script to interact with the Sindri API.
Setupβ
Clone the Sindri resources repository and move into the merkle_tree
tutorial.
git clone https://github.com/Sindri-Labs/sindri-resources.git
cd sindri-resources/circuit_tutorials/plonky2/merkle_tree
Make sure that the merkle_tree
project directory contains the following files:
π¦ merkle_tree
βββ πCargo.toml
βββ πinput_1024.json
βββ πcircuit
β βββ πCargo.toml
β βββ πrust-toolchain
β βββ πsindri.json
β βββ πsrc
β βββ πlib.rs
β βββ πmerkle_tree.rs
βββ πREADME.md
βββ πrust-toolchain
βββ πsample.env
βββ πsrc
βββ πmain.rs
The circuit described above is contained in the merkle_tree_circuit
directory.
You'll also need your Sindri API key to authenticate all requests to the API, so make sure you have this ready (see #api-authentication).
Modify the sample.env
file to include your API key and rename it to .env
.
Upload & Compileβ
In your main function, we have three methods:
- 'compile_circuit' to upload the circuit to Sindri and compile it.
- 'prove_circuit' which generate a proof for the circuit.
- 'verify_proof' to verify the proof.
Compileβ
Running compile will upload the merkle_tree_circuit files to Sindri.
If the compile checks pass, a json
file will be outputted to data/compile_out.json
.
{
"circuit_id": "bacaa9ca-f1c2-4c28-84c8-117f2548d60e",
"circuit_name": "merkle_tree_circuit",
"circuit_type": "plonky2",
"compute_time": "P0DT00H00M59.673416S",
"compute_time_sec": 59.673416,
"compute_times": {
"clean_up": 4.49532,
"compile": 22.33991,
"precompilation_checks": 37.0868,
"save_results": 0.24671,
"total": 64.16873
},
"date_created": "2024-08-13T19:00:04.989Z",
"error": null,
"file_size": 2992277,
"has_smart_contract_verifier": false,
"has_verification_key": false,
"meta": {},
"num_proofs": 0,
"plonky2_version": "0.2.2",
"project_name": "merkle_tree_circuit",
"proving_scheme": "plonky2",
"public": false,
"queue_time": "P0DT00H00M01.002722S",
"queue_time_sec": 1.002722,
"status": "Ready",
"struct_name": "merkle_tree::MerkleTreeCircuit",
"tags": ["latest"],
"team": "****",
"team_avatar_url": "****",
"team_slug": "****",
"uploaded_file_name": "filename.filetype",
"verification_key": null,
"warnings": null
}
Proveβ
The prove_circuit method requires a path to a valid input file.
The input_1024.json
file contains 1024 leaf values and the index of the desired leaf node for which to prove inclusion.
{"inputs":[17385365412,13508786373,...,14749629153,3531383147], "index": 0}
The prove_circuit method will pull the circuit ID from the compile_out.json
file.
The proof data is returned to the user in the data/proof_out.json
file.
The proofs
field of this JSON file contains everything that we need to verify the proof locally.
For Plonky2 proofs, we need three Rust structs to verify the proof:
- The
ProofWithPublicInputs
struct. - The
CommonCircuitData
struct. - The
VerifierOnlyCircuitData
struct.
These three structs are encoded as base64 strings.
The base64 strings are quite large for Plonky2 circuits, so they've been replaced with "****" in the sample proof_out.json
fiel shown below.
"circuit_id": "66d36825-3e72-4444-84d4-430ff402ffc1",
"circuit_name": "merkle_tree_circuit",
"circuit_team": "roy-sindri",
"circuit_team_avatar_url": "https://gravatar.com/avatar/758874998f5bd0c393da094e1967a72b?s=400&d=identicon&r=x",
"circuit_team_slug": "roy-sindri",
"circuit_type": "plonky2",
"compute_time": "P0DT00H00M01.203837S",
"compute_time_sec": 1.203837,
"compute_times": {
"clean_up": 0.18932,
"file_setup": 0.46645,
"prove": 0.04134,
"save_results": 0.50672,
"total": 1.20384
},
"date_created": "2024-08-30T08:50:40.932Z",
"error": null,
"file_size": 100671,
"has_smart_contract_calldata": false,
"has_verification_key": false,
"meta": {},
"perform_verify": false,
"project_name": "merkle_tree_circuit",
"proof": {
"common": "****"
"proof": "****"
"verifier_data": "****"
},
"proof_id": "8695040a-b1d8-4e74-95c1-1cb8441e8150",
"public": null,
"queue_time": "P0DT00H00M01.486846S",
"queue_time_sec": 1.486846,
"smart_contract_calldata": null,
"status": "Ready",
"team": "roy-sindri",
"team_avatar_url": "https://gravatar.com/avatar/758874998f5bd0c393da094e1967a72b?s=400&d=identicon&r=x",
"team_slug": "roy-sindri",
"verification_key": null,
"warnings": null
}
Verifyβ
The verify_proof method comprises several steps:
- Extracts the
proof
field from theproof_out.json
file. - Deserializes the base64 strings to the serialized Plonky2 types byte arrays.
- Defines a Gate Serializer for the Plonky2 circuit used for generating the proof.
This is necessary to deserialize the
CommonCircuitData
struct. - Deserialzies the byte arrays to their respective Plonky2 types.
will take the proof data from the
proof_out.json
file, deserialize the base64 strings, to their Plonky2 typeserify the proof locally. - Verifies the proof using the
verify
method from the Plonky2 API.
Running the verify_proof
method will show that the proof was verified successfully.
In order to run the code for the Merkle tree circuit, run the following command from the root of the merkle_tree
directory:
cargo run --release