5/5
## Finalizing the Merkle Tree Insertion Logic To complete the functionality of our ZK-mixer, we first need to finalize the `_insert` function within the `IncrementalMerkleTree.sol` contract. This function is responsible for adding a new leaf (commitment) to our Merkle tree and updating its structure. After the core loop that calculates hashes up the tree levels, two crucial steps remain: storing the new Merkle root and managing the leaf index. ### Storing the New Merkle Root Once a new leaf is inserted and its impact has been propagated up the tree through iterative hashing, the `currentHash` variable holds the newly computed root of the entire Merkle tree. This new root must be persisted in the contract's state. We achieve this by assigning `currentHash` to the state variable `s_root`. This `s_root` represents the most up-to-date state of our Merkle tree, incorporating all previously deposited commitments. ```solidity // src/IncrementalMerkleTree.sol (within _insert function, after the for loop) // ... // store the root in storage s_root = currentHash; ``` ### Managing and Returning the Leaf Index For each new leaf, we need to track its position in the tree. The `s_nextLeafIndex` state variable stores the index where the *next* leaf will be inserted. Initially, within the `_insert` function, we capture the current value of `s_nextLeafIndex` into a local variable, let's call it `_insertedLeafIndex`. This `_insertedLeafIndex` represents the index at which the current leaf is being inserted. ```solidity // src/IncrementalMerkleTree.sol (within _insert function) uint32 _insertedLeafIndex = s_nextLeafIndex; // Index for the current insertion // ... (rest of the insertion logic, including s_root = currentHash;) ``` After successfully inserting the leaf and updating the root, we must increment `s_nextLeafIndex` so it's ready for the subsequent insertion: ```solidity // src/IncrementalMerkleTree.sol (within _insert function, after storing the root) // increment the next leaf index s_nextLeafIndex = _insertedLeafIndex + 1; ``` Crucially, the `Mixer` contract (which will call this `_insert` function) needs to know the index at which the current leaf was actually inserted. This information is vital for the `Deposit` event, which helps off-chain services reconstruct the tree. Therefore, the `_insert` function is modified to return the `_insertedLeafIndex` *before* it was incremented to become `s_nextLeafIndex`. The updated function signature and return statement look like this: ```solidity // src/IncrementalMerkleTree.sol function _insert(bytes32 _leaf) internal returns (uint32) { uint32 _insertedLeafIndex = s_nextLeafIndex; // Capture index for this insertion // ... (existing leaf insertion and Merkle root calculation logic) ... // store the root in storage s_root = currentHash; // increment the next leaf index for future insertions s_nextLeafIndex = _insertedLeafIndex + 1; // Return the index of the leaf that was just inserted return _insertedLeafIndex; } ``` This ensures that the caller receives the exact index of the leaf that was processed in that particular call to `_insert`. ## Integrating the Merkle Tree into the Mixer Contract With the `IncrementalMerkleTree.sol` contract's `_insert` function finalized, we can now integrate this Merkle tree logic into our main `Mixer.sol` contract. This involves importing necessary contracts, setting up inheritance, and modifying the constructor. ### Importing Dependencies The `Mixer.sol` contract needs to be aware of `IncrementalMerkleTree` and the hashing library it uses (e.g., `Poseidon2`). We also include `IVerifier` for context, as it's typically part of a ZK-mixer system. ```solidity // src/Mixer.sol pragma solidity ^0.8.0; import {IncrementalMerkleTree} from "./IncrementalMerkleTree.sol"; import {Poseidon2} from "./poseidon2-evm/Poseidon2.sol"; // Assuming Poseidon2 is the hasher import {IVerifier} from "./Verifier.sol"; ``` ### Establishing Inheritance To directly use the Merkle tree functionalities, the `Mixer` contract will inherit from `IncrementalMerkleTree`. This makes all `public` and `internal` functions and state variables of `IncrementalMerkleTree` available within `Mixer`. ```solidity // src/Mixer.sol contract Mixer is IncrementalMerkleTree { // ... Mixer contract state variables and functions will follow } ``` ### Modifying the Constructor The `IncrementalMerkleTree` contract has its own constructor that requires parameters like the tree depth and an instance of the hasher. When `Mixer` inherits from `IncrementalMerkleTree`, its constructor must call the parent contract's constructor with the necessary arguments. The `Mixer` constructor will now accept `_merkleTreeDepth` and a `Poseidon2 _hasher` instance, passing them along to the `IncrementalMerkleTree` constructor. ```solidity // src/Mixer.sol contract Mixer is IncrementalMerkleTree { IVerifier public i_verifier; uint256 public constant DENOMINATION = 1 ether; // Example denomination mapping(bytes32 => bool) public s_commitments; // To track used commitments event Deposit(bytes32 indexed commitment, uint32 insertedIndex, uint256 timestamp); constructor(IVerifier _verifier, uint32 _merkleTreeDepth, Poseidon2 _hasher) IncrementalMerkleTree(_merkleTreeDepth, _hasher) // Call parent constructor { i_verifier = _verifier; } // ... other functions like deposit and withdraw } ``` This setup ensures that when a `Mixer` contract is deployed, the underlying `IncrementalMerkleTree` is also properly initialized. ## Completing the Deposit Function in the Mixer Contract The `deposit` function is central to the mixer's operation, allowing users to add their commitments (shielded notes) to the Merkle tree. We'll now finalize this function by incorporating the Merkle tree insertion and emitting a crucial event for off-chain processes. ### Inserting the Commitment into the Merkle Tree When a user calls `deposit` with their `_commitment`, this commitment needs to be inserted as a leaf into the Merkle tree. Since `Mixer` now inherits from `IncrementalMerkleTree`, we can directly call the `_insert` function (which is `internal`). This function will handle the hashing, update the tree structure, store the new root, and return the index at which the commitment was inserted. ```solidity // src/Mixer.sol (within deposit function) // ... (pre-checks for commitment uniqueness and denomination) ... // 3. Add the commitment to the on-chain incremental Merkle tree // This calls the _insert function inherited from IncrementalMerkleTree uint32 insertedIndex = _insert(_commitment); ``` The `insertedIndex` variable now holds the leaf index of the newly deposited commitment. ### Emitting the Deposit Event After successfully inserting the commitment, it's vital to emit an event. Smart contract events are the primary mechanism for off-chain services to listen to on-chain activity and synchronize their state. The `Deposit` event will provide the necessary information for users or relayers to reconstruct the Merkle tree off-chain, which is essential for generating withdrawal proofs. The event definition should include the commitment itself, its index in the tree, and a timestamp. ```solidity // src/Mixer.sol (event definition, usually at the contract level) event Deposit(bytes32 indexed commitment, uint32 insertedIndex, uint256 timestamp); ``` Within the `deposit` function, after the `_insert` call, we emit this event: ```solidity // src/Mixer.sol (within deposit function) // ... // uint32 insertedIndex = _insert(_commitment); // s_commitments[_commitment] = true; // Mark commitment as used // 5. Emit an event with the commitment, its index, and timestamp emit Deposit(_commitment, insertedIndex, block.timestamp); ``` **Rationale for Event Parameters:** * `_commitment`: This is the actual leaf data added to the tree. Off-chain clients collect these commitments. Directly iterating through on-chain mappings (like `s_filledSubtrees` or a conceptual `s_leaves` array if it existed for all leaves) from an off-chain application is inefficient or impossible. Events provide a direct data feed. * `insertedIndex`: This tells the off-chain client the exact position (index) of the `_commitment` within the Merkle tree's leaves. This is crucial for correctly placing the leaf when reconstructing the tree off-chain. * `block.timestamp`: Including the timestamp helps in correctly ordering deposits, especially if events are fetched or processed in batches or if there's any ambiguity in transaction ordering from the event consumer's perspective. ### Full `deposit` Function Structure Putting it all together, the `deposit` function in `Mixer.sol` will look like this: ```solidity // src/Mixer.sol function deposit(bytes32 _commitment) external payable { // 1. Check if the commitment has already been used if (s_commitments[_commitment]) { revert("Mixer: Commitment already added."); // Or use custom error } // 2. Check if the amount of ETH sent is the correct denomination if (msg.value != DENOMINATION) { revert("Mixer: Deposit amount not correct."); // Or use custom error } // 3. Add the commitment to the on-chain incremental Merkle tree // This calls the _insert function inherited from IncrementalMerkleTree uint32 insertedIndex = _insert(_commitment); // 4. Mark the commitment as used to prevent re-depositing the same commitment s_commitments[_commitment] = true; // 5. Emit an event with the commitment, its index, and timestamp emit Deposit(_commitment, insertedIndex, block.timestamp); } ``` This function now correctly handles deposits, updates the on-chain Merkle tree, and provides the necessary information for off-chain proof generation via the `Deposit` event. ## Understanding Key Concepts for ZK-Mixer Deposits Several core concepts underpin the functionality of the `deposit` process in a ZK-mixer. Understanding these is crucial for appreciating the design and security of the system. * **Incremental Merkle Tree:** This data structure is at the heart of the mixer. It allows for efficient addition of new leaves (commitments) and recalculation of the Merkle root. Instead of recomputing the entire tree from scratch with each new deposit, only the path from the new leaf to the root needs updating, significantly saving gas. * **Off-Chain Merkle Tree Reconstruction:** Users (or relayers acting on their behalf) need to generate zero-knowledge proofs to withdraw their funds. These proofs typically require demonstrating knowledge of a secret corresponding to a commitment within the Merkle tree, along with the Merkle path to that commitment. Building and maintaining a full copy of the Merkle tree off-chain is necessary because storing and retrieving all tree data on-chain for every user's proof generation would be prohibitively expensive. * **Events for Off-Chain Data Synchronization:** Smart contract storage, particularly mappings, are not easily iterable from external applications. Events (like our `Deposit` event) serve as a broadcast mechanism. Off-chain services listen for these events to collect all deposited commitments and their indices, allowing them to accurately reconstruct and maintain their local copy of the Merkle tree in sync with the on-chain state. * **Fixed Denomination:** The `DENOMINATION` check (`msg.value == DENOMINATION`) is a critical privacy feature. If users could deposit and withdraw arbitrary amounts, these amounts could become a linking factor, potentially deanonymizing transactions. By enforcing a fixed denomination (e.g., 1 ETH, 10 ETH), all deposits and withdrawals within that pool are fungible from an amount perspective, enhancing privacy. * **Poseidon Hash Function:** The choice of hash function is vital in ZK-SNARK applications. Poseidon is specifically designed to be "SNARK-friendly," meaning it's efficient to compute within a zero-knowledge circuit. This is essential because the Merkle tree computations (hashing pairs of nodes) will be part of the ZK proof for withdrawals. ## Important Considerations and Best Practices A few additional notes and tips are relevant when working with the `deposit` function and the Merkle tree integration: * **`_insert` Function Visibility:** The `_insert` function in `IncrementalMerkleTree.sol` is marked `internal`. This visibility means it can be called by the contract itself and by contracts that inherit from it, such as our `Mixer.sol`. It cannot be called directly by external accounts or other non-derived contracts. * **`s_commitments` Mapping for Uniqueness:** The `mapping(bytes32 => bool) public s_commitments;` in `Mixer.sol` plays a crucial role in preventing replay attacks or accidental double-deposits of the same commitment. Before a new commitment is added to the Merkle tree, the `deposit` function checks this mapping. If `s_commitments[_commitment]` is `true`, it means the commitment has already been processed, and the transaction reverts. After a successful insertion, `s_commitments[_commitment]` is set to `true`. * **Error Handling:** Using `revert` with descriptive messages or, even better, custom errors (e.g., `revert Mixer_CommitmentAlreadyAdded(_commitment);`) improves the developer and user experience by providing clear reasons for transaction failures. With the `deposit` function now complete, including robust Merkle tree integration and event emission, the ZK-mixer is ready to accept user commitments. The next logical step involves developing the `withdraw` function, which will leverage the Merkle tree and require users to provide zero-knowledge proofs.
A constructive guide to Finalizing the Merkle Tree Insertion Logic - Master the completion of the Merkle tree's `_insert` function, covering new root storage and leaf index handling. See this logic integrated into the Mixer contract, enabling the `deposit` function and event emission for off-chain tree reconstruction.
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