Katkat

Implement and Explain a Merkle Tree

CursorSoftware Engineer · Technical Screen

Last 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?