Implement and Explain a Merkle Tree
CursorSoftware Engineer · Technical ScreenLast updated
Tests understanding of cryptographic hash trees (Merkle trees), including tree construction, odd-node handling, proof generation, and verification.
Problem Statement
A Merkle tree is a binary tree where every leaf node represents the cryptographic hash of a data block, and every non-leaf node represents the cryptographic hash of its concatenated children's hashes. Merkle trees enable efficient and secure verification of contents in large data structures.
Implement a MerkleTree class using Python's hashlib.sha256 that supports constructing a tree from a list of string data blocks, generating the Merkle root hash, generating an audit proof for a specific leaf, and verifying a proof against a known root.
Hashing Conventions
- Leaf hash:
sha256(data.encode('utf-8')).hexdigest() - Parent hash:
sha256((left_hash + right_hash).encode('utf-8')).hexdigest() - If a level has an odd number of nodes (greater than 1), duplicate the last node's hash to form a pair.
Class Interface
class MerkleTree:
def __init__(self, data_blocks: list[str]):
"""
Initializes the Merkle tree with a non-empty list of data strings.
"""
pass
def get_root(self) -> str:
"""
Returns the hex digest of the Merkle root.
"""
pass
def get_proof(self, index: int) -> list[tuple[str, str]]:
"""
Generates an audit proof for the leaf at `index` (0-based).
Each element in the proof list is a tuple: (sibling_hash, direction),
where direction is 'L' if sibling is on the left, or 'R' if on the right.
The list should be ordered from leaf level up to the root level.
"""
pass
@staticmethod
def verify_proof(data: str, proof: list[tuple[str, str]], root: str) -> bool:
"""
Verifies whether `data` belongs to the tree represented by `root`
using the provided `proof`.
"""
pass
Constraints
1 <= len(data_blocks) <= 10^4- Each block is an ASCII string of length
1 <= len(data) <= 10^3 0 <= index < len(data_blocks)
Example
tree = MerkleTree(["a", "b", "c"])
root = tree.get_root()
proof = tree.get_proof(0)
# Leaf 0 has sibling leaf 1 on the right
# Level 0 hashes: [H("a"), H("b"), H("c"), H("c")] (c is duplicated)
# Parent 0: H(H("a") + H("b")), Parent 1: H(H("c") + H("c"))
# Root: H(Parent 0 + Parent 1)
assert MerkleTree.verify_proof("a", proof, root) == True
assert MerkleTree.verify_proof("tampered", proof, root) == False
Edge Cases to Consider
- Single element lists (
len(data_blocks) == 1). - Odd numbers of elements at multiple levels of the tree.
Hints
Hint 1
Build the tree level-by-level from bottom to top, saving each intermediate level so you can easily trace sibling paths for proofs.
Hint 2
When computing sibling index at level `k` for position `idx`: if `idx` is even, sibling is `idx + 1` (on the right); if odd, sibling is `idx - 1` (on the left).
Hint 3
During verification, start by hashing the raw data block, then fold each sibling hash into the running hash according to the direction flag ('L' means sibling + current, 'R' means current + sibling).
Follow-up questions
- How would you optimize `MerkleTree` to support dynamic updates (modifying a leaf) in `O(log n)` time without rebuilding the entire tree?
- What security vulnerability arises from using `sha256(left + right)` without domain separation or prefixes (second-preimage attack), and how can you mitigate it?
- How does Git's internal commit and tree model differ from a standard binary Merkle tree?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.