Skip to content

Content Addressing of File System Objects ​

A primary-source survey of how twenty-one systems turn a tree of files into a digest — from Plan 9's Venti in 2002 through Nix, git, OSTree and Bazel's REAPI to the intra-file Merkle trees of Bao and fs-verity — and of the two places the field's own experience says it goes wrong: tar's absent canonical form and cache keys mistaken for content addresses.

The evidence base for the content-addressing layer of sparkles:build-primitives, and the falsification gate for the scheme seam that layer is designed around.

This survey answers nine questions:

  1. Serial serialization or Merkle DAG? Overwhelmingly Merkle, and everything a build system wants — subtree caching, O(depth) rehash, independent verification, parallel hashing — lives on that side of the line. See axis 3.
  2. Is there one canonical entry order? No. Four canonical answers, two deliberately non-canonical ones, and one subject that never orders at all. Two of the four disagree on inputs as ordinary as a directory a beside a file a.b. See axis 2 and the worked case.
  3. What belongs inside an identity? Timestamps: almost unanimously not. Everything above that floor: no agreement whatsoever. See axis 1.
  4. Are intra-file chunk trees a peer-to-peer curiosity? No — REAPI put one in a build protocol in 2023, and Venti had one in 2002. See axis 4.
  5. Can canonical form be retrofitted onto a format that lacks it? Only partly, and the people who tried have a list of what provably cannot be fixed. OCI is the ecosystem-scale demonstration.
  6. What does a level-2 tree buy over an index of chunk digests?O(log n) verification against a 32-byte root versus depth-2 trust requiring the whole index first. See index versus tree.
  7. How does the field handle case and duplicate collisions? Five ways, only one of which detects at ingest without corrupting the identity. See the disagreements.
  8. Is a cache key a content address? No, and ccache's sloppiness list is a published taxonomy of exactly how they differ.
  9. What should sparkles:build-primitives adopt? See the recommendations — including the two places this survey contradicted an earlier decision.

Last reviewed: September 21, 2026

NOTE

Scope. This tree is about turning a tree of files into a digest. It is not about selecting the files — that is Fileset & File-Selection Query Languages — nor about the store, signature and cache layers above the digest, which sparkles:nix reaches through libnixstore-c rather than reimplementing.

Deliberately excluded, and named so they are not re-derived: typed-data Merkleization (Ethereum SSZ) addresses consensus objects rather than file system objects. Content-defined chunking is not excluded but is treated as what it is — a level-2 boundary policy, surveyed where a subject uses one (casync, bup, unixfs, Snix). Deferred, noted not omitted: Perkeep, restic/borg, Go's checksum database (a Merkle log, a different shape), and git packfile delta encoding (storage beneath identity).


Master catalog ​

