5/5
## Understanding Your Noir Circuit and Prover.toml for ECDSA Verification This lesson guides you through creating the `Prover.toml` input file essential for proving a Noir circuit designed to verify ECDSA signatures. We'll generate cryptographic components using the `cast` command-line tool and then use a helper shell script to format these components correctly for Noir. Our starting point is an existing Noir circuit, typically found in `src/main.nr`. This circuit's primary function is to verify an ECDSA signature against a public key and a hashed message. The `main` function within this circuit defines the inputs it expects: ```rust fn main( pub_key_x: [u8; 32], pub_key_y: [u8; 32], signature: [u8; 64], // r and s components concatenated hashed_message: [u8; 32], expected_address: Field // The Ethereum address derived from the public key ) { // The ecrecover function attempts to recover an Ethereum address // from the public key, signature, and hashed message. // The 'false' argument indicates an Ethereum non-prefixed hash. let address: Field = ecrecover::ecrecover(pub_key_x, pub_key_y, signature, hashed_message, false); // Assert that the recovered address matches the expected address. // If they don't match, the proof will fail. assert(address == expected_address, "Address does not match expected address"); } ``` Let's break down these inputs: * `pub_key_x`: An array of 32 bytes representing the X-coordinate of the ECDSA public key. * `pub_key_y`: An array of 32 bytes representing the Y-coordinate of the ECDSA public key. * `signature`: A 64-byte array, which is the concatenation of the `r` and `s` values of the ECDSA signature. * `hashed_message`: A 32-byte array representing the Keccak-256 hash of the message that was signed. * `expected_address`: A `Field` type representing the Ethereum address anticipated to be derived from the provided public key if the signature is valid. To execute or prove this Noir circuit, we must supply these inputs via a `Prover.toml` file. Running the `nargo check` command will generate a template `Prover.toml` file with placeholders for these inputs: ```bash nargo check ``` This results in a `Prover.toml` file similar to this: ```toml expected_address = "" hashed_message = ["", "", "", ..., ""] // 32 empty strings pub_key_x = ["", "", "", ..., ""] // 32 empty strings pub_key_y = ["", "", "", ..., ""] // 32 empty strings signature = ["", "", "", ..., ""] // 64 empty strings ``` The main challenge lies in correctly populating these placeholders. Specifically, Noir expects byte arrays (like `pub_key_x`, `pub_key_y`, `signature`, and `hashed_message`) to be formatted as arrays of decimal strings, where each string is the decimal representation (0-255) of a single byte. Raw hexadecimal strings from cryptographic tools need to be converted to this format. ## Automating Input Formatting with `generate_inputs.sh` To simplify the conversion of hexadecimal strings to Noir's required `u8` array format, we'll use a helper shell script named `generate_inputs.sh`. **Purpose:** This script automates the process of reading raw hexadecimal input values and transforming them into the correctly formatted `Prover.toml` file. **Functionality:** 1. The script reads key-value pairs from a temporary file (which we'll call `inputs.txt`). In this file, values are stored as raw hexadecimal strings. 2. For inputs that correspond to byte arrays in Noir (`hashed_message`, `pub_key_x`, `pub_key_y`, `signature`), the script iterates through the hex string, processing two characters (one byte) at a time. 3. Each two-character hex representation of a byte is converted into its decimal equivalent. 4. These decimal values are then assembled into a TOML array of strings (e.g., `["123", "45", "201", ...]`) and written to the `Prover.toml` file. 5. The `expected_address`, being a `Field` type in Noir, is treated as a direct string value and doesn't undergo byte-wise decimal conversion. **Source:** The `generate_inputs.sh` script is available at `github.com/cyfrin/zk-noir-simple-ecrecover/blob/main/circuits/generate_inputs.sh`. You'll need to copy the content of this script into a local file named `generate_inputs.sh` in your project's circuit directory. ## Staging Raw Hex Values in `inputs.txt` Before running the `generate_inputs.sh` script, we need to prepare a temporary file, `inputs.txt`, to hold the raw hexadecimal values generated by our cryptographic tools. This file acts as an intermediary, making it easier to manage the inputs before they are formatted for `Prover.toml`. Create a file named `inputs.txt` with the following structure. We will populate the `0x...` values in the next steps: ``` expected_address = "0x..." hashed_message = "0x..." pub_key_x = "0x..." pub_key_y = "0x..." signature = "0x..." ``` The `generate_inputs.sh` script is designed to read from this `inputs.txt` file and write its formatted output to `Prover.toml`. ## Generating Cryptographic Inputs using `cast` We will use `cast`, a versatile command-line tool from the Foundry suite, to generate the necessary cryptographic components: the hashed message, the signer's Ethereum address, the public key, and the signature. *(Optional Tip: If you encounter issues with `cast`, especially if using a nightly build of Foundry, consider switching to a stable release by running `foundryup @ stable`.)* **a. Generating the Hashed Message** First, we need to hash the message that will be signed. Let's use the simple message "hello". 1. Open your terminal and run the following `cast` command to Keccak-256 hash the string "hello": ```bash cast keccak "hello" ``` 2. This command will output a 32-byte hexadecimal hash. For example: `0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8` 3. Copy this output (including the `0x` prefix) and paste it as the value for `hashed_message` in your `inputs.txt` file. **b. Retrieving the Expected Ethereum Address** Next, we need the Ethereum address of the account that will sign the message. This address will be our `expected_address`. We'll use a Foundry account aliased as `updraft` for this example. If you have a different account setup, use your relevant account alias or private key. 1. Run the following command to get the address for the `updraft` account: ```bash cast wallet address --account updraft ``` 2. The command will output the Ethereum address. For example: `0x52d64Ed1FD087797e2030fc914259e052F2bD67` 3. Copy this address and paste it as the value for `expected_address` in `inputs.txt`. **c. Obtaining Public Key Coordinates (X and Y)** The Noir circuit requires the X and Y coordinates of the public key separately. `cast` can provide the full uncompressed public key. 1. Run this command to get the public key for the `updraft` account: ```bash cast wallet public-key --account updraft ``` 2. The output will be a 64-byte hexadecimal string, which is the concatenation of the X and Y coordinates (each 32 bytes), without the `0x04` uncompressed prefix. For example: `0x533367042c3e9456fec155940165c28c01fe6e28601337df50562ddc4f36bfb97dcafe27cadbe10861bb67e0599382f79dc29c1d39d93b10f716c8ceff1743ed` 3. We need to split this into two 32-byte components. The first 32 bytes (64 hex characters) form the X coordinate, and the last 32 bytes form the Y coordinate. You can do this manually or use a utility. For instance, you could ask an AI assistant like ChatGPT: "can you split this public key into an x and y component: `<full_public_key_hex>`" Using the example above: * X component: `0x533367042c3e9456fec155940165c28c01fe6e28601337df50562ddc4f36bfb9` * Y component: `0x7dcafe27cadbe10861bb67e0599382f79dc29c1d39d93b10f716c8ceff1743ed` 4. Paste these X and Y components (including their `0x` prefixes) into the `pub_key_x` and `pub_key_y` fields in your `inputs.txt` file, respectively. **d. Generating the Signature** Finally, we sign the *hashed message* (obtained in step 4a) using the `updraft` account. 1. Take the `hashed_message` value (e.g., `0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8`) from your `inputs.txt`. 2. Run the `cast wallet sign` command. Critically, we must use the `--no-hash` flag because our input is *already* a hash. If we omit this flag, `cast` will hash the input again, leading to an incorrect signature. ```bash cast wallet sign --no-hash <hashed_message_hex_from_step_4a> --account updraft ``` Replace `<hashed_message_hex_from_step_4a>` with your actual hashed message. 3. The command will output the signature. This is typically a 65-byte hexadecimal string: the 32-byte `r` value, the 32-byte `s` value, and a 1-byte recovery ID `v` at the end. For example: `0xdd935f778351217ec02e6f857e47f0cea837380f30eccf63742b9a97cf1872b4316700b028504591f6b31203b86ccb3363353eedd8f9cf4f344a769c689525b31b` Our Noir circuit's `signature` input expects a 64-byte value (r and s concatenated). The `generate_inputs.sh` script is designed to handle this by taking only the first 64 bytes (128 hex characters) of the provided signature, effectively using just the `r` and `s` components. The `ecrecover` function within Noir, when used with the `false` flag for Ethereum-style recovery, can often derive the necessary information without an explicit `v` value passed in this manner. 4. Copy the full 65-byte signature output and paste it as the value for `signature` in `inputs.txt`. After completing these steps, your `inputs.txt` file should be fully populated with the hexadecimal values generated by `cast`. ## Populating Prover.toml using the Helper Script With `inputs.txt` prepared and the `generate_inputs.sh` script in place, we can now generate the `Prover.toml` file. 1. **Make the script executable:** Open your terminal in the directory containing `generate_inputs.sh` and run: ```bash chmod +x generate_inputs.sh ``` 2. **Ensure `Prover.toml` is not locked:** If you have `Prover.toml` open in a text editor, it's a good idea to close it, as the script will overwrite this file. 3. **Execute the script:** Run the script from your terminal: ```bash ./generate_inputs.sh ``` Upon successful execution, the script will output: `Wrote Prover.toml` 4. **Verify `Prover.toml`:** Open the `Prover.toml` file. It should now be populated with the correctly formatted inputs. For example, using the illustrative values from previous steps, it would look something like this: ```toml expected_address = "0x52d64Ed1FD087797e2030fc914259e052F2bD67" hashed_message = ["28", "139", "255", "149", "6", "133", "194", "237", "75", "195", "23", "79", "52", "114", "40", "123", "86", "217", "81", "123", "156", "148", "129", "39", "49", "154", "9", "167", "163", "109", "234", "200"] pub_key_x = ["83", "51", "103", "4", "44", "62", "148", "86", "254", "193", "133", "148", "1", "101", "194", "140", "1", "254", "110", "40", "96", "19", "55", "223", "80", "86", "45", "220", "79", "54", "191", "185"] pub_key_y = ["125", "202", "254", "39", "202", "219", "225", "8", "97", "187", "103", "224", "89", "147", "130", "247", "157", "194", "156", "29", "57", "217", "147", "16", "247", "22", "200", "206", "255", "23", "67", "237"] signature = ["221", "147", "95", "119", "131", "81", "33", "126", "192", "46", "111", "133", "126", "71", "240", "206", "168", "55", "56", "15", "48", "236", "207", "99", "116", "43", "154", "151", "207", "24", "114", "180", "49", "103", "0", "176", "40", "80", "69", "145", "246", "179", "18", "3", "184", "108", "203", "51", "99", "53", "238", "221", "143", "156", "244", "243", "52", "74", "118", "156", "104", "149", "37", "179"] ``` Note how the byte arrays are now arrays of decimal strings, and `expected_address` remains a single hexadecimal string. ## Next Steps: Proving Your Circuit You have now successfully prepared the `Prover.toml` file with all the necessary inputs for your Noir ECDSA verification circuit. This setup allows you to proceed with compiling and proving your circuit using `nargo prove`. **Key Takeaways and Best Practices:** * **Noir Byte Array Formatting:** Remember that Noir's `Prover.toml` expects `u8` arrays as arrays of decimal strings (e.g., `["10", "255", "0"]`). * **`cast --no-hash`:** This flag is crucial when using `cast wallet sign` with a message that has already been hashed. * **Utility Tools:** Tools like `cast` are invaluable for generating cryptographic data. For simple data manipulations like splitting hex strings, you can use scripting or even an AI assistant, but always verify the results. * **Executable Scripts:** Ensure your shell scripts (like `generate_inputs.sh`) are made executable using `chmod +x <script_name>`. * **Input Management Workflow:** The `inputs.txt` (raw hex) -> helper script -> `Prover.toml` (formatted) workflow is a practical pattern for managing complex inputs for Noir circuits, especially when not yet integrating with JavaScript SDKs. * **Ethereum `ecrecover` Specifics:** The `false` argument in the Noir `ecrecover(..., ..., ..., ..., false)` call signifies that the `hashed_message` is an Ethereum-style hash (i.e., not prefixed with "\x19Ethereum Signed Message:\n32"). While this manual, script-assisted method is excellent for understanding the process, testing, and demonstration, real-world decentralized applications often generate these inputs and manage proofs programmatically. This is typically done using JavaScript libraries like `noir_js` or within frameworks like Aztec.nr's `aztec.js`, which will be a topic for more advanced lessons. For now, your circuit is ready for proving!
A practical breakdown of Understanding Your Noir Circuit and Prover.toml for ECDSA Verification - Uncover the process of creating the `Prover.toml` file, essential for proving ECDSA signature verifications in Noir. You'll learn to generate crypto inputs using `cast` and automate their formatting into the required decimal string arrays with a helper script.
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