5/5
## Generating and Verifying ECDSA Proofs with Nargo and Barretenberg This lesson details the process of generating cryptographic proofs for an Elliptic Curve Digital Signature Algorithm (ECDSA) circuit using Nargo and Barretenberg (`bb`). We'll cover creating a witness, generating a proof, performing off-chain verification, and finally, generating a Solidity smart contract for on-chain verification. ## Generating the Witness: The Prover's First Step The journey to a verifiable proof begins with the prover. The prover executes the circuit with a set of known inputs to produce a "witness." This witness is a complete execution trace, capturing all intermediate values within the circuit for those specific inputs. To generate the witness, use the Nargo command-line interface: ```bash nargo execute ``` This command interacts with your Noir circuit, typically defined in a `src/main.nr` file. For our ECDSA example, the circuit's main function (`main`) accepts public key components (`pub_key_x`, `pub_key_y`), a `signature`, a `hashed_message`, and an `expected_address`. Internally, it uses an `ecrecover` function to derive an Ethereum address from the signature, message, and public key. It then asserts that this derived address matches the `expected_address` public input. A simplified representation of the `main.nr` circuit is: ```noir // src/main.nr fn main( pub_key_x: [u8; 32], pub_key_y: [u8; 32], // Corrected from 'вертикала' in source signature: [u8; 64], hashed_message: [u8; 32], expected_address: Field ) { let address: Field = ecrecover::ecrecover(pub_key_x, pub_key_y, signature, hashed_message); assert(address == expected_address, "Address does not match expected address"); } ``` Executing `nargo execute` performs several actions: 1. It compiles the Noir circuit (`main.nr`) into bytecode. 2. It runs this bytecode with the inputs provided (usually found in a `Prover.toml` file in the project's root). 3. It generates a witness file if the execution is successful. The terminal output will indicate success: `[zk_ecdsa] Circuit witness successfully solved` `[zk_ecdsa] Witness saved to /Users/ciaranightingale/code/Updraft/recording/zk_ecdsa/target/zk_ecdsa.gz` This process creates two important files in the `target` directory: * `zk_ecdsa.json`: The compiled bytecode of your circuit. * `zk_ecdsa.gz`: The compressed witness file, containing the detailed execution trace. ## Generating a Cryptographic Proof With the witness generated, the prover can now use the Barretenberg (`bb`) command-line tool to create a cryptographic proof. This proof attests to the correct execution of the circuit for the inputs that produced the witness, without revealing those inputs. The command to generate the proof is: ```bash bb prove -b ./target/zk_ecdsa.json -w ./target/zk_ecdsa.gz -o ./target ``` Let's break down the flags: * `-b ./target/zk_ecdsa.json`: Specifies the path to the circuit's bytecode. * `-w ./target/zk_ecdsa.gz`: Specifies the path to the generated witness. * `-o ./target`: Specifies the output directory where the proof will be saved. Upon successful execution, Barretenberg will create a file named `proof` within the `./target/` directory. The terminal will confirm: `Proof saved to "./target/proof"` ## Off-Chain Proof Verification Before attempting on-chain verification, it's prudent to verify the proof off-chain. This step requires a "verification key" (VK), which is derived from the circuit's structure. ### A. Generating the Verification Key (VK) The verification key is essential for anyone wanting to verify proofs for this specific circuit. It can be generated by the verifier or provided publicly by the circuit developer. To generate the VK using Barretenberg: ```bash bb write_vk -b ./target/zk_ecdsa.json -o ./target ``` * `-b ./target/zk_ecdsa.json`: Points to the circuit's bytecode. * `-o ./target`: Designates the output directory for the VK. This command creates a file named `vk` in the `./target/` directory. The terminal output will be: `VK saved to "./target/vk"` ### B. Verifying the Proof Off-Chain Now, the verifier, equipped with the `vk` and the `proof`, can perform the verification. The command for off-chain verification is: ```bash bb verify -k ./target/vk -p ./target/proof ``` * `-k ./target/vk`: Specifies the path to the verification key. * `-p ./target/proof`: Specifies the path to the proof generated by the prover. Successful verification will result in the terminal displaying: `Proof verified successfully` **Crucial Concept:** This off-chain verification confirms that the prover possesses a set of inputs (public key, signature, hashed message) that satisfy the logic of the ECDSA circuit (i.e., the signature is valid for the given message and public key, and the recovery results in the `expected_address`). Importantly, the verifier achieves this confirmation *without needing access to the prover's private inputs*. The only public input directly used by the verifier during this specific check (if they were to inspect the circuit definition alongside the proof) is the `expected_address`. ## Understanding Input Privacy and the `Prover.toml` File The private inputs used by the prover to generate the witness (and subsequently the proof) are typically stored in a `Prover.toml` file. For our ECDSA circuit, this file would contain values for `pub_key_x`, `pub_key_y`, `signature`, `hashed_message`, and the public input `expected_address`. An example `Prover.toml` might look like this: ```toml // Prover.toml (example content) expected_address = "0x52d64dEaD1FD0877797e2030fC914259e952Fb067" hashed_message = ["28", "138", /* ... more byte values ... */ "75", "195"] pub_key_x = ["83", "51", /* ... more byte values ... */ "85", "146"] pub_key_y = ["125", "202", /* ... more byte values ... */ "97", "187"] signature = ["221", "147", /* ... more byte values ... */ "192", "46", "111"] ``` **Critical Note:** The `Prover.toml` file is strictly local to the prover. It contains sensitive information and **must not** be shared with the verifier or committed to public repositories. The entire purpose of ZK-proofs is to verify computations without revealing such private data. In real-world applications, these inputs would often be managed by a frontend application or JavaScript environment that interacts with the proving tools. ## Generating a Solidity Verifier for On-Chain Verification To enable verification of these ECDSA proofs on a blockchain (like Ethereum), a Solidity smart contract verifier is required. ### A. Compile the Circuit (Verifier's Perspective) From the perspective of someone developing or deploying the verifier smart contract, they only need the circuit's bytecode (`.json` file). They do not need the prover's specific inputs, witness, or `Prover.toml` file. To simulate this, one might clear the `target` directory and any `Prover.toml` file, then recompile the circuit: ```bash nargo compile ``` This command regenerates the `target` directory containing the `zk_ecdsa.json` bytecode file, which is the sole artifact needed from the circuit's definition for the next steps. ### B. Generating a Gas-Optimized Verification Key for On-Chain Use When generating a verification key for an on-chain Solidity verifier, it's often more gas-efficient to configure it to use Keccak hashes. Ethereum natively supports Keccak, making proofs structured this way cheaper to verify on-chain compared to ZK-specific hash functions like Poseidon if not handled carefully. To generate a VK optimized for Keccak-based on-chain verifiers: ```bash bb write_vk --oracle_hash keccak -b ./target/zk_ecdsa.json -o ./target ``` * `--oracle_hash keccak`: This crucial flag instructs Barretenberg to create a VK compatible with a proof system using Keccak for certain internal hashing operations relevant to the verifier contract. * `-b ./target/zk_ecdsa.json`: Path to the circuit bytecode. * `-o ./target`: Output directory for the new VK. This will overwrite or create a `vk` file in `./target/`, now tailored for a Keccak-based Solidity verifier. ### C. Creating the Solidity Verifier Contract Barretenberg can automatically generate a Solidity verifier contract directly from the (Keccak-optimized) verification key. Use the following command: ```bash bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol ``` * `-k ./target/vk`: Specifies the path to the verification key (which should be the Keccak-optimized one). * `-o ./target/Verifier.sol`: Defines the output path and filename for the generated Solidity contract. This command produces a `Verifier.sol` file in the `./target/` directory. The terminal will confirm: `Solidity verifier saved to "./target/Verifier.sol"` The generated `Verifier.sol` contract is intricate, containing the necessary cryptographic logic and the embedded verification key data. A key component is typically a `verify` function. While the exact structure can vary based on the underlying proof system (e.g., Honk, Plonk), it will generally conform to an interface like: ```solidity // Example structure often found in or inherited by the generated contract interface IVerifier { function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); } // The generated contract, e.g., HonkVerifier, will implement this or a similar function contract HonkVerifier is /* BaseVerifier, potentially IVerifier */ { // ... considerable generated code including VK data and cryptographic helpers ... function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { // ... logic to load the embedded VK data ... } function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) public view returns (bool) { // ... complex cryptographic logic to verify the _proof against the _publicInputs using the embedded VK ... // Returns true if the proof is valid, false otherwise. } } ``` The `verify` function takes two primary arguments: 1. `_proof`: The cryptographic proof bytes, generated by the prover off-chain. 2. `_publicInputs`: An array of `bytes32` values representing the public inputs to the circuit. For our ECDSA example, this would be the `expected_address`. ## Utilizing the Solidity Verifier Smart Contract Once generated, the `Verifier.sol` smart contract can be compiled and deployed to an EVM-compatible blockchain. A prover, having generated a proof for their specific private inputs off-chain (as described in earlier steps), can then interact with this deployed smart contract. They would call its `verify` function, submitting their `_proof` bytes and the corresponding `_publicInputs` (e.g., the `expected_address` they are claiming their signature resolves to). The smart contract's `verify` function will execute the verification logic on-chain. If the proof is valid for the given public inputs, the function will return `true`. This provides on-chain, trustless confirmation that the prover possesses private inputs satisfying the circuit's conditions, all without those private inputs ever being revealed on the blockchain. ## Key Concepts Recap * **Prover:** The entity holding private inputs, aiming to prove knowledge or correct computation without revealing these inputs. Generates a witness and then a proof. * **Verifier:** The entity that checks the prover's claim using the proof and a verification key. * **Nargo:** The toolkit for writing, compiling, and executing Noir (ZK-DSL) circuits. * **Barretenberg (`bb`):** The C++ library and command-line interface serving as the cryptographic backend for generating proofs, verification keys, and verifier contracts. * **Bytecode (`.json` file):** The compiled, machine-readable representation of the Noir circuit. * **Witness (`.gz` file):** A complete execution trace of the circuit for a specific set of inputs, including all private and public values. * **Proof:** A compact cryptographic string that attests to the correct execution of the circuit and knowledge of a valid witness, without revealing private inputs. * **Verification Key (VK):** Data derived from the circuit's structure that enables a verifier to check the validity of proofs for that circuit. * **Keccak Hash:** A cryptographic hash function widely used in Ethereum. Using a VK generated with `--oracle_hash keccak` can lead to more gas-efficient on-chain verifiers. * **Solidity Verifier Contract:** A smart contract designed to verify ZK-proofs on a blockchain. ## Important Considerations and Best Practices * **Privacy of `Prover.toml`:** Crucially, the `Prover.toml` file, containing private inputs, must **never** be shared or committed to public version control systems. * **On-Chain VK Optimization:** When targeting on-chain verification, always use the `--oracle_hash keccak` flag during `bb write_vk` to generate a VK suitable for more gas-efficient Solidity verifiers. * **Deployment of `Verifier.sol`:** The generated `Verifier.sol` is generally ready for deployment. However, depending on your target blockchain and deployment tools, you might need to adjust Solidity compiler versions or optimizer settings. * **Further Steps:** Deploying the `Verifier.sol` contract and interacting with it from a client application or another smart contract involves standard blockchain development practices. This lesson has walked through the end-to-end flow: from defining an ECDSA validation circuit in Noir, generating a witness and proof using Nargo and Barretenberg, performing off-chain verification, and finally, generating a Solidity smart contract to enable secure and private on-chain verification of these proofs.
A hands-on walkthrough of Generating and Verifying ECDSA Proofs with Nargo and Barretenberg - Master the end-to-end process of creating cryptographic proofs for ECDSA circuits using Nargo and Barretenberg, from witness generation to off-chain validation. This lesson guides you through generating Solidity smart contracts for secure and efficient on-chain proof verification.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025
Duration: 6min
Duration: 1h 11min
Duration: 2h 12min
Duration: 3h 19min
Course Overview
About the course
Noir syntax
Create a witness, a proof, and Solidity verifier contracts
Use the Poseidon commitment scheme
Create ZK circuits and build a full ZK protocol
ZK Merkle trees and hashing in Noir
Verify signatures without revealing the signer
Build the backend for a full-stack ZK application with noir.js and bb.js
How to create proofs and verify them in a front-end
Last updated on June 12, 2025