SubjectEcosystemLevel 1 (directory tree)Level 2 (file bytes)Entry orderingDigestLink
VentiPlan 9pointer-block tree; directories are filessame pointer treen/a — no namesSHA-1 "score"venti
Nix Archive (NAR)Nixserial serializationwhole blob, inlinebyte-wise, one sequenceSHA-256 over the streamnar
Git objectsGitMerkle DAGwhole blobbyte-wise + implicit /SHA-1 / SHA-256git
Git cache-treeGit— (incremental maintenance of the above)———cachetree
Nix git-hashingNix (experimental)git trees over Nix's FSO modelgit blobsgit'sSHA-1 / SHA-256nixgit
Snix castoreSnix (Nix reimplementation)Merkle DAG, protobuf Directoryplain BLAKE3; FastCDC below identitybyte-wise, three arraysBLAKE3castore
Mercurial → SaplingMercurial, Saplingflat whole-tree listing, then per-directory treesper-file revlogsorted rowsSHA-1 over parents + contenthg
Bazel REAPIBazel, Buck2, PantsMerkle DAG of protobuf Directorywhole blob or SHA256TREEbyte-wise, three arrays{hash, size_bytes}, 7 functionsreapi
OSTreeLinux OS imagesMerkle DAG (dirtree + dirmeta)whole blob + headerbyte-wise, two arraysSHA-256ostree
Software HeritageSource-code archiveMerkle DAG above the tree (dir→rev→rel→snp)sha1_gitgit's, reimplementedSHA-1, fixed by the grammarswh
bup / git-annexBackupgit trees, with fanoutcontent-defined chunks as git blobsgit'sSHA-1bup
casync / desyncsystemd ecosystemcatar, a canonical serial formatCDC across file boundariescanonicalSHA-512/256 per chunkcasync
IPFS unixfs / CIDIPFS, FilecoinDAG-PB; huge directories HAMT-shardedpluggable chunkern/a (links, not sorted names)self-describing CIDipfs
Bao / BLAKE3irohnoneMerkle chunk treen/aBLAKE3, 32 raw bytesbao
Tahoe-LAFSDistributed storagemutable dirnodes; no ordering at allthree-tier tree over erasure-coded ciphertextnonecapability, not a hashtahoe
OCI image layersContainersan ordered chain of tar layerswhole tar archiveunspecifiedsha256: ×3 identitiesoci
eStargz / SOCIContainerstar, made seekableper-chunk digests in an indexworkload-derivedTOC digest / zTOCestargz
composefs + fs-verityLinux, OSTreeEROFS image over a shared storekernel-built, kernel-enforced Merkle treeimage layoutparameterized descriptor digestcfs
dm-verity / APK v4Linux, Androidnone — numbered blocksMerkle tree; shipped in .apk.idsign/aparameterized, signed rootapk
Reproducible archivesDebian, wider ecosystem— (a retrofit onto tar/zip/ar/cpio)—only two formats can be ordered at all—repro
ccache / sccache / LLVM CASBuild caches— (inputs, not trees)——BLAKE3 key, saltedcaches

Shared vocabulary — file system object, the level-1/level-2 split, canonical form, parameterized digests, identity versus provenance, index versus tree — is defined once in concepts.

By level-1 composition ​

| Composition | Systems | Consequence | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------- | | Serial serialization | NAR, casync | No subtree values; sharing must be recovered below the tree, at chunk level, or not at all | | Flat whole-tree listing | Mercurial | O( | repo files | ) to parse — the failure that forced the move to trees | | Merkle DAG | git, Nix git-hashing, Snix, REAPI, OSTree, SWH, bup, unixfs | Subtree identity, O(depth) rehash, object-granular verification | | Chain, not tree | OCI | O(n) invalidation in the layer count; order-dependent semantics | | None — level 2 only | Bao, dm-verity | Proof the two levels are independent | | Level 2 is level 1 | Venti | The sharpest proof of the same point: one machine run twice |

By entry ordering ​

AnswerSystemsMechanism
One sequence, raw bytesNARstd::string <; the parser rejects name <= prevName
One sequence, / appended to directoriesgit, Nix git-hashing, SWH, bupbase_name_compare substitutes / for a directory name's terminating NUL — four independent implementations
Two arrays, raw bytesOSTreefiles and directories sorted separately by strcmp
Three arrays, raw bytesREAPI, Snixfiles, directories and symlinks each sorted by UTF-8 bytes
A privileged prefixJAR, via strip-nondeterminisma comparator forcing META-INF/ first
Workload-derivedeStargza prefetch plan from a profiling run; .prefetch.landmark marks the boundary
Versioned in the dataVenti's vaca known-wrong comparator retired by minting a new block magic
NoneTahoe, unixfschildren are never sorted
UnspecifiedOCIwhatever the producer's tar emitted

By metadata floor ​

FloorSystems
None — bytes or blocks onlyBao, Venti, dm-verity
One executable bitNAR, Nix git-hashing, Snix
Path + revision id + one flagMercurial
Five modes, incl. symlink and submodulegit, SWH
Executable bit + open key/value properties (incl. optional mtime)REAPI
uid, gid, mode, xattrs — inside the content hashOSTree
Negotiable by feature flagcasync
Full POSIX, including timestampsOCI, composefs

By partial verification ​

GranularitySystems
None — all or nothingNAR, Mercurial (flat), OCI
Per object / per directorygit, Nix git-hashing, OSTree, REAPI, SWH, bup
Per chunk, via an index (depth-2 trust)eStargz / SOCI, casync
Per chunk, via a tree (O(log n))Bao, REAPI SHA256TREE, fs-verity, dm-verity / APK v4, Tahoe
Reserved but unimplementedSnix — StatBlobResponse.bao is // still todo

