syntheca — Specification
version 1.0 RC1
This document specifies the syntheca protocol: a content-addressable
storage layer over an apotheca cella.
syntheca derives the apotheca name from a hash of the bytes for the
depositum namespace; apotheca’s Ok on an equal-digest re-deposit is
the benign dedup hit, while apotheca’s Collision outcome
(different bytes under one name) surfaces as the hard
HashCollision error (§2.1,
§3.3). The underlying cella’s pinax
(compare-and-swap) namespace is surfaced unchanged as a transparent
pass-through.
The specification is language-agnostic. The reference implementation
is in Rust (syntheca-rs/).
For motivation and the relationship to apotheca, see
README.md. For everything substrate-level —
cella model, backend protocol, atomicity, on-disk layout, name
validation, pinax mechanics — see
../../apotheca/spec/SPEC.md.
This document covers only what is content-addressing-specific plus the
pass-through surface.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.
1 Scope and terminology
A syntheca cella is an apotheca cella (apotheca SPEC §1.1) augmented with the contract that a depositum’s name is derived from its bytes by a fixed hash function (§3). The cella’s pinax namespace (apotheca SPEC §1.6) is unconstrained by syntheca: pinax names are caller-chosen.
Terminology not redefined here is inherited from apotheca SPEC §1.
1.1 Hash
A hash in this specification is a BLAKE3 digest of length 32 octets. BLAKE3 is defined by the BLAKE3 specification. The hash function is fixed by this specification; changing it is a protocol revision.
1.2 Name encoding
The apotheca depositum name corresponding to a hash is the 64-octet
ASCII string of 64 lowercase hexadecimal digits encoding the 32
octets of the hash, most-significant byte first. This string is a
valid apotheca name (apotheca SPEC §4.1): non-empty, contains no
/ or NUL, length within [1, 255].
Two hashes are equal iff their 32-octet sequences are equal. Implementations MUST NOT accept uppercase or mixed-case hex as a hash input on the wire (CLI, library); equality is over the underlying octets and the canonical encoding is lowercase.
This encoding constrains only the depositum namespace. Pinax names are caller-chosen apotheca names with no syntheca-imposed structure.
2 Operations
A syntheca cella exposes five operations: deposit, get,
and stat on the depositum namespace; get_pinax and
set_pinax on the pinax namespace. The depositum operations
carry CAS semantics specific to syntheca; the pinax operations are a
transparent pass-through to the underlying apotheca cella (apotheca
SPEC §2.4–§2.5).
2.1 deposit(bytes)
Stores bytes and returns their hash.
Compute
h = blake3(bytes).Call
apotheca.deposit(name=hex(h), bytes).If apotheca returns
Ok, returnh.If apotheca returns
Collision, returnHashCollision. With a collision-resistant hash this case does not occur from honest inputs; it indicates either backend corruption or a successful adversarial collision against BLAKE3.
The Collision path exists because apotheca’s deposit
discriminates by its own digest (SHA-256, apotheca SPEC §2.1), not by
BLAKE3. If the existing depositum’s bytes differ from bytes,
their SHA-256 digests differ, and apotheca returns Collision
even though syntheca submitted the same name. syntheca surfaces this
case as HashCollision rather than masking it.
2.2 get(hash)
Returns the bytes stored under hash.
Call
apotheca.get(name=hex(hash)). apotheca verifies the bytes against its stored SHA-256 (apotheca SPEC §2.2); a mismatch surfaces asIntegrityError.If
verify_on_readis enabled (§4), computeblake3(bytes)and compare tohash. If they differ, returnIntegrityError.Return the bytes.
Outcomes: Bytes, NotFound, IntegrityError.
The bytes are equal, octet-for-octet, to the bytes most recently
passed to a deposit call that returned Ok for
hash.
2.3 stat(hash)
Returns metadata for hash.
Call
apotheca.stat(name=hex(hash)).Return
{ size, sha256 }from apotheca’s response. The BLAKE3 hash itself is the input and is not re-reported.
Outcomes: Metadata, NotFound.
stat MUST NOT re-hash the bytes.
2.4 get_pinax(name)
Pass-through to apotheca.get_pinax(name). name is an
apotheca name chosen by the caller (apotheca SPEC §4); syntheca
imposes no content-addressing constraint on it.
Outcomes are exactly those of apotheca’s get_pinax (apotheca
SPEC §2.4): Bytes, NotFound,
IntegrityError. syntheca adds no additional verification
beyond apotheca’s mandatory pinax integrity check (apotheca SPEC
§3.5); verify_on_read (§4) does not
apply to pinakes.
2.5 set_pinax(name, bytes, expected)
Pass-through to apotheca.set_pinax(name, bytes, expected).
expected is Option<Digest> carrying the SHA-256 the
caller believes is currently stored, or None to require
absence.
Outcomes are exactly those of apotheca’s set_pinax (apotheca
SPEC §2.5): Ok,
Conflict { actual: Option<Digest> }.
3 Integrity
3.1 Two-hash regime (depositum)
A syntheca depositum has two associated digests: the BLAKE3 hash that names it (§1.1), and the SHA-256 digest stored by apotheca (apotheca SPEC §3.1). Both are mandatory and serve different purposes:
BLAKE3 identifies the depositum within syntheca and is the unit of content-addressed equality.
SHA-256 is apotheca’s storage-integrity digest and ETag (apotheca SPEC §3.3).
Implementations MUST NOT replace one with the other and MUST NOT change apotheca’s hash function to BLAKE3 to deduplicate the two.
3.2 Verification (depositum)
get always benefits from apotheca’s mandatory SHA-256
verification. get additionally verifies BLAKE3 if
verify_on_read is enabled (§4).
deposit detects BLAKE3-name collisions against differing bytes
through apotheca’s SHA-256-based collision check.
3.3 BLAKE3 collisions
A HashCollision outcome from deposit
(§2.1) indicates that two distinct byte sequences
produced the same BLAKE3 digest. With a collision-resistant hash this
does not occur from honest inputs; implementations MUST surface
this as a hard error, MUST NOT overwrite the existing depositum,
and SHOULD log the event.
3.4 Pinax integrity
Pinax integrity is governed entirely by apotheca SPEC §3.5. syntheca adds no further hash; the pinax namespace is single-hash (SHA-256 only) because there is no content-addressed naming contract to verify against.
4 Verify-on-read
Implementations MUST support a verify_on_read mode that,
when enabled, recomputes blake3(bytes) on every depositum
get and returns IntegrityError if the result does not
equal the requested hash.
The default is verify_on_read = true. Implementations MAY
allow disabling it per cella for read-mostly hot paths where
apotheca’s SHA-256 verification is considered sufficient.
verify_on_read is in addition to apotheca’s SHA-256
verification; disabling it does not disable apotheca’s check. It
applies only to the depositum namespace; pinax reads are governed by
apotheca’s mandatory pinax integrity check
(§3.4).
5 CLI surface
The reference CLI binary is syn. It exposes the operations
one-for-one against the default local cella unless configured
otherwise. The default cella root is ~/.syntheca/. The root
MAY be overridden per invocation; the reference syn binary
accepts --cella <PATH> for this.
The syntheca library itself fixes no default — Cella::open
takes the root as a parameter — so consumers above syntheca
instantiate their own cella at their own root rather than sharing
~/.syntheca/.
5.1 syn deposit
syn deposit <path>
syn deposit -
<path>reads bytes from the named file.-reads bytes from standard input.Computes
blake3(bytes), performsdeposit, and writes the resulting hash as 64 lowercase hex digits followed by a newline to standard output.Exit status
0onOk(including idempotent re-deposit). Exit status non-zero onHashCollisionor any I/O error, with a diagnostic to standard error.
5.2 syn get
syn get <hash>
<hash>is 64 lowercase hex digits.Writes the bytes to standard output. Performs verification per §2.2.
Exit status
0on success. Exit status non-zero onNotFound,IntegrityError, malformed hash, or any I/O error.
5.3 syn stat
syn stat <hash>
Writes a human-readable summary to standard output:
size <decimal> sha256 <hex>The BLAKE3 hash is the input; it is not re-reported.
Exit status
0on success. Exit status non-zero onNotFound, malformed hash, or any I/O error.
5.4 syn pinax get
syn pinax get <name>
Writes the pinax bytes for
<name>to standard output. Performs verification per §2.4.Exit status
0on success. Exit status non-zero onNotFound,IntegrityError, invalid name, or any I/O error.
5.5 syn pinax set
syn pinax set --name <name> (--expect-absent | --expect <hex>) <path>
syn pinax set --name <name> (--expect-absent | --expect <hex>) -
<path>reads bytes from the named file;-reads from standard input.Exactly one of
--expect-absentand--expect <hex>MUST be given.--expect-absentcorresponds toexpected = None;--expect <hex>corresponds toexpected = Some(<hex>)where<hex>is 64 lowercase hexadecimal digits.Exit status
0onOk(including idempotent re-set). OnConflict, exit status non-zero with a diagnostic of the formconflict: actual=<hex>orconflict: actual=absentto standard error. Other I/O errors exit non-zero with a diagnostic.
The pinax CLI is a verbatim mirror of apo pinax {get,set}
(apotheca SPEC §7.4–§7.5); it exists on syn so callers do not
need to juggle two binaries against the same cella.
6 Errors
The error conditions defined by this specification are:
Errors inherited from apotheca (apotheca SPEC §8) propagate unchanged unless this document maps them otherwise. A malformed hash on input (non-hex, wrong length) is an implementation-defined input error, distinguishable from the protocol-defined errors above; an invalid pinax name is likewise an implementation-defined input error (apotheca SPEC §4).
7 Out of scope
Inherited from apotheca’s out-of-scope list. Additionally:
Hash function choice. Fixed at BLAKE3 by §1.1.
Enumeration / listing. apotheca exclusion, transitive.
State chains, manifests, history. Project concerns above syntheca; see
README.md"Out of scope (deliberately)". Note that pinakes are not on this list as of v0.2: state-chain heads and similar mutable pointers are stored in pinakes, but the state-chain semantics on top of those pinakes remain a project concern.
8 Future work (non-normative)
Per-cella hash function selection, if a second hash is ever needed.
A streaming
depositthat hashes incrementally rather than buffering.Read-side
verify_on_readpolicy in configuration once apotheca grows a configuration mechanism.