5/5
## Generating Zero-Knowledge Proofs Off-Chain: The `_getProof` Solidity Helper This lesson details the creation of a crucial Solidity helper function, `_getProof`. Its primary role is to interface with an external JavaScript/TypeScript script using Foundry's Foreign Function Interface (FFI). This approach allows us to delegate the computationally intensive task of Zero-Knowledge (ZK) proof generation to an off-chain environment, keeping our smart contracts efficient and cost-effective. ### 1. Preparing `leaves` for the Merkle Tree The `_getProof` function, which we will build, requires an array of `leaves` as one of its inputs. In the context of a ZK application, these `leaves` typically represent commitments (e.g., hashed deposit information) that have already been recorded and added to a Merkle tree. In a production application, you would retrieve these leaves by querying past "deposit" events emitted by your smart contract. These events would contain the individual commitments, which you would then collect and order (for instance, by their timestamp or block number) to reconstruct the state of the Merkle tree. For this tutorial and for streamlined testing, we'll employ a simplified, or "hacky," method to populate the `leaves` array directly within our test function, `testMakeWithdrawal`. This involves first making a deposit, which generates a unique `_commitment`. Then, we initialize the `leaves` array to contain only this single commitment. Consider the following snippet from the `testMakeWithdrawal` function, executed after a deposit has been made: ```solidity // (Inside testMakeWithdrawal function, after a deposit) // create a proof // get the leaves of the tree (hacky way for testing) bytes32[] memory leaves = new bytes32[](1); leaves[0] = _commitment; // _commitment is generated from the deposit bytes memory _proof = _getProof(_nullifier, _secret, recipient, leaves); ``` Here, `_nullifier`, `_secret`, and `recipient` are variables defined earlier in the `testMakeWithdrawal` function. They pertain to the specific deposit that is intended to be withdrawn, for which we are generating this proof. ### 2. Defining the `_getProof` Helper Function The `_getProof` function orchestrates the call to our external proof-generation script. Let's define its signature. It accepts the `_nullifier` and `_secret` (private inputs known to the user proving withdrawal), the `_recipient` (the address where funds will be withdrawn), and the `_leaves` array (containing all existing commitments in the Merkle tree) as inputs. It will return the generated ZK `proof` as `bytes memory`. ```solidity function _getProof( bytes32 _nullifier, bytes32 _secret, address _recipient, bytes32[] memory _leaves ) internal returns (bytes memory proof) { // Function body to be detailed next } ``` ### 3. Preparing Inputs for the External Script (`inputs` array) To invoke an external script via Foundry's FFI, we must prepare an array of strings, `string[] memory inputs`. This array will contain the command to execute the script, followed by all necessary arguments. **Size of the `inputs` array:** The total size of the `inputs` array is determined by the command itself and the arguments required by the script: * The first three elements are for the script execution command (e.g., `npx`, `tsx`, and the script path). * The next three elements are the core inputs for our proof generation logic: `_nullifier`, `_secret`, and `_recipient`. * The remaining elements will be all the `_leaves` from the Merkle tree. Therefore, the total size of the array will be `6 + _leaves.length`. ```solidity string[] memory inputs = new string[](6 + _leaves.length); ``` **Populating the `inputs` array:** 1. **Script Execution Command:** We specify the command to run our TypeScript script. `npx` is a tool to execute Node.js package binaries, and `tsx` is a TypeScript executor. ```solidity inputs[0] = "npx"; inputs[1] = "tsx"; inputs[2] = "js-scripts/generateProof.ts"; // Path to our TypeScript script ``` 2. **Core Proof Inputs (converted to strings):** The external script (`generateProof.ts`) will use these inputs to compute the necessary components for the ZK proof, such as the Merkle `root`, `nullifier_hash`, and `merkle_proof`, as defined by our ZK circuit (e.g., in a `main.nr` Nargo file). These inputs must be converted to strings. ```solidity inputs[3] = vm.toString(_nullifier); inputs[4] = vm.toString(_secret); ``` The `_recipient` address requires careful conversion. ZK circuits often expect inputs as field elements or fixed-size byte arrays. An `address` (20 bytes) is cast to `uint160`, then to `uint256` (which pads it to 32 bytes), then to `bytes32`, and finally converted to a string using `vm.toString`. ```solidity // Convert address to uint160, then uint256 (padding), then bytes32, then string inputs[5] = vm.toString(bytes32(uint256(uint160(_recipient)))); ``` 3. **Merkle Tree Leaves (converted to strings):** All existing `_leaves` of the Merkle tree are passed to the script. We iterate through the `_leaves` array, convert each `bytes32` leaf to a string, and add it to our `inputs` array. ```solidity for (uint256 i = 0; i < _leaves.length; i++) { inputs[6 + i] = vm.toString(_leaves[i]); } ``` ### 4. Calling the External Script (FFI) With the `inputs` array fully prepared, we use Foundry's `vm.ffi()` cheatcode to execute the specified command. This powerful cheatcode runs the `js-scripts/generateProof.ts` script in a separate shell process, passing all the prepared strings as command-line arguments. The external script is designed to perform the heavy computation (Merkle tree construction, witness generation, proof generation) and then print the resulting ZK proof to its standard output. The `vm.ffi()` function captures this standard output as `bytes memory`. ```solidity // use ffi to run scripts in the CLI to create the proof bytes memory result = vm.ffi(inputs); ``` ### 5. Decoding the Result The `result` obtained from `vm.ffi()` is the raw byte output from the script. This output needs to be decoded into the data structure expected by our Solidity contract, which in this case is the ZK proof itself, represented as `bytes`. We use `abi.decode()` for this purpose. ```solidity // decode the result to get the proof proof = abi.decode(result, (bytes)); // The function implicitly returns this 'proof' due to the named return variable ``` This completes the `_getProof` function. ### 6. Creating the External Script File As a final preparatory step, we need to create the actual TypeScript file that will be invoked. This file, `generateProof.ts`, will reside in a `js-scripts` directory alongside any other helper scripts. ``` js-scripts/ ├── generateCommitment.ts └── generateProof.ts // New file created ``` The detailed implementation of `generateProof.ts` is beyond the scope of this lesson and will be covered subsequently. Its responsibilities will include: * Parsing the command-line arguments (passed via the `inputs` array). * Reconstructing the Merkle tree from the provided `leaves`. * Generating a Merkle proof for the specific commitment being withdrawn (identified by `_secret` and `_nullifier`). * Interacting with a ZK proving system (like Aztec's Nargo/Noir or Circom/SnarkJS) to compile the circuit, generate a witness, and create the final ZK proof. ### Key Concepts * **Zero-Knowledge Proofs (ZKPs):** Cryptographic methods that allow one party (the prover) to prove to another party (the verifier) that a statement is true, without revealing any information beyond the validity of the statement itself. Essential for privacy-preserving applications. * **Merkle Tree:** A hash-based data structure used to efficiently verify the integrity and membership of large sets of data. In this context, it stores commitments, and a Merkle proof demonstrates that a specific commitment is part of the tree. * **FFI (Foreign Function Interface):** A mechanism enabling a program written in one language to call functions or use services written in another. Foundry's `vm.ffi()` allows Solidity tests and scripts to execute arbitrary shell commands, bridging the gap to off-chain tools. * **Off-Chain Computation:** The practice of performing computationally expensive, complex, or data-intensive tasks outside the blockchain environment. This saves on-chain gas costs, reduces block congestion, and allows for more complex operations than feasible on-chain. ZK proof generation is a prime candidate. * **Solidity `vm.toString()` and `abi.decode()`:** Foundry cheatcodes and Solidity built-in functions, respectively. `vm.toString()` converts various Solidity types to their string representations, necessary for passing data to external scripts via FFI. `abi.decode()` parses ABI-encoded byte arrays back into Solidity types. ### Important Considerations * **Simplified `leaves` Creation:** Remember that the method shown for creating the `leaves` array in the test function is a simplification. Real-world applications require robust event sourcing mechanisms to accurately reconstruct the Merkle tree's state. * **Input Array Integrity:** The construction of the `inputs` array is critical. The order, data types, and formatting of arguments must precisely match what the external script (`generateProof.ts`) expects to parse. * **Error Handling:** This lesson does not cover error handling for FFI calls (e.g., what happens if the external script fails or returns an error). In production code, you would need to implement checks and appropriate error management. * **Data Type Compatibility:** The casting of the `_recipient` address (from `address` to `bytes32`) illustrates a common challenge: ensuring data types are compatible between the Solidity environment and the off-chain environment, including the specific requirements of the ZK circuit. This `_getProof` function acts as a vital bridge, enabling Solidity smart contracts (especially during testing, or potentially through an off-chain relayer in a live system) to harness the power of sophisticated off-chain tooling for generating Zero-Knowledge proofs.
A detailed introduction to Generating Zero-Knowledge Proofs Off-Chain: The `_getProof` Solidity Helper - Discover how to construct the `_getProof` Solidity function, leveraging Foundry's FFI to call external scripts for efficient off-chain ZK proof generation. This lesson explains the preparation of inputs like Merkle tree leaves and private user data, and the process of invoking the external 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