Files
alkblobs/docs/architecture/decisions/002-canonical-hash-and-key-encoding.md
T
glm-5.3-flash abf8c5ae9c docs(architecture): ADR-010/011 — I/O seams, GC-state home, vocabulary; pre-decomposition consistency round
ADR-010 (new): the Backend trait's I/O seams pinned before any code —
'get' returns a crate-internal read cursor (kv engines materialize;
'local' pread loop; pg-lo lo_get windows), 'put' has two named forms
(whole-value kv / staged-put large); GC state is store-core-owned via
a crate-internal contract-tested engine-state seam on the SQL-backed
engines (backends never learn liveness — ADR-005 verbatim); the store
core holds the joint entry+pin transaction (corrects ADR-009 §2's
ownership statement); kv-tier fleet-validity rule added (mirror of
ADR-008 §3); ops pin-token TTL race resolved (the token IS the pin;
renewal rides have/re-put; pause-past-TTL falls to delete-then-recover);
feature graph pinned (pg-lo does not imply postgres).

ADR-011 (new): vocabulary pinned — tier (contract, exactly two) vs
engine (concrete impl, exactly five); store instance / node / fleet
split (fleet = pool-sharing, not node count — fixes requirements.md's
self-contradiction with REQ-2); mem demoted from 'backend'/'testing
tier' to the kv tier's third engine (contract-reference engine);
fs tier renamed 'large' including the feature name (last free moment
before code exists); constructor modes pinned (dual-tier default,
kv-only, mem-only) plus the composite fleet-validity predicate.

Consistency round across all specs and ADR-003/004/007/008/009: stale
pg-lo passages resolved per ADR-009's supersession; ADR-008 owner
'node id' → store-instance id; threshold wording corrected (conservative
edge, not midpoint, of the 128-256 KiB crossover zone); SweepReport
shape specced; mem classification unified; 008/009 ADR files renamed to
match the tier rename; README deferral-policy recap aligned (third
category = decided-but-sequenced work, not a parking kind); POC crate
list completed.

Verification: two independent architecture review rounds (the first
found 4 criticals — unpinned trait I/O shapes, missing fleet-state
seam, node/fleet self-contradiction, pin-token/TTL conflict — all
resolved; final round: zero criticals); all markdown links resolve;
ADR tables complete (11 ADRs).
2026-10-03 05:55:20 +00:00

5.5 KiB

ADR-002: Canonical hash and key encoding

Status

Accepted

Context

The store keys every entry under a content hash. Phase 0 (OQ-BL-03) resolved the hash policy — canonical git-blob-sha-256, tolerated git-blob-sha-1, BLAKE3 demoted — after the inherited premise "iroh uses BLAKE3, so we must" was rejected (convention 7: borrow conclusions, not welds; nothing in the consumer set forces BLAKE3: alkgit forces git's derivation by protocol definition, and every other consumer sees addresses, not algorithms).

The residual was the key encoding mechanics — the byte layout of keys inside backends. This is the crate's declared one-way door (AGENTS convention 5): key bytes persist in kv rows and local-engine filenames, with no schema-migration story; and this is the wire-format ADR that must exist before the first consumer (convention 7's gate on publishing wire-relevant decisions). POC #1 validated the mechanics empirically: byte-exact git hash-object interop (finding 4, both algorithms, external-oid round-trip into the pool), and tagged-key coexistence of both algorithms in one flat pool (finding 5).

Decision

Hash policy (three classes)

  1. Canonical — git-blob-sha-256: H("blob <len>\0" + content), SHA-256. The store's canonical algorithm; git objects are first-class entries with zero indirection; one algorithm domain = one address space, so non-git consumers dedup into the same pool by construction (ADR-005's premise).
  2. Tolerated legacy — git-blob-sha-1: same derivation, SHA-1 — accepted for existing-repo interop (a protocol necessity; git's own collision-hardened SHA-1 threat model inherited verbatim).
  3. Excluded — BLAKE3: not present in this crate. It was an iroh-blobs inheritance, not a requirement (its virtues — merkle-native trees, raw throughput — are irrelevant here under the canonical digest at our scales). If a chunk-tree transfer encoding is ever adopted it is a new decision at a new layer (ADR-006 excludes it from this crate's scope by scoping, not by hedge).

Security footnote (carried from OQ-BL-03): one algorithm across consumers is safe here — SHA-256 has no practical cross-protocol ambiguity at these input shapes; the preamble domain-separates the derivation. SHA-1's caveat is git's accepted position, inherited.

Key encoding (the one-way door's bytes)

Physical key layout inside all backends:

key := <algorithm byte> <digest bytes>
algorithm byte: 0x01 = git-blob-sha-256 (32-byte digest)
                0x02 = git-blob-sha-1   (20-byte digest)
  • Algorithm identity is inside the key. A per-store single-algorithm configuration would not have survived POC #1's external-oid interop test — external git oids and pooled workspace content must be addressable from the same table.
  • The tag byte is dropped (POC finding 5: exactly one key kind exists; the algorithm byte is the load-bearing part). Honesty note: the POC validated the tagged layout [tag][algorithm][digest]; dropping the single tag byte is the decided simplification of that layout (the finding's own reasoning — one key kind — is the evidence the simplification rests on; the algorithm byte, the load-bearing half, is the tested part).
  • Fixed length per algorithm (32 or 20 bytes); total key lengths 33/21. Fixed-length enum-tagged keys sort cleanly in total order — required for the sweep's live-set diffs (BTreeMap/range-scan walks).
  • Unknown-algorithm and truncated keys are rejects, not guesses (POC's length-checking test is the standard).
  • Backends are opaque to all of this: they store &[u8] keys; only the store core constructs/interprets typed keys (ADR-003).

Algorithm bytes are assigned from this ADR's registry; new algorithms require a new ADR (another one-way-door byte).

The preamble as abstraction

An algorithm = domain-separated derivation (its own input preprocessing) + digest. The preamble "blob <len>\0" is not a special case — POC #1's domain-separated-premise abstraction held with no special-casing anywhere. Note the structural consequence: the length is inside the hashed input, which is what forces the two put paths (ADR-003) and caps verification at whole-blob (ADR-006) — both accepted rather than worked around.

Consequences

Positive

  • Git oids (either format) address pool entries directly from external producers — no mapping layer, byte-exact (tested against the git CLI, not just vectors).
  • One address space across all consumers; dedup is structural.
  • Key bytes are self-describing; backend data survives digest-layer evolution (opaque at the boundary).

Negative

  • The encoding is frozen once a backend ships data (the door closes); algorithm byte 0x00 is reserved and never assigned (a future encoding revision would need a different key universe or a migration ADR).
  • SHA-1 acceptance inherits git's threat model — documented, not re-litigated here.

Neutral

  • Variable-length length-prefixed keys were considered (POC finding 5) and rejected for now: two algorithms make fixed-length simpler and sort-stable. Extension to a third algorithm re-opens byte layout — new ADR.

References

  • docs/research/phase-0.md OQ-BL-03 (resolution + encoding notes)
  • docs/research/poc-trait-dispatch-findings.md findings 4/5
  • hashing-and-keys.md
  • ADR-003 (opaque byte keys; two put paths the preamble forces); ADR-005 (one address space); ADR-006 (whole-blob verification)