Skip to main content

reth_transaction_pool/blobstore/
mod.rs

1//! Storage for blob data of EIP4844 transactions.
2
3use alloy_eips::{
4    eip4844::{BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1},
5    eip7594::{BlobCellMask, BlobTransactionSidecarVariant, Cell},
6};
7use alloy_primitives::{TxHash, B128, B256};
8pub use converter::BlobSidecarConverter;
9pub use disk::{DiskFileBlobStore, DiskFileBlobStoreConfig, OpenDiskFileBlobStore};
10pub use mem::InMemoryBlobStore;
11pub use noop::NoopBlobStore;
12use std::{
13    fmt,
14    ops::Deref,
15    sync::{
16        atomic::{AtomicU64, AtomicUsize, Ordering},
17        Arc,
18    },
19};
20pub use tracker::{BlobStoreCanonTracker, BlobStoreUpdates};
21
22mod converter;
23pub mod disk;
24mod mem;
25mod noop;
26mod tracker;
27
28/// Blob cell availability stored for a transaction.
29///
30/// Bit `i` corresponds to cell index `i`. The two words are stored least-significant first: index
31/// `0` contains cells `0..64` and index `1` contains cells `64..128`.
32#[derive(Debug, Clone)]
33pub struct BlobCellAvailability(Arc<[AtomicU64; 2]>);
34
35impl BlobCellAvailability {
36    const LOW_WORD: usize = 0;
37    const HIGH_WORD: usize = 1;
38
39    /// Returns full availability for all blob cells.
40    pub fn full() -> Self {
41        Self(Arc::new([AtomicU64::new(u64::MAX), AtomicU64::new(u64::MAX)]))
42    }
43
44    /// Returns a snapshot of the available cells.
45    ///
46    /// The two words are loaded independently. Future writers must only add availability bits so
47    /// that a concurrent snapshot can understate availability but never overstate it.
48    pub fn get(&self) -> BlobCellMask {
49        let low = self.0[Self::LOW_WORD].load(Ordering::Relaxed) as u128;
50        let high = self.0[Self::HIGH_WORD].load(Ordering::Relaxed) as u128;
51        BlobCellMask::from_bits((high << 64) | low)
52    }
53
54    /// Returns true if all blob cells are available.
55    pub fn is_full(&self) -> bool {
56        self.get().bits() == u128::MAX
57    }
58}
59
60impl PartialEq for BlobCellAvailability {
61    fn eq(&self, other: &Self) -> bool {
62        self.get() == other.get()
63    }
64}
65
66impl Eq for BlobCellAvailability {}
67
68/// A blob sidecar paired with its shared cell availability.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct PooledBlobSidecar {
71    sidecar: BlobTransactionSidecarVariant,
72    availability: BlobCellAvailability,
73}
74
75impl PooledBlobSidecar {
76    /// Creates a sidecar with the given shared cell availability.
77    pub const fn new(
78        sidecar: BlobTransactionSidecarVariant,
79        availability: BlobCellAvailability,
80    ) -> Self {
81        Self { sidecar, availability }
82    }
83
84    /// Returns the wrapped sidecar.
85    pub const fn sidecar(&self) -> &BlobTransactionSidecarVariant {
86        &self.sidecar
87    }
88
89    /// Returns whether this is an EIP-7594 sidecar.
90    pub const fn is_eip7594(&self) -> bool {
91        self.sidecar.is_eip7594()
92    }
93
94    /// Returns the shared cell availability.
95    pub const fn availability(&self) -> &BlobCellAvailability {
96        &self.availability
97    }
98
99    /// Consumes the wrapper and returns the sidecar.
100    pub fn into_sidecar(self) -> BlobTransactionSidecarVariant {
101        self.sidecar
102    }
103}
104
105impl Deref for PooledBlobSidecar {
106    type Target = BlobTransactionSidecarVariant;
107
108    fn deref(&self) -> &Self::Target {
109        &self.sidecar
110    }
111}
112
113impl From<BlobTransactionSidecarVariant> for PooledBlobSidecar {
114    fn from(sidecar: BlobTransactionSidecarVariant) -> Self {
115        // TODO: Initialize this with the actual mask once sparse sidecars are supported.
116        Self::new(sidecar, BlobCellAvailability::full())
117    }
118}
119
120/// A blob store that can be used to store blob data of EIP4844 transactions.
121///
122/// This type is responsible for keeping track of blob data until it is no longer needed (after
123/// finalization).
124///
125/// Note: this is Clone because it is expected to be wrapped in an Arc.
126pub trait BlobStore: fmt::Debug + Send + Sync + 'static {
127    /// Inserts the blob sidecar into the store
128    fn insert(&self, tx: B256, data: PooledBlobSidecar) -> Result<(), BlobStoreError>;
129
130    /// Inserts multiple blob sidecars into the store
131    fn insert_all(&self, txs: Vec<(B256, PooledBlobSidecar)>) -> Result<(), BlobStoreError>;
132
133    /// Deletes the blob sidecar from the store
134    fn delete(&self, tx: B256) -> Result<(), BlobStoreError>;
135
136    /// Deletes multiple blob sidecars from the store
137    fn delete_all(&self, txs: Vec<B256>) -> Result<(), BlobStoreError>;
138
139    /// A maintenance function that can be called periodically to clean up the blob store, returns
140    /// the number of successfully deleted blobs and the number of failed deletions.
141    ///
142    /// This is intended to be called in the background to clean up any old or unused data, in case
143    /// the store uses deferred cleanup: [`DiskFileBlobStore`]
144    fn cleanup(&self) -> BlobStoreCleanupStat;
145
146    /// Retrieves the decoded blob data for the given transaction hash.
147    fn get(&self, tx: B256) -> Result<Option<Arc<BlobTransactionSidecarVariant>>, BlobStoreError>;
148
149    /// Checks if the given transaction hash is in the blob store.
150    fn contains(&self, tx: B256) -> Result<bool, BlobStoreError>;
151
152    /// Retrieves all decoded blob data for the given transaction hashes.
153    ///
154    /// This only returns the blobs that were found in the store.
155    /// If there's no blob it will not be returned.
156    ///
157    /// Note: this is not guaranteed to return the blobs in the same order as the input.
158    fn get_all(
159        &self,
160        txs: Vec<B256>,
161    ) -> Result<Vec<(B256, Arc<BlobTransactionSidecarVariant>)>, BlobStoreError>;
162
163    /// Returns the exact [`BlobTransactionSidecarVariant`] for the given transaction hashes in the
164    /// exact order they were requested.
165    ///
166    /// Returns an error if any of the blobs are not found in the blob store.
167    fn get_exact(
168        &self,
169        txs: Vec<B256>,
170    ) -> Result<Vec<Arc<BlobTransactionSidecarVariant>>, BlobStoreError>;
171
172    /// Return the [`BlobAndProofV1`]s for a list of blob versioned hashes.
173    fn get_by_versioned_hashes_v1(
174        &self,
175        versioned_hashes: &[B256],
176    ) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError>;
177
178    /// Return the [`BlobAndProofV2`]s for a list of blob versioned hashes.
179    /// Blobs and proofs are returned only if they are present for _all_ requested
180    /// versioned hashes.
181    ///
182    /// This differs from [`BlobStore::get_by_versioned_hashes_v1`] in that it also returns all the
183    /// cell proofs in [`BlobAndProofV2`] supported by the EIP-7594 blob sidecar variant.
184    ///
185    /// The response also differs from [`BlobStore::get_by_versioned_hashes_v1`] in that this
186    /// returns `None` if any of the requested versioned hashes are not present in the blob store:
187    /// e.g. where v1 would return `[A, None, C]` v2 would return `None`. See also <https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#engine_getblobsv2>
188    fn get_by_versioned_hashes_v2(
189        &self,
190        versioned_hashes: &[B256],
191    ) -> Result<Option<Vec<BlobAndProofV2>>, BlobStoreError>;
192
193    /// Return the [`BlobAndProofV2`]s for a list of blob versioned hashes.
194    ///
195    /// The response is always the same length as the request. Missing or older-version blobs are
196    /// returned as `None` elements.
197    fn get_by_versioned_hashes_v3(
198        &self,
199        versioned_hashes: &[B256],
200    ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError>;
201
202    /// Return the [`BlobCellsAndProofsV1`]s for a list of blob versioned hashes and requested cell
203    /// indices.
204    ///
205    /// The response is always the same length as the request. Missing or older-version blobs are
206    /// returned as `None` elements.
207    fn get_by_versioned_hashes_v4(
208        &self,
209        versioned_hashes: &[B256],
210        indices_bitarray: B128,
211    ) -> Result<Vec<Option<BlobCellsAndProofsV1>>, BlobStoreError>;
212
213    /// Return whether each requested blob versioned hash is available.
214    ///
215    /// The response is always the same length and order as the request.
216    fn has_versioned_hashes(&self, versioned_hashes: &[B256]) -> Result<Vec<bool>, BlobStoreError>;
217
218    /// Returns all requested cells for all blobs belonging to the transaction.
219    ///
220    /// The `indices_bitarray` is applied independently to every blob in the tx.
221    ///
222    /// Returned cells are flattened in blob order, then cell-index order.
223    ///
224    /// Example:
225    /// If the tx contains blobs `[blob0, blob1]` and the requested indices are
226    /// `[2, 5, 9]`, the returned vector is:
227    ///
228    /// ```text
229    /// [
230    ///   blob0_cell2,
231    ///   blob0_cell5,
232    ///   blob0_cell9,
233    ///   blob1_cell2,
234    ///   blob1_cell5,
235    ///   blob1_cell9,
236    /// ]
237    /// ```
238    fn get_cells(
239        &self,
240        tx_hash: TxHash,
241        indices_bitarray: B128,
242    ) -> Result<Option<Vec<Cell>>, BlobStoreError>;
243
244    /// Data size of all transactions in the blob store.
245    fn data_size_hint(&self) -> Option<usize>;
246
247    /// How many blobs are in the blob store.
248    fn blobs_len(&self) -> usize;
249}
250
251/// Error variants that can occur when interacting with a blob store.
252#[derive(Debug, thiserror::Error)]
253pub enum BlobStoreError {
254    /// Thrown if the blob sidecar is not found for a given transaction hash but was required.
255    #[error("blob sidecar not found for transaction {0:?}")]
256    MissingSidecar(B256),
257    /// Failed to decode the stored blob data.
258    #[error("failed to decode blob data: {0}")]
259    DecodeError(#[from] alloy_rlp::Error),
260    /// Other implementation specific error.
261    #[error(transparent)]
262    Other(Box<dyn core::error::Error + Send + Sync>),
263}
264
265/// Keeps track of the size of the blob store.
266#[derive(Debug, Default)]
267pub(crate) struct BlobStoreSize {
268    data_size: AtomicUsize,
269    num_blobs: AtomicUsize,
270}
271
272impl BlobStoreSize {
273    #[inline]
274    pub(crate) fn add_size(&self, add: usize) {
275        self.data_size.fetch_add(add, Ordering::Relaxed);
276    }
277
278    #[inline]
279    pub(crate) fn sub_size(&self, sub: usize) {
280        let _ = self.data_size.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
281            Some(current.saturating_sub(sub))
282        });
283    }
284
285    #[inline]
286    pub(crate) fn update_len(&self, len: usize) {
287        self.num_blobs.store(len, Ordering::Relaxed);
288    }
289
290    #[inline]
291    pub(crate) fn inc_len(&self, add: usize) {
292        self.num_blobs.fetch_add(add, Ordering::Relaxed);
293    }
294
295    #[inline]
296    pub(crate) fn sub_len(&self, sub: usize) {
297        let _ = self.num_blobs.try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
298            Some(current.saturating_sub(sub))
299        });
300    }
301
302    #[inline]
303    pub(crate) fn data_size(&self) -> usize {
304        self.data_size.load(Ordering::Relaxed)
305    }
306
307    #[inline]
308    pub(crate) fn blobs_len(&self) -> usize {
309        self.num_blobs.load(Ordering::Relaxed)
310    }
311}
312
313impl PartialEq for BlobStoreSize {
314    fn eq(&self, other: &Self) -> bool {
315        self.data_size.load(Ordering::Relaxed) == other.data_size.load(Ordering::Relaxed) &&
316            self.num_blobs.load(Ordering::Relaxed) == other.num_blobs.load(Ordering::Relaxed)
317    }
318}
319
320/// Statistics for the cleanup operation.
321#[derive(Debug, Clone, Default, PartialEq, Eq)]
322pub struct BlobStoreCleanupStat {
323    /// the number of successfully deleted blobs
324    pub delete_succeed: usize,
325    /// the number of failed deletions
326    pub delete_failed: usize,
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use alloy_eips::{eip4844::BlobTransactionSidecar, eip7594::BlobTransactionSidecarEip7594};
333
334    #[expect(dead_code)]
335    struct DynStore {
336        store: Box<dyn BlobStore>,
337    }
338
339    #[test]
340    fn pooled_blob_sidecar_defaults_to_full_availability() {
341        let sidecars = [
342            BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar::default()),
343            BlobTransactionSidecarVariant::Eip7594(BlobTransactionSidecarEip7594::default()),
344        ];
345
346        for sidecar in sidecars {
347            assert!(PooledBlobSidecar::from(sidecar).availability().is_full());
348        }
349    }
350
351    #[test]
352    fn blob_cell_availability_uses_cell_index_bit_order() {
353        let availability =
354            BlobCellAvailability(Arc::new([AtomicU64::new(1), AtomicU64::new(1 << 1)]));
355
356        let mask = availability.get();
357        assert!(mask.contains(0));
358        assert!(mask.contains(65));
359        assert_eq!(mask.count(), 2);
360    }
361}