ADR-0002 made a promise: Chum owns the integrity guarantee end-to-end. address == content is not a property the host asserts — it is a property the code checks on every read. SP5 is where the promise was redeemed.
The gap ADR-0002 left open
When ADR-0002 was ratified (chum/docs/decisions/0002-substrate-hosting.md, Accepted 2026-06-06), the consequence was explicit: “A non-self-verifying backend MUST re-hash on read to preserve address == content.” The GetByCID function in internal/object/service.go already had a comment flagging this — the scaffold fscas backend hashes while writing, so its Put path is trustworthy, but a future S3 backend returns bytes over an untrusted wire with no inherent integrity check. Re-hashing on read was required.
The question was where to put the re-hash. Baking it into each backend would mean duplicating the logic and the risk of missing it in a future adapter. SP5’s answer is a decorator: internal/substrate/verify.
The verify decorator
internal/substrate/verify/verify.go wraps any object.Substrate implementation. Its Get method returns a verifyingReader — a ReadCloser that tees every byte through cid.NewHasher() as the caller reads the stream. At EOF, it compares the computed CID against the requested ID. On mismatch, the final Read returns object.ErrCorrupt instead of io.EOF. The read fails closed.
The streaming design matters: the object is never buffered entirely in memory. For a 10 GB object the check still works, and the first byte is delivered to the caller while hashing proceeds concurrently with consumption. Put and Has delegate unchanged — Put already hashes while writing (that is how it computes the CID to return), and Has is an existence check with no integrity implication.
func New(inner object.Substrate) *Store { return &Store{inner: inner} }
That is the entire public API. One constructor, one wrapped seam. The verify package carries a compile-time assertion (var _ object.Substrate = (*Store)(nil)) so the interface can never drift silently.
This is the brand-critical piece. The claim “content-addressed object storage” is not verified by convention — it is verified by a verifyingReader on every Get call, regardless of which backend holds the bytes. Integrity is a composable layer, not a backend property.
The s3cas backend
internal/substrate/s3cas/s3cas.go is the S3-compatible production backend. The CID string is the object key. aws-sdk-go-v2 with a custom endpoint resolver makes the same code speak to AWS S3, Cloudflare R2, MinIO, and Backblaze B2 — the endpoint is just a URL, no SDK fork required.
Put stages the incoming reader to a temp file while hashing through cid.NewHasher() — the same pattern fscas uses. Staging yields a known content-length before the PutObject call. S3’s PutObject requires a content-length, and the length is only known after hashing, so staging is not an avoidance of streaming — it is a prerequisite for knowing the CID before keying the object. Has short-circuits a re-Put with a HeadObject call: CAS is idempotent, so a duplicate write with the same CID is safe, but avoiding the upload is cheaper.
Get calls GetObject and returns the body. Integrity enforcement is not duplicated here — it lives in the verify wrapper. A 404 maps to object.ErrNotFound.
Multipart streaming upload for very large objects (staging to disk is the v1 path) is a deferred refinement. The current Put reads the whole object to a temp file before uploading. For objects in the gigabyte range this trades memory for a local disk write, which is acceptable for v1.
The backend selector
cmd/chumd/main.go selects the inner backend via CHUM_SUBSTRATE=disk|s3, then wraps it:
sub := verify.New(inner)
One line is the entire integrity guarantee. Every path through the system — disk or S3, dev or production — goes through verify.New. The fscas backend is retained and hardened as the disk default, not retired: it is correct for single-binary self-hosting, and the verify decorator makes it as rigorous on read as s3cas.
S3 config (CHUM_S3_BUCKET, CHUM_S3_ENDPOINT, CHUM_S3_REGION) comes from environment variables, consistent with Chum’s existing env-only config style. Credentials resolve via the standard AWS chain — ambient IAM or explicit CHUM_S3_ACCESS_KEY_ID / CHUM_S3_SECRET_ACCESS_KEY.
Where verification sits relative to the cache
verify wraps Substrate, so service.readBytes only re-hashes on the substrate-miss path. A cache hit (cache/noop in the default build; cache/pellets in SP-9) returns bytes without re-hashing. This is correct: the cache is populated exclusively from already-verified substrate reads — cache.Put runs after the verified sub.Get — so a cached entry was verified when it was written. The boundary is stated explicitly in the SP5 design doc so the guarantee is unambiguous: the verification authority is the Substrate wrapper; the cache trusts what the verified substrate gave it.
What durability means here
“Durable” in SP5’s context is honest about what it covers and what it defers. The fscas backend writes to a CAS directory on local disk; replication and erasure-coding are not SP5’s scope (deferred per the design spec §6). The s3cas backend delegates durability to the object-storage provider — AWS S3’s eleven-nines durability, R2’s equivalent. SP5 does not implement its own replication.
What SP5 does implement is integrity on read, for every backend, unconditionally. That is the piece no prior implementation had: the guarantee that what you get out is byte-for-byte what went in, checked every time, not trusted from the host. The verify decorator is the single, reusable realization of that guarantee, and it is the piece SP-13 hardening will reuse without modification.
The substrate is now content-addressed in the full sense: the address is verified against the content on every read, across local disk and object storage, across dev and production, through a single composable decorator over an unchanged seam.