Milestones ​

| Year | Event | | ---------- | --------------------------------------------------------------------------------------------------------------- | ---------- | ------ | ------------- | ----------- | | 1980 | Merkle publishes hash trees (IEEE S&P) — cited by Venti as its ancestor | | 1998 | Stanford Archival Vault: a write-once object log, with "no way to share data between objects" | | 2000 | SFSRO (OSDI): SHA-1 blocks "applied recursively" | | 2001-10 | LBFS (SOSP): content-defined chunking | | 2002-01 | Venti (FAST '02): a block-level content-addressed store; directories are files of VtEntry scores | | 2003 | NAR appears in the Nix tree — archive.cc's history begins 2003-11-18 | | 2005-04 | Git's object model ships (first commit 2005-04-07) | | 2011-10 | OSTree begins — git's model plus uid/gid/mode/xattrs, minus timestamps | | 2018–2019 | REAPI v2 stabilizes the Directory/Digest canonical form | | 2019-10 | Sapling's retrospective states the O( | repo files | )→O( | changed files | ) argument | | 2020 | BLAKE3 published — a tree hash by construction, making verified streaming practical | | 2021-04 | SWHID v1.6 — SHA-1 welded into the identifier grammar | | 2022-05 | Nix's git-hashing implementation lands — approximate | | 2023-03-14 | REAPI adds SHA256TREE, "a version of SHA-256 that supports chunking" (PR #235) | | 2024–2025 | iroh settles on 16 KiB chunk groups; Snix ships BLAKE3 identity with NAR at the boundary |

NOTE

Dates come from the repositories' own histories where a local clone or forge API was available, and from cited papers otherwise. Git-history dates record when an implementation appeared, which may postdate a design; entries marked approximate are exactly that.


Suggested reading paths ​

"I want the findings, not the survey." → Synthesis & recommendations — in particular axis 2 (no single canonical order), axis 4 (chunk trees are not exotic), and the two contradictions this survey produced against earlier decisions.

"I'm implementing the content-addressing layer." → concepts → NAR → git objects → Nix git-hashing → Snix castore (a production system that made the same architectural choice, and one different one) → recommendations.

"I'm designing the scheme seam." → the falsification gate, then REAPI and Bao, which stress opposite ends of it.

"I care about incremental rebuilds." → git cache-tree (the mechanism, and racy-git) → build input caches (the same shape with the opposite failure mode).

"I care about metadata fidelity." → OSTree → composefs + fs-verity → axis 1.

"I want to know why tar keeps failing." → OCI layers → reproducible archives → NAR's rationale.


Sources ​

Every GitHub citation pins a 40-character commit SHA; non-GitHub forges are pinned by commit where the forge supports it. Verified against local clones or forge APIs:

  • Nix — NixOS/nix at 1d8bdc1ee63246b591a8d77d0d481485cd09d438
  • Git — git/git at f78ce2f7b6df702f93d40b85d6bda92a3f65da79
  • Linux — torvalds/linux at e43ffb69e0438cddd72aaa30898b4dc446f664f8
  • OSTree — ostreedev/ostree at 1d5a312a3189b0fbd70fe6769aadb19a366fedb2
  • Bazel REAPI — bazelbuild/remote-apis at 76ddd98e1f92c0e2e71d0d3aa6906eca31754c03
  • iroh blobs — n0-computer/iroh-blobs at e82cbdcbdac9a78033174aad55e3199b2cf4c0dc
  • Sapling — facebook/sapling at 87b7db94b0a7fc5186b472f0ee8d01fd5a2036a2
  • OCI image-spec — opencontainers/image-spec at af26a05fba5ee648512f4ea3c9fda1fcc1b6d6dc
  • casync — systemd/casync at b4b7e5606f785572b78a43626a27a45fe3df2fbd
  • Snix — snix/snix at 86a6e6ef8c1c43d015b09f10051de78b2cae548b (Forgejo, git.snix.dev)
  • strip-nondeterminism — salsa.debian.org at 904281367bd7fbc34a0831a28dd40457b6753c1c

Related in-repo trees: Fileset & File-Selection Query Languages (which files to name), iroh (the peer-to-peer side of Bao), and Monorepo Tooling (what consumes a build-input digest).