Merkle Tree em Python Puro: A Árvore Que Detecta Se Um Único Byte Foi Alterado em 10 GB de Dados (Sem Conferir Tudo)

Você tem 10 GB de logs. Ou 50.000 arquivos de backup. Ou um dataset crítico que precisa provar que não foi adulterado. Conferir byte por byte? Esquece. O Merkle Tree resolve isso com uma única hash de 64 caracteres — e te diz exatamente onde está a diferença se algo mudou.

O Problema Que Ninguém Te Conta

Quando você precisa verificar se um arquivo grande foi corrompido ou alterado, a solução óbvia é calcular o hash do arquivo inteiro. SHA-256, pronto. Mas e quando você tem milhares de arquivos? E quando só um bloco de 4 KB foi alterado em 10 GB de dados? Recalcular tudo é burrice.

O Git resolve isso de forma elegante: ele não armazena só o hash do commit. Ele constrói uma árvore de hashes onde cada nó pai é o hash dos filhos. Se um único byte muda em qualquer arquivo, a hash raiz muda. E você pode encontrar qual bloco mudou em O(log n) — sem recalcular tudo.

Isso é um Merkle Tree. E hoje você vai implementar um do zero.

O Que É Um Merkle Tree (Sem Diagrama Chato)

Pense assim:

1. Você quebra seus dados em blocos (ex: 4 KB cada)

2. Calcula o hash de cada bloco → essas são as folhas

3. Agrupa as folhas em pares e calcula o hash de cada par → nós internos

4. Repete até sobrar um único hash → a raiz (root hash)

Se qualquer bloco muda, todos os hashes acima dele mudam. A raiz final é uma “impressão digital” de todos os dados. E a mágica: você pode provar que um bloco específico pertence ao conjunto original mostrando apenas os hashes do “caminho” até a raiz — isso se chama Merkle Proof.

Implementação Completa: Merkle Tree em Python Puro

Sem bibliotecas externas. Só hashlib da stdlib.

import hashlib
from typing import List, Optional, Tuple
from dataclasses import dataclass

@dataclass
class MerkleNode:
    hash: str
    left: Optional['MerkleNode'] = None
    right: Optional['MerkleNode'] = None
    
    @property
    def is_leaf(self) -> bool:
        return self.left is None and self.right is None

class MerkleTree:
    def __init__(self, data_blocks: List[bytes]):
        if not data_blocks:
            raise ValueError("Precisa de pelo menos um bloco de dados")
        
        self.blocks = data_blocks
        self.leaves = [self._hash(block) for block in data_blocks]
        self.root = self._build_tree(self.leaves)
    
    def _hash(self, data: bytes) -> str:
        """SHA-256 de qualquer coisa."""
        return hashlib.sha256(data).hexdigest()
    
    def _hash_pair(self, left: str, right: str) -> str:
        """Hash da concatenação de dois hashes (ordenados)."""
        # Ordenar garante que hash(a,b) == hash(b,a) não aconteça
        combined = left + right if left <= right else right + left
        return self._hash(combined.encode())
    
    def _build_tree(self, hashes: List[str]) -> MerkleNode:
        """Constrói a árvore bottom-up."""
        if len(hashes) == 1:
            return MerkleNode(hash=hashes[0])
        
        nodes = [MerkleNode(hash=h) for h in hashes]
        
        while len(nodes) > 1:
            next_level = []
            
            for i in range(0, len(nodes), 2):
                left = nodes[i]
                # Se ímpar, duplica o último nó
                right = nodes[i + 1] if i + 1 < len(nodes) else nodes[i]
                
                parent_hash = self._hash_pair(left.hash, right.hash)
                parent = MerkleNode(hash=parent_hash, left=left, right=right)
                next_level.append(parent)
            
            nodes = next_level
        
        return nodes[0]
    
    def get_proof(self, index: int) -> List[Tuple[str, str]]:
        """
        Retorna a Merkle Proof para o bloco no índice dado.
        Cada elemento é (hash, posição) onde posição é 'left' ou 'right'.
        """
        if index < 0 or index >= len(self.leaves):
            raise IndexError(f"Índice {index} fora do range [0, {len(self.leaves)})")
        
        proof = []
        current_level = self.leaves
        current_index = index
        
        while len(current_level) > 1:
            next_level = []
            
            for i in range(0, len(current_level), 2):
                left = current_level[i]
                right = current_level[i + 1] if i + 1 < len(current_level) else current_level[i]
                
                # Se estamos no par atual, adiciona o irmão à proof
                if i == current_index - (current_index % 2):
                    if current_index % 2 == 0:
                        # Sou o left, meu irmão é right
                        proof.append((right, 'right'))
                    else:
                        # Sou o right, meu irmão é left
                        proof.append((left, 'left'))
                
                parent_hash = self._hash_pair(left, right)
                next_level.append(parent_hash)
            
            current_level = next_level
            current_index = current_index // 2
        
        return proof
    
    def verify_proof(self, block: bytes, proof: List[Tuple[str, str]], root: str) -> bool:
        """Verifica se um bloco pertence à árvore usando apenas a proof."""
        current_hash = self._hash(block)
        
        for sibling_hash, position in proof:
            if position == 'left':
                current_hash = self._hash_pair(sibling_hash, current_hash)
            else:
                current_hash = self._hash_pair(current_hash, sibling_hash)
        
        return current_hash == root

Caso de Uso Real: Verificador de Integridade de Backups

Agora a parte que importa. Vamos usar o Merkle Tree para verificar se um backup de 10 GB não foi corrompido — sem recalcular tudo.

import os
from pathlib import Path

