pub trait BlobStore:
Send
+ Sync
+ Debug {
// Required methods
fn put<'life0, 'life1, 'async_trait>(
&'life0 self,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<Digest, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn put_at<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn get_raw<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn get<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
fn expire<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
at: Timestamp,
reason: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn has<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<bool, BlobError>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait;
// Provided method
fn tenant(&self) -> &str { ... }
}Expand description
Bytes addressed by their own hash.
§Two roles, and only one of them answers every method
Most of this trait is the contract every implementation carries: put,
get, expire, has, and the rule that an expired address stays expired.
put_at and get_raw are the envelope
pair, and they belong to a backing store — one something may seal onto.
A sealing decorator refuses both on purpose
(EncryptedBlobs does), because exposing
them through it is an unsealed side door: a caller reaching put_at on a
deployment that asked for sealing writes plaintext under a scope whose
erasure can never reach it. So a store that refuses the pair is not
incomplete, and a battery demanding it of everything would fail the one
implementation whose refusal is the guarantee — which is why
testkit::conformance_blob has two entry points rather than one.
Stated here rather than only at the decorator that refuses, because the reader who needs it is writing the next implementation.
Required Methods§
Sourcefn put<'life0, 'life1, 'async_trait>(
&'life0 self,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<Digest, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn put<'life0, 'life1, 'async_trait>(
&'life0 self,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<Digest, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Store bytes and return the address they landed at.
The returned digest MUST be Digest::of of exactly the bytes given.
That is a contract, not a description: callers that case-link before
writing — governed media,
store_blob — commit erasure
traversal to the digest they computed, so a store answering with any
other address would file the bytes outside that traversal, linked
under one digest and stored under another where no erasure that follows
the link can reach them. The media ingest treats a mismatch as a broken
contract and fails the effect rather than prefer the store’s answer.
Envelope encryption keeps the contract true by addressing ciphertext at
the plaintext digest through put_at.
Writing the same bytes twice is the same write.
An expired address stays expired. A store MUST refuse a write to an
address it holds a tombstone for, with
BlobError::Expired. Content addressing makes
this the one rule that is not obvious: the address is the bytes, so a
later write of the same bytes lands on the erased object and puts the
data back — silently, under a tombstone that still says when and why it
went. An erasure a subsequent write reverses is worse than one that
never ran, because the first was reported as discharged. It is reachable
through the ordinary API: a resumed run re-storing what it stored
before, or a second run of the same matter doing the same work.
A different erasure unit writing the same bytes is unaffected —
ScopedBlobs gives it another address, which is what that type is
for.
§Errors
BlobError::Expired if the address holds a tombstone, or
BlobError::Backend if the backing store rejects the write.
Sourcefn put_at<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn put_at<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
bytes: &'life1 [u8],
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Store bytes at an address that is not their own digest.
The one legitimate reason to separate the two: envelope encryption, where a payload is addressed by the digest of the plaintext and stored as ciphertext. Every digest already written to a journal keeps meaning what it meant, and the encryption stays invisible to everything that only ever held an address.
Callers other than EncryptedBlobs
almost certainly want put: an address that does not
describe its contents is a content-addressed store with its defining
property switched off, and get can no longer verify.
Carries put’s tombstone rule, and needs it more: this is
the write path a sealed deployment takes, so the refusal has to be here
or sealing is the configuration that loses it.
§Errors
BlobError::Expired if the address holds a tombstone, or
BlobError::Backend if the backing store rejects the write.
Sourcefn get_raw<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get_raw<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Fetch exactly what is stored, without verifying it against the address.
The counterpart to put_at: what is stored there is an
envelope, so it does not hash to the address and the ordinary check would
reject it. Verification does not disappear — it moves to after the
envelope is opened, where it is a claim about the payload rather than
about the envelope.
§Errors
BlobError::NotFound if nothing is stored there,
BlobError::Expired if a tombstone says the bytes were erased, and
BlobError::UnreadableTombstone if one is there and does not read.
Sourcefn get<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn get<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Fetch bytes, verifying them against the address before returning.
§Errors
BlobError::NotFound if nothing is stored there,
BlobError::Corrupt if what is stored does not hash to digest,
BlobError::Expired if a tombstone says the bytes were erased, and
BlobError::UnreadableTombstone if one is there and does not read.
Sourcefn expire<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
at: Timestamp,
reason: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn expire<'life0, 'life1, 'async_trait>(
&'life0 self,
digest: Digest,
at: Timestamp,
reason: &'life1 str,
) -> Pin<Box<dyn Future<Output = Result<(), BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Drop a blob’s bytes, leaving a tombstone that says it was deliberate.
This is the erasure half of retention, and it works because the chain only ever committed to a digest: the record still proves what happened and that it was not altered, while the bytes it described are gone. That is the property an Article 17 request needs and an Article 12 obligation must survive — they are only in tension if the payload lives in the chain, which is why it does not.
Expiring twice is the same expiry; the first tombstone stands, so a retry cannot rewrite when or why the data went.
§Errors
If the backing store rejects the write.
Sourcefn has<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<bool, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn has<'life0, 'async_trait>(
&'life0 self,
digest: Digest,
) -> Pin<Box<dyn Future<Output = Result<bool, BlobError>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Whether anything is stored at that address.
Does not verify: this answers a retention question, and a caller who needs to trust the bytes must read them.
§Errors
If the backing store cannot be reached.
Provided Methods§
Sourcefn tenant(&self) -> &str
fn tenant(&self) -> &str
Whose bytes this handle can reach.
Content addressing makes two tenants writing identical bytes land on one object, which is a feature within a tenant and a defect across them. The severe half is not reading — payloads are sealed under a per-tenant data key — it is erasure: tombstoning a shared object destroys the other tenant’s data while discharging one tenant’s request, and reports success for both.
Reported so a plane can refuse a blob store scoped to a different tenant than itself, the same way it refuses a mismatched journal.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".