Skip to main content

Merkle DAG

Every commit produces a cryptographic fingerprint of the entire branch state, stored in a Merkle DAG (Directed Acyclic Graph).

Merkle Tree Structure

A Merkle tree is a binary tree of hashes:
  • Leaves are hashes of the actual data files
  • Internal nodes are hashes of their two children
  • Root is a single 32-byte value representing every file in the branch
If any single bit in any file changes, the root hash changes completely.

Leaf Hashing

Each leaf hash incorporates both the file’s relative path and its content:
This means:
  • Two files with identical content but different paths produce different leaf hashes
  • Renaming a file changes the tree root even without content changes
  • Paths are normalized to forward slashes for cross-platform consistency

Tree Construction

The tree is built in a deterministic order:
  1. Walk the branch directory, skipping excluded files (*.sock, *.pid, postmaster.pid)
  2. Sort leaves by relative path (alphabetical)
  3. Pair leaves and hash them together
  4. If an odd leaf remains at any level, promote it to the next level
  5. Continue pairing until one hash remains — the root
An empty branch (no files) produces a well-known root:

The DAG

Commits form a directed acyclic graph through parent references:
Each commit records:
  • hash — 7-character BLAKE3 digest of tree_root + parent_hash + timestamp
  • branch — Which branch this commit belongs to
  • parent_hash — Previous commit hash (NULL for first commit)
  • tree_root — Merkle tree root (64 hex chars)
  • verified — Whether the tree has been verified against the stored root
  • message — User-provided commit message
  • created_at — ISO 8601 timestamp

SQLite Schema

Why BLAKE3?

For Graft’s workload (hashing ~130 files per commit on a 2GB database), BLAKE3 completes in ~2-3 seconds. SHA-256 would take 3-4x longer.

File-Level Hashing

Graft hashes files, not blocks. A 2GB Postgres database has ~130 files (not 525K 4KB pages). This means:
  • Small Merkle tree (~6KB in memory)
  • Fast to compute (~2-3 seconds)
  • Actionable errors: base/16384/12547 is corrupted tells you exactly what’s wrong
  • Acceptable trade-off: corruption is detected at file granularity, not block granularity