class BackupIntegrityChecker:
    def __init__(self, chunk_size: int = 4096):  # 4 KB chunks
        self.chunk_size = chunk_size
    
    def chunk_file(self, filepath: str) -> List[bytes]:
        """Lê um arquivo em chunks de tamanho fixo."""
        chunks = []
        with open(filepath, 'rb') as f:
            while True:
                chunk = f.read(self.chunk_size)
                if not chunk:
                    break
                chunks.append(chunk)
        return chunks
    
    def create_manifest(self, filepath: str) -> dict:
        """
        Cria um manifest com a root hash e todas as leaf hashes.
        Salve isso em lugar seguro.
        """
        chunks = self.chunk_file(filepath)
        tree = MerkleTree(chunks)
        
        return {
            'filepath': filepath,
            'root_hash': tree.root.hash,
            'leaf_hashes': tree.leaves,
            'chunk_size': self.chunk_size,
            'total_chunks': len(chunks),
            'file_size': os.path.getsize(filepath)
        }
    
    def verify_file(self, filepath: str, manifest: dict) -> dict:
        """
        Verifica se o arquivo ainda corresponde ao manifest.
        Retorna quais chunks estão corrompidos (se houver).
        """
        chunks = self.chunk_file(filepath)
        
        if len(chunks) != manifest['total_chunks']:
            return {
                'valid': False,
                'error': f"Tamanho mudou: {len(chunks)} chunks vs {manifest['total_chunks']} esperado"
            }
        
        # Verifica a root hash primeiro (rápido)
        tree = MerkleTree(chunks)
        if tree.root.hash == manifest['root_hash']:
            return {'valid': True, 'corrupted_chunks': []}
        
        # Se falhou, encontra quais chunks específicos mudaram
        corrupted = []
        for i, (chunk, expected_hash) in enumerate(zip(chunks, manifest['leaf_hashes'])):
            actual_hash = tree._hash(chunk)
            if actual_hash != expected_hash:
                corrupted.append({
                    'chunk_index': i,
                    'byte_offset': i * self.chunk_size,
                    'expected_hash': expected_hash,
                    'actual_hash': actual_hash
                })
        
        return {
            'valid': False,
            'corrupted_chunks': corrupted,
            'total_corrupted': len(corrupted)
        }

# Exemplo de uso
checker = BackupIntegrityChecker(chunk_size=8192)  # 8 KB chunks

# Primeira vez: cria o manifest
manifest = checker.create_manifest('/backup/database_2026_07_14.sql')
print(f"Root hash: {manifest['root_hash']}")
print(f"Total chunks: {manifest['total_chunks']}")

# Salva o manifest (JSON, banco, onde quiser)
import json
with open('/backup/database_2026_07_14.manifest.json', 'w') as f:
    json.dump(manifest, f, indent=2)

# Depois: verifica integridade
with open('/backup/database_2026_07_14.manifest.json', 'r') as f:
    saved_manifest = json.load(f)

result = checker.verify_file('/backup/database_2026_07_14.sql', saved_manifest)

if result['valid']:
    print("✅ Arquivo íntegro")
else:
    print(f"❌ {result['total_corrupted']} chunks corrompidos:")
    for chunk in result['corrupted_chunks']:
        offset_mb = chunk['byte_offset'] / (1024 * 1024)
        print(f"   Chunk {chunk['chunk_index']} (offset: {offset_mb:.2f} MB)")

Por Que Isso É Melhor Que SHA-256 Simples?

| Abordagem | Tempo para verificar 10 GB | Detecta **onde** mudou? |

|———–|—————————|————————|

| SHA-256 do arquivo inteiro | ~30 segundos | ❌ Não |

| SHA-256 de cada chunk (sem árvore) | ~30 segundos + O(n) comparações | ✅ Sim, mas lento |

| **Merkle Tree** | ~30 segundos (primeira vez) + O(log n) depois | ✅ Sim, rápido |

A mágica está na Merkle Proof: você pode provar que um bloco específico pertence ao conjunto mostrando apenas ~20 hashes (para 1 milhão de blocos), não todos.


🧠 O Perrengue do Olivetto

Semana passada eu tinha um script que fazia backup de 8 GB de logs toda madrugada. Depois copiava pra um NAS via rsync. Um dia o rsync disse “done” mas o arquivo no NAS estava com 3 bytes corrompidos no meio — provavelmente erro de rede que o TCP não pegou.

Demorei 2 dias pra perceber que os logs estavam inconsistentes. Se eu tivesse guardado um manifest com Merkle Tree, teria detectado o chunk corrompido em segundos — e sabido exatamente quais 4 KB retransmitir, não os 8 GB inteiros.

A lição: integridade não é binária. Não basta saber “mudou ou não”. Você precisa saber onde mudou pra consertar rápido.


Onde Mais Isso É Usado?

Git: cada commit tem uma Merkle Tree dos arquivos

IPFS: arquivos são endereçados pela root hash

Bitcoin/Ethereum: blocos usam Merkle Trees pra provar transações

Btrfs/ZFS: filesystems com checksum por bloco (mesma ideia)

S3/Glacier: alguns serviços usam pra verificação de integridade

CTA: O Que Você Vai Verificar Hoje?

Agora você tem um verificador de integridade que:

– Detecta corrupção em qualquer arquivo

– Te diz exatamente onde está o problema

– Usa O(log n) espaço pra proofs

– Roda em Python puro, sem dependências

Pergunta: qual arquivo crítico do seu sistema você não verifica hoje? Aquele backup que você “tem certeza” que está OK? Roda esse código nele agora. Se der match, dorme tranquilo. Se não der, você acabou de se livrar de um bug que ia te acordar às 3 da manhã.

Testa aí e me conta nos comentários: qual foi o arquivo mais suspeito que você verificou?

Posts Similares