OCI image layers
The survey's counter-example: an ecosystem-scale content-addressing scheme built on a serialization format that has no canonical form — exactly the failure NAR was invented to avoid.
| Ecosystem | Docker, containerd, Podman, every container registry |
| Level 1 | an ordered chain of tar layers, not a tree |
| Level 2 | whole tar archive, compressed or not |
| Node model | POSIX tar — modes, uid/gid, mtime, hardlinks, plus .wh.* whiteouts |
| Entry ordering | unspecified — whatever the producer's tar emitted |
| Digest | sha256:<hex>, with DiffID / layer digest / ChainID as three distinct identities |
| Specification | config.md, layer.md |
Overview
What it solves
Naming, transferring and deduplicating filesystem changesets, so an image is a base plus deltas and a registry stores each delta once.
Design philosophy
Unlike every other subject here, OCI did not define a serialization — it adopted tar, and then had to add rules around it. The consequence is visible in the specification's own hedging (config.md):
A layer DiffID is the digest over the layer's uncompressed tar archive […] Layers SHOULD be packed and unpacked reproducibly to avoid changing the layer DiffID, for example by using tar-split to save the tar headers.
Read that twice. The identity of a layer depends on byte-level details of a tar archive that the format does not canonicalize, so the recommended mitigation is to save the original tar headers alongside the content and replay them — because regenerating them is not guaranteed to reproduce the same bytes. Every Merkle scheme in this survey defines a canonical form precisely so that step is unnecessary.
How it works
Three identities, which are routinely confused:
| Identity | Digest over | Used for |
|---|---|---|
| layer digest | the layer blob as stored (usually gzip- or zstd-compressed) | registry addressing, the manifest |
DiffID | the layer's uncompressed tar archive | the image config's rootfs.diff_ids |
ChainID | a recursion over DiffIDs | identifying the result of applying a prefix of the stack |
The ChainID recursion is the closest thing OCI has to a Merkle structure (config.md):
ChainID(L₀) = DiffID(L₀)
ChainID(L₀|...|Lₙ₋₁|Lₙ) = Digest(ChainID(L₀|...|Lₙ₋₁) + " " + DiffID(Lₙ))While a layer's
DiffIDidentifies a single changeset, theChainIDidentifies the subsequent application of those changesets.
Dimension 1 — node model
Whatever tar carries: mode bits, uid/gid, mtime, device nodes, hardlinks. This is the richest node model in the survey and the least disciplined — the fields that Nix and OSTree exclude because they vary between machines are all inside the digest here. Plus a model no one else has: whiteouts (.wh.<name> entries) encoding deletion, which only make sense because a layer is a diff rather than a tree.
Dimension 2 — canonical form and ordering
There is none. Tar entry order is the producer's, padding and header field encodings vary between implementations (GNU versus pax versus ustar), and mtimes are recorded verbatim. Two builders producing byte-identical trees routinely produce different DiffIDs. This is the single strongest empirical argument for the position NAR's own rationale states in the abstract.
Dimension 3 — level-1 composition
A chain, not a tree: layers apply in order, later layers shadowing earlier ones. ChainID is a Merkle list, so a change in layer k invalidates every ChainID from k onward — O(n) in the layer count rather than git's O(depth) in the tree.
Dimension 4 — level-2 granularity
Whole archive. Partial fetch was retrofitted later by seekable layer formats, which are a separate subject precisely because they are not part of the base model.
Dimension 5 — digest
sha256:<hex> in the descriptor form, algorithm-tagged (so more agile than git, less than multicodec). The same layer has two different digests depending on whether you mean the stored blob or the uncompressed tar, which is a recurring source of bugs.
Dimension 6 — partial verification
None in the base model: a layer is verified after the whole blob arrives.
Strengths
- Enormous deployment, with a registry ecosystem that works.
ChainIDgives a principled identity for "the result of applying these layers", which a naive list of digests does not.- Algorithm-tagged digests.
- Layers as diffs make incremental distribution cheap even without a tree.
Weaknesses
- No canonical form. Identity depends on producer-specific tar encoding, mitigated by preserving the original headers rather than by specification.
- Timestamps and ownership inside the digest, so identical trees built twice differ.
O(n)chain invalidation instead ofO(depth)tree invalidation.- Three identities for one layer.
- No partial verification without a separate format.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Reuse tar rather than define a format | Universal tooling; layers are ordinary archives | No canonical serialization — the exact failure NAR exists to prevent |
| Layers as ordered diffs, with whiteouts | Incremental distribution without a tree | A chain, not a tree: O(n) invalidation and order-dependent semantics |
Separate DiffID from the stored blob digest | Compression must not change the identity of content | Two digests per layer, routinely confused |
ChainID as a Merkle list | Names the result of applying a prefix | Recomputed for every layer after a change |
Reproducibility as a SHOULD, with tar-split as the fallback | Cannot retrofit canonicity onto tar | Producers must store headers to reproduce their own digests |