5/5
## Pre-computing Zero Subtree Hashes for Incremental Merkle Trees In the context of incremental Merkle trees, particularly when utilized within zk-SNARK applications employing Poseidon hashing, the concept of "zero subtrees" is fundamental. This lesson details the process of pre-computing and storing these zero subtree hashes, which are essential for initializing the tree and efficiently constructing Merkle proofs for empty elements. ## Understanding Zero Subtrees in Merkle Trees When an incremental Merkle tree is initialized or when parts of it remain unfilled, we use "zero values" to represent these empty slots. Instead of a literal `0`, a predetermined hash is used for an empty leaf. These hashes are recursively defined: * **`zeros[0]` (Level 0):** This is the hash of a specific, arbitrary value chosen to represent a single empty leaf node. * **`zeros[1]` (Level 1):** This is the hash of a pair of `zeros[0]` values: `PoseidonHash(zeros[0], zeros[0])`. This represents an empty subtree of height 1. * **`zeros[i]` (Level i):** Generally, this is the hash of two `zeros[i-1]` values: `PoseidonHash(zeros[i-1], zeros[i-1])`. This represents an empty subtree of height `i`. Pre-computing these "zero hashes" for each level of the tree allows for efficient construction and verification of Merkle proofs, especially when dealing with sparse trees. ## Smart Contract Setup for Merkle Tree Initialization The primary goal is to initialize the Merkle tree within a smart contract with a root that accurately represents a completely empty tree of a specified depth. 1. **Storing the Merkle Root:** A state variable is declared in the smart contract to store the current root of the incremental Merkle tree. ```solidity // In IncrementalMerkleTree.sol bytes32 public s_root; ``` 2. **Initializing the Root in the Constructor:** Upon deployment, the constructor sets the tree's depth (`i_depth`) and then initializes `s_root`. This initialization uses a `zeros` function, which provides the root hash of a completely zero-filled tree corresponding to the given depth. ```solidity // Inside the constructor of IncrementalMerkleTree.sol // ... i_depth = _depth; // ... // Initialize the tree with zeros (precompute all the zero subtrees) // store the initial root in storage s_root = zeros(_depth); // s_root now holds the root of an empty tree of 'i_depth' ``` ## The `zeros` Function: Retrieving Pre-computed Hashes The `zeros` function is designed to return the pre-computed hash of a zero-filled subtree for a given level `i`. * **Rationale for Pre-computation:** Calculating these hashes on-chain, especially with cryptographic functions like Poseidon, is computationally intensive and would lead to high gas costs. Therefore, these values are computed off-chain and then hardcoded into the smart contract. * **Function Signature:** ```solidity // In IncrementalMerkleTree.sol function zeros(uint32 i) public view returns (bytes32) { // Implementation with hardcoded values } ``` The parameter `i` denotes the level (or height) of the zero subtree whose root hash is being requested. The `public view` visibility allows external and internal read-only access without incurring gas fees. ## Calculating and Hardcoding `zeros[0]`: The Base Empty Leaf The `zeros[0]` value is the foundational hash representing a single empty leaf. 1. **Defining the Empty Leaf Representation:** An arbitrary string, "cyfrin", is chosen for this purpose. This string is first hashed using `keccak256`. Using Foundry's `chisel`, a Solidity REPL: ``` keccak256("cyfrin") // Output will be a bytes32 value ``` 2. **Adhering to Poseidon Field Constraints:** The Poseidon hash function, used for combining nodes in the Merkle tree, operates over a finite field. This means its inputs must be valid field elements, specifically, they must be less than the field's prime modulus. The output of `keccak256` might exceed this modulus. * The Poseidon implementation referenced (e.g., from `zemse/poseidon2-evm`) defines this prime modulus. For instance, in `Field.sol`: `uint256 constant PRIME = 21888242871839275222246405745277275088548364400416034343698204186575808495617;` * To ensure compatibility, the `keccak256` hash is taken modulo this prime. In `chisel`: ``` bytes32(uint256(keccak256("cyfrin")) % 21888242871839275222246405745277275088548364400416034343698204186575808495617) // Expected Output: 0x0d823319708ab99ec915efd4f7e03d11ca1790918e8f04cd14100aceca2aa9ff ``` This resulting `bytes32` value is our `zeros[0]`. 3. **Hardcoding `zeros[0]` into the Contract:** ```solidity // Inside the zeros function if (i == 0) return bytes32(0x0d823319708ab99ec915efd4f7e03d11ca1790918e8f04cd14100aceca2aa9ff); ``` ## Calculating and Hardcoding Subsequent `zeros[i]` Values For levels `i > 0`, each `zeros[i]` is derived by hashing the previous level's zero hash with itself: `zeros[i] = PoseidonHash(zeros[i-1], zeros[i-1])`. 1. **Off-Chain Poseidon Hashing:** These calculations are performed off-chain. For example, to calculate `zeros[1]`: * A utility contract implementing the Poseidon hash function (e.g., a simplified `Poseidon2.sol` based on `zemse/poseidon2-evm`) can be deployed to a local development environment like Remix IDE. This contract would expose a function like `hash(Field.Type x, Field.Type y)`. * The previously calculated `zeros[0]` value (`0x0d82...ff`) is supplied as both inputs `x` and `y` to this `hash` function. * The output of this operation is `zeros[1]`. For the given `zeros[0]`, `zeros[1]` is: `0x0a170a9598425eb05eb0dc06986c6afc717811e874326a79576c02d338bdf14f13` 2. **Hardcoding `zeros[1]`:** ```solidity // Inside the zeros function, continued else if (i == 1) return bytes32(0x0a170a9598425eb05eb0dc06986c6afc717811e874326a79576c02d338bdf14f13); ``` 3. **Iterative Calculation for All Levels:** This process is repeated. `zeros[2]` is calculated as `PoseidonHash(zeros[1], zeros[1])`, `zeros[3]` as `PoseidonHash(zeros[2], zeros[2])`, and so forth, up to the maximum depth supported by the `zeros` function. If the Merkle tree's maximum depth is less than 32 (e.g., `_depth` is checked in the constructor `if (_depth >= 32) revert;`), then `zeros` would need to support up to level 31. ## Complete `zeros` Function Implementation The `zeros` function evolves into a series of conditional statements, each returning a hardcoded `bytes32` hash for a specific level `i`. An error is typically included to handle requests for levels outside the pre-computed range. ```solidity // In IncrementalMerkleTree.sol // Custom error for invalid level requests error IncrementalMerkleTree_LevelOutOfBounds(uint256 level); contract IncrementalMerkleTree { uint32 public i_depth; bytes32 public s_root; // Constructor where i_depth and s_root are initialized constructor(uint32 _depth) { if (_depth == 0 || _depth >= 32) { // Max depth for this example setup revert("IncrementalMerkleTree: Depth must be between 1 and 31."); } i_depth = _depth; s_root = zeros(_depth); } function zeros(uint32 i) public pure returns (bytes32) { // Changed to pure as it doesn't read state if (i == 0) return bytes32(0x0d823319708ab99ec915efd4f7e03d11ca1790918e8f04cd14100aceca2aa9ff); else if (i == 1) return bytes32(0x0a170a9598425eb05eb0dc06986c6afc717811e874326a79576c02d338bdf14f13); else if (i == 2) return bytes32(0x1215385125455297316888396c043196900499731040918329722889890718215697709583); // Value derived from PoseidonHash(zeros[1], zeros[1]) // ... additional else if statements for i = 3 up to i = 31 ... // Example for level 31 (ensure this value is correctly calculated and corresponds to your max depth) else if (i == 31) return bytes32(0x0213fb841f90e06958cf4403477dbff7c5906249daabfee147f853db7c0801); // Example, actual value must be calculated else { revert IncrementalMerkleTree_LevelOutOfBounds(i); } } } ``` *Note: The `zeros` function is marked `pure` as it doesn't access contract storage and its output depends only on inputs. The list of hashes for levels 2 through 31 must be generated by repeatedly applying the Poseidon hash to the previous level's zero hash.* ## Conceptualizing Zero Subtree Levels Understanding the relationship between subtree height and the `zeros[i]` index is crucial: * A **subtree of height 0** corresponds to a single leaf node. If this leaf is considered "empty," its value is `zeros[0]`. * A **subtree of height 1** is a node directly above two leaf nodes. If both underlying leaves are empty (each represented by `zeros[0]`), this node's value is `PoseidonHash(zeros[0], zeros[0])`, which is precisely `zeros[1]`. * A **subtree of height 2** is a node whose children are two subtree roots of height 1. If all leaves in this entire subtree are empty, its value is `PoseidonHash(zeros[1], zeros[1])`, which is `zeros[2]`. This pattern continues up the tree. The `zeros(i)` function effectively returns the root hash of a completely empty Merkle subtree of height `i`. When initializing the main tree of depth `D`, `s_root` is set to `zeros(D)`. ## Key Technical Considerations * **Off-Chain Computation is Paramount:** The pre-computation of zero subtree hashes is vital for maintaining gas efficiency in smart contracts. On-chain cryptographic hashing for numerous elements is prohibitively expensive. * **Hashing Algorithm Consistency:** Maintain strict consistency in the choice and application of hashing algorithms. If `keccak256` is used for an initial transformation of the "empty leaf" representation, and Poseidon for internal Merkle tree nodes, this scheme must be universally applied. * **Finite Field Modulus Compliance:** When outputs from one hash function (e.g., `keccak256`) serve as inputs to another that operates over a finite field (e.g., Poseidon), ensure inputs are valid field elements. This often requires a modulo operation with the field's prime. * **Utilize Off-Chain Tooling:** * Libraries like `zemse/poseidon2-evm` (GitHub) provide reference implementations for Poseidon hashing. * Tools like Foundry's `chisel` are invaluable for quick off-chain cryptographic calculations and Solidity interactions. * Development environments like Remix IDE facilitate deploying and interacting with utility contracts (e.g., for Poseidon hashing) off-chain to generate the required hash values. By adhering to these principles, developers can correctly and efficiently implement the `zeros` array, a cornerstone for building robust and gas-conscious incremental Merkle trees in Solidity.
An essential guide to Pre-computing Zero Subtree Hashes for Incremental Merkle Trees - Master the off-chain calculation of zero subtree hashes (`zeros[i]`) using Poseidon, crucial for initializing gas-efficient Incremental Merkle Trees. Implement these pre-computed values directly into your Solidity `zeros` function to represent empty tree states.
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