Durability
DraftFinalization’s Completed is a one-time transfer acknowledgement, and verify_asset proves cryptographic validity, never that the server still holds the bytes. This page defines the re-checkable durability verdict, the signed custody receipt, and the rule every client follows before destroying local data — the transfer protocol’s invariants 6 and 7. Server: capsule-server::verify, capsule-server::attestation; client gate: capsule-library::library::ReleaseGate, driven by capsule-sdk::verify.
What “Safely Stored” Means
Section titled “What “Safely Stored” Means”durable is the conjunction, over every hash the client declared for the asset, of three key-free facts:
- indexed — a live asset row of the caller’s references the address;
- stored — the bytes are present at the content address in the blob store;
- retrievable — indexed ∧ stored ∧ nothing withholds it: not marked by GC, not quarantined, not under a moderation hold.
durable attests this home server’s storage only — not replicas, peers or backups.
Endpoint
Section titled “Endpoint”POST /v1/storage/verify takes a list of assets, each with the exact blob hashes the client relies on, and returns one verdict per asset with a per-blob breakdown and the server’s checked_at. It is a read, idempotent and cheap. The shape is in the OpenAPI contract.
- Fail closed. An unknown asset is not durable; a collaborator that cannot answer is an error, never an optimistic verdict. An empty declaration is
400, not a vacuousdurable: true. - Owner-scoped. Another account’s asset is indistinguishable from a non-existent one.
- No existence oracle.
indexedis checked first: a hash the asset does not reference reportsstored = false, indexed = falseeven if the store holds those bytes. Every declared hash appears in the response, in order. deepre-reads and re-hashes the declared blobs, reportingintact,corruptorrate_limitedper blob. It is rate-limited per account; a throttled request still returns the structural verdict.deepis absent when no scan ran, which a client MUST distinguish fromintact. Results are not coalesced: a cached integrity result would be a durability claim with an undisclosed age.
Custody Receipts
Section titled “Custody Receipts”A verdict says the bytes are serveable now. A custody receipt proves the server accepted them, so “the server lost my photo” and “the client never uploaded it” stop being symmetric claims. The type is capsule-core::crypto::receipts::CustodyReceipt, shared by client and server.
- Contents. Server identity and signing-key id, a per-server
receipt_seqandprior_receipt_hash, the upload and asset ids, the blob role, the server-recomputedciphertext_hashand size, the uploader, andreceived_atfrom the server clock.envelope_hash(SHA-256 over the signed manifest bytes) is present only on theprovenanceblob’s receipt; the manifest already commits to the other blobs. - Signed under the server attestation key, a long-lived hybrid Ed25519 + ML-DSA-65 key distinct from the operational key, with an append-only key history so old receipts verify forever.
- Issued with custody, in the same transaction as the index record (Upload — Finalization). There is no receipt without custody and no indexed blob without a receipt.
- Hash-chained and append-only.
receipt_seqandprior_receipt_hashform the server’s receipt log; no API path overwrites or deletes an entry. Position allocation and signing happen in one operation, so the chain cannot fork. - Fetched via
GET /v1/upload/{id}/receiptwithin the session window andGET /v1/assets/{asset_id}/receiptsdurably. The durable fetch is owner-only; a non-owner gets the unknown-asset404. - Verified and kept by the client. The client checks the signature under the pinned key, that
ciphertext_hash,sizeandenvelope_hashmatch what it sent, and a gross clock-drift bound onreceived_at. It stores receipts beside the provenance chain and in the backup artifact; the client copy is evidence, not cache.
Signed Storage Attestation
Section titled “Signed Storage Attestation”The verdict MAY be requested in a signed form carrying a client nonce and signed under the attestation key, so an honest server’s self-reported loss and periodic audits are retainable evidence. The type is StorageAttestation in capsule-core::crypto::storage_attestation, shared by client and server.
- Requested by sending
attestation_nonce— 32 bytes the client draws fresh per request, as lowercase hex — onPOST /v1/storage/verify. The response then also carriesattestation, the canonical-CBOR attestation in base64. - Contents. Server identity and signing-key id, the account the verdicts were computed for, the client’s nonce, the server’s
checked_at, and the same per-asset, per-blob verdicts the unsigned response carries,deepincluded when a scan ran. Thestorage-attestation/v1version and field set keep it from being mistaken for a custody receipt, which the same key signs. - Priced like
deep. Signing spends the account’s deep-verify budget, charged once per request that asks for either. A throttled signed request is refused whole with429andRetry-After, never downgraded to an unsigned answer. A client that asked for evidence and got none would not know it. - Verified by the client. The signature verifies under the pinned attestation key, the nonce is the one the client sent, and the verdicts answer exactly the assets and declared hashes asked about, in order. A replayed or tampered attestation fails one of these checks. The client acts on the verdicts read from the signed core, and keeps the bytes as evidence.
- Kept by the client. Only an attestation that verified is kept, and it is kept exactly as received: appended to the log of every asset it answers for, beside that asset’s custody receipts, and carried in the backup artifact. The log is append-only and idempotent on the nonce, so a retried request never duplicates an entry and a later attestation reporting a blob lost never displaces the earlier one that reported it held. Like receipts, the client copy is evidence, not cache: a library rebuild never reads or rewrites it, and a restore returns it byte for byte. The reference client asks for one with
capsule verify --signed, which declares each asset’s original and metadata blob.
Proof of Loss
Section titled “Proof of Loss”A client proves loss with: (1) a receipt for hash H; (2) later non-holding — a signed attestation reporting H not stored, or a failed content-addressed fetch, which is self-verifying; and (3) the absence of an authorized purge. The server’s rebuttal is a device-signed delete with an elapsed retention window. Conversely, a custody claim with no valid receipt is unbacked. A receipt from another server fails on its server binding. Accepted v1 limitation: a server can show different log views to different auditors; gossiped signed log heads are the planned extension.
Verify Before Destroy
Section titled “Verify Before Destroy”Before any post-write local cleanup that drops irreplaceable bytes, a client MUST hold all three:
- a verified custody receipt for the write;
durable = truefrom/storage/verifyfor every declared blob;verify_assetacceptance.
A transport failure on any check retains the bytes. A non-durable verdict never triggers a destructive action: the client retains the copy, retries with backoff, and shows “not yet confirmed on server”. A server that withholds receipts therefore never becomes the sole holder of an only copy.
The gate applies to:
- releasing or evicting a device-owned original (Space Recovery);
- Move-mode source deletion after import. The local commit never deletes a source; the source is deleted only after both it and the library copy read back as the signed original, and the gate passes. With no server to ask, every source is kept;
- streaming import release (Pipeline);
- discarding local working state tied to a write (staged temporaries, the pre-edit copy across a
replace). - releasing an original handed to a delegate: the receipt is fetched from the server by the origin and checked against the hash the origin computed, never relayed by the delegate.
It does not apply to intentional deletes (governed by the retention window), to rebuildable derivative cache, or to re-fetchable server-origin blobs. The SQLite catalog is not rebuildable cache: it migrates in place and evicting it is a repair action.
The verdict is point-in-time: the client re-verifies if a bounded interval passes before release, and on the server a blob that just answered durable cannot be deleted faster than the GC grace window. Together these make the gate sound without a lease.