Skip to main content

BlobStore

Trait BlobStore 

Source
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§

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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§

Source

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".

Implementors§