Skip to main content

reth_transaction_pool/blobstore/
disk.rs

1//! A simple diskstore for blobs
2
3use crate::blobstore::{BlobStore, BlobStoreCleanupStat, BlobStoreError, BlobStoreSize};
4use alloy_eips::{
5    eip4844::{BlobAndProofV1, BlobAndProofV2, BlobCellsAndProofsV1},
6    eip7594::{BlobCellMask, BlobTransactionSidecarVariant, Cell},
7    eip7840::BlobParams,
8    merge::EPOCH_SLOTS,
9};
10use alloy_primitives::{map::B256Set, TxHash, B128, B256};
11use parking_lot::{Mutex, RwLock};
12use schnellru::{ByLength, LruMap};
13use std::{fmt, fs, io, path::PathBuf, sync::Arc};
14use tracing::{debug, trace};
15
16/// How many [`BlobTransactionSidecarVariant`] to cache in memory.
17pub const DEFAULT_MAX_CACHED_BLOBS: u32 = 100;
18
19/// A cache size heuristic based on the highest blob params
20///
21/// This uses the max blobs per tx and max blobs per block over 16 epochs: `21 * 6 * 512 = 64512`
22/// This should be ~4MB
23const VERSIONED_HASH_TO_TX_HASH_CACHE_SIZE: u64 =
24    BlobParams::bpo2().max_blobs_per_tx * BlobParams::bpo2().max_blob_count * EPOCH_SLOTS * 16;
25
26/// A blob store that stores blob data on disk.
27///
28/// The type uses deferred deletion, meaning that blobs are not immediately deleted from disk, but
29/// it's expected that the maintenance task will call [`BlobStore::cleanup`] to remove the deleted
30/// blobs from disk.
31#[derive(Clone, Debug)]
32pub struct DiskFileBlobStore {
33    inner: Arc<DiskFileBlobStoreInner>,
34}
35
36impl DiskFileBlobStore {
37    /// Opens and initializes a new disk file blob store according to the given options.
38    pub fn open(
39        blob_dir: impl Into<PathBuf>,
40        opts: DiskFileBlobStoreConfig,
41    ) -> Result<Self, DiskFileBlobStoreError> {
42        let blob_dir = blob_dir.into();
43        let DiskFileBlobStoreConfig { max_cached_entries, .. } = opts;
44        let inner = DiskFileBlobStoreInner::new(blob_dir, max_cached_entries);
45
46        // initialize the blob store
47        inner.delete_all()?;
48        inner.create_blob_dir()?;
49
50        Ok(Self { inner: Arc::new(inner) })
51    }
52
53    #[cfg(test)]
54    fn is_cached(&self, tx: &B256) -> bool {
55        self.inner.blob_cache.lock().get(tx).is_some()
56    }
57
58    #[cfg(test)]
59    fn clear_cache(&self) {
60        self.inner.blob_cache.lock().clear()
61    }
62
63    /// Look up EIP-7594 blobs by their versioned hashes.
64    ///
65    /// This returns a result vector with the **same length and order** as the input
66    /// `versioned_hashes`. Each element is `Some(BlobAndProofV2)` if the blob is available, or
67    /// `None` if it is missing or an older sidecar version.
68    ///
69    /// The lookup first scans the in-memory cache and, if not all blobs are found, falls back to
70    /// reading candidate sidecars from disk using the `versioned_hash -> tx_hash` index.
71    fn get_by_versioned_hashes_eip7594(
72        &self,
73        versioned_hashes: &[B256],
74    ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
75        // we must return the blobs in order but we don't necessarily find them in the requested
76        // order
77        let mut result = vec![None; versioned_hashes.len()];
78        let mut missing_count = result.len();
79        // first scan all cached full sidecars
80        for (_tx_hash, blob_sidecar) in self.inner.blob_cache.lock().iter() {
81            if let Some(blob_sidecar) = blob_sidecar.as_eip7594() {
82                for (hash_idx, match_result) in
83                    blob_sidecar.match_versioned_hashes(versioned_hashes)
84                {
85                    let slot = &mut result[hash_idx];
86                    if slot.is_none() {
87                        missing_count -= 1;
88                    }
89                    *slot = Some(match_result);
90                }
91            }
92
93            // return early if all blobs are found.
94            if missing_count == 0 {
95                // since versioned_hashes may have duplicates, we double check here
96                if result.iter().all(|blob| blob.is_some()) {
97                    return Ok(result);
98                }
99            }
100        }
101
102        // not all versioned hashes were found, try to look up a matching tx
103        let mut missing_tx_hashes = Vec::new();
104        let mut seen_missing_tx_hashes = B256Set::default();
105
106        {
107            let mut versioned_to_txhashes = self.inner.versioned_hashes_to_txhash.lock();
108            for (idx, _) in
109                result.iter().enumerate().filter(|(_, blob_and_proof)| blob_and_proof.is_none())
110            {
111                // this is safe because the result vec has the same len
112                let versioned_hash = versioned_hashes[idx];
113                if let Some(tx_hash) = versioned_to_txhashes.get(&versioned_hash).copied() &&
114                    seen_missing_tx_hashes.insert(tx_hash)
115                {
116                    missing_tx_hashes.push(tx_hash);
117                }
118            }
119        }
120
121        // if we have missing blobs, try to read them from disk and try again
122        if !missing_tx_hashes.is_empty() {
123            let blobs_from_disk = self.inner.read_many_decoded(missing_tx_hashes);
124            for (_, blob_sidecar) in blobs_from_disk {
125                if let Some(blob_sidecar) = blob_sidecar.as_eip7594() {
126                    for (hash_idx, match_result) in
127                        blob_sidecar.match_versioned_hashes(versioned_hashes)
128                    {
129                        if result[hash_idx].is_none() {
130                            result[hash_idx] = Some(match_result);
131                        }
132                    }
133                }
134            }
135        }
136
137        Ok(result)
138    }
139
140    /// Look up EIP-7594 blob cells by their versioned hashes.
141    fn get_by_versioned_hashes_cells_eip7594(
142        &self,
143        versioned_hashes: &[B256],
144        indices_bitarray: B128,
145    ) -> Result<Vec<Option<BlobCellsAndProofsV1>>, BlobStoreError> {
146        let cell_mask = BlobCellMask::new(indices_bitarray);
147        let mut result = vec![None; versioned_hashes.len()];
148        let mut missing_count = result.len();
149
150        let cached_blob_sidecars = self
151            .inner
152            .blob_cache
153            .lock()
154            .iter()
155            .map(|(_, blob_sidecar)| Arc::clone(blob_sidecar))
156            .collect::<Vec<_>>();
157        for blob_sidecar in cached_blob_sidecars {
158            if let Some(blob_sidecar) = blob_sidecar.as_eip7594() {
159                for (hash_idx, match_result) in blob_sidecar
160                    .match_versioned_hashes_cells(versioned_hashes, cell_mask)
161                    .map_err(|err| BlobStoreError::Other(Box::new(err)))?
162                {
163                    let slot = &mut result[hash_idx];
164                    if slot.is_none() {
165                        missing_count -= 1;
166                    }
167                    *slot = Some(match_result);
168                }
169            }
170
171            if missing_count == 0 && result.iter().all(Option::is_some) {
172                return Ok(result)
173            }
174        }
175
176        let mut missing_tx_hashes = Vec::new();
177        let mut seen_missing_tx_hashes = B256Set::default();
178        {
179            let mut versioned_to_txhashes = self.inner.versioned_hashes_to_txhash.lock();
180            for (idx, _) in
181                result.iter().enumerate().filter(|(_, cells_and_proofs)| cells_and_proofs.is_none())
182            {
183                let versioned_hash = versioned_hashes[idx];
184                if let Some(tx_hash) = versioned_to_txhashes.get(&versioned_hash).copied() &&
185                    seen_missing_tx_hashes.insert(tx_hash)
186                {
187                    missing_tx_hashes.push(tx_hash);
188                }
189            }
190        }
191
192        if !missing_tx_hashes.is_empty() {
193            let blobs_from_disk = self.inner.read_many_decoded(missing_tx_hashes);
194            for (_, blob_sidecar) in blobs_from_disk {
195                if let Some(blob_sidecar) = blob_sidecar.as_eip7594() {
196                    for (hash_idx, match_result) in blob_sidecar
197                        .match_versioned_hashes_cells(versioned_hashes, cell_mask)
198                        .map_err(|err| BlobStoreError::Other(Box::new(err)))?
199                    {
200                        if result[hash_idx].is_none() {
201                            result[hash_idx] = Some(match_result);
202                        }
203                    }
204                }
205            }
206        }
207
208        Ok(result)
209    }
210}
211
212impl BlobStore for DiskFileBlobStore {
213    fn insert(&self, tx: B256, data: BlobTransactionSidecarVariant) -> Result<(), BlobStoreError> {
214        self.inner.insert_one(tx, data)
215    }
216
217    fn insert_all(
218        &self,
219        txs: Vec<(B256, BlobTransactionSidecarVariant)>,
220    ) -> Result<(), BlobStoreError> {
221        if txs.is_empty() {
222            return Ok(())
223        }
224        self.inner.insert_many(txs)
225    }
226
227    fn delete(&self, tx: B256) -> Result<(), BlobStoreError> {
228        if self.inner.contains(tx)? {
229            self.inner.txs_to_delete.write().insert(tx);
230        }
231        Ok(())
232    }
233
234    fn delete_all(&self, txs: Vec<B256>) -> Result<(), BlobStoreError> {
235        if txs.is_empty() {
236            return Ok(())
237        }
238        let txs = self.inner.retain_existing(txs)?;
239        self.inner.txs_to_delete.write().extend(txs);
240        Ok(())
241    }
242
243    fn cleanup(&self) -> BlobStoreCleanupStat {
244        let txs_to_delete = std::mem::take(&mut *self.inner.txs_to_delete.write());
245        let mut stat = BlobStoreCleanupStat::default();
246        let mut subsize = 0;
247        debug!(target:"txpool::blob", num_blobs=%txs_to_delete.len(), "Removing blobs from disk");
248        for tx in txs_to_delete {
249            let path = self.inner.blob_disk_file(tx);
250            let filesize = fs::metadata(&path).map_or(0, |meta| meta.len());
251            match fs::remove_file(&path) {
252                Ok(_) => {
253                    stat.delete_succeed += 1;
254                    subsize += filesize;
255                }
256                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
257                    // Already deleted by a concurrent cleanup task
258                    stat.delete_succeed += 1;
259                }
260                Err(e) => {
261                    stat.delete_failed += 1;
262                    let err = DiskFileBlobStoreError::DeleteFile(tx, path, e);
263                    debug!(target:"txpool::blob", %err);
264                }
265            };
266        }
267        self.inner.size_tracker.sub_size(subsize as usize);
268        self.inner.size_tracker.sub_len(stat.delete_succeed);
269        stat
270    }
271
272    fn get(&self, tx: B256) -> Result<Option<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
273        self.inner.get_one(tx)
274    }
275
276    fn contains(&self, tx: B256) -> Result<bool, BlobStoreError> {
277        self.inner.contains(tx)
278    }
279
280    fn get_all(
281        &self,
282        txs: Vec<B256>,
283    ) -> Result<Vec<(B256, Arc<BlobTransactionSidecarVariant>)>, BlobStoreError> {
284        if txs.is_empty() {
285            return Ok(Vec::new())
286        }
287        self.inner.get_all(txs)
288    }
289
290    fn get_exact(
291        &self,
292        txs: Vec<B256>,
293    ) -> Result<Vec<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
294        if txs.is_empty() {
295            return Ok(Vec::new())
296        }
297        self.inner.get_exact(txs)
298    }
299
300    fn get_by_versioned_hashes_v1(
301        &self,
302        versioned_hashes: &[B256],
303    ) -> Result<Vec<Option<BlobAndProofV1>>, BlobStoreError> {
304        // the response must always be the same len as the request, misses must be None
305        let mut result = vec![None; versioned_hashes.len()];
306
307        // first scan all cached full sidecars
308        for (_tx_hash, blob_sidecar) in self.inner.blob_cache.lock().iter() {
309            if let Some(blob_sidecar) = blob_sidecar.as_eip4844() {
310                for (hash_idx, match_result) in
311                    blob_sidecar.match_versioned_hashes(versioned_hashes)
312                {
313                    result[hash_idx] = Some(match_result);
314                }
315            }
316
317            // return early if all blobs are found.
318            if result.iter().all(|blob| blob.is_some()) {
319                return Ok(result);
320            }
321        }
322
323        // not all versioned hashes were be found, try to look up a matching tx
324
325        let mut missing_tx_hashes = Vec::new();
326        let mut seen_missing_tx_hashes = B256Set::default();
327
328        {
329            let mut versioned_to_txhashes = self.inner.versioned_hashes_to_txhash.lock();
330            for (idx, _) in
331                result.iter().enumerate().filter(|(_, blob_and_proof)| blob_and_proof.is_none())
332            {
333                // this is safe because the result vec has the same len
334                let versioned_hash = versioned_hashes[idx];
335                if let Some(tx_hash) = versioned_to_txhashes.get(&versioned_hash).copied() &&
336                    seen_missing_tx_hashes.insert(tx_hash)
337                {
338                    missing_tx_hashes.push(tx_hash);
339                }
340            }
341        }
342
343        // if we have missing blobs, try to read them from disk and try again
344        if !missing_tx_hashes.is_empty() {
345            let blobs_from_disk = self.inner.read_many_decoded(missing_tx_hashes);
346            for (_, blob_sidecar) in blobs_from_disk {
347                if let Some(blob_sidecar) = blob_sidecar.as_eip4844() {
348                    for (hash_idx, match_result) in
349                        blob_sidecar.match_versioned_hashes(versioned_hashes)
350                    {
351                        if result[hash_idx].is_none() {
352                            result[hash_idx] = Some(match_result);
353                        }
354                    }
355                }
356            }
357        }
358
359        Ok(result)
360    }
361
362    fn get_by_versioned_hashes_v2(
363        &self,
364        versioned_hashes: &[B256],
365    ) -> Result<Option<Vec<BlobAndProofV2>>, BlobStoreError> {
366        let result = self.get_by_versioned_hashes_eip7594(versioned_hashes)?;
367
368        // only return the blobs if we found all requested versioned hashes
369        if result.iter().all(|blob| blob.is_some()) {
370            Ok(Some(result.into_iter().map(Option::unwrap).collect()))
371        } else {
372            Ok(None)
373        }
374    }
375
376    fn get_by_versioned_hashes_v3(
377        &self,
378        versioned_hashes: &[B256],
379    ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
380        self.get_by_versioned_hashes_eip7594(versioned_hashes)
381    }
382
383    fn get_by_versioned_hashes_v4(
384        &self,
385        versioned_hashes: &[B256],
386        indices_bitarray: B128,
387    ) -> Result<Vec<Option<BlobCellsAndProofsV1>>, BlobStoreError> {
388        self.get_by_versioned_hashes_cells_eip7594(versioned_hashes, indices_bitarray)
389    }
390
391    fn has_versioned_hashes(&self, versioned_hashes: &[B256]) -> Result<Vec<bool>, BlobStoreError> {
392        let mut result = vec![false; versioned_hashes.len()];
393        for (_tx_hash, blob_sidecar) in self.inner.blob_cache.lock().iter() {
394            for available_hash in blob_sidecar.versioned_hashes() {
395                for (idx, requested_hash) in versioned_hashes.iter().enumerate() {
396                    if !result[idx] && *requested_hash == available_hash {
397                        result[idx] = true;
398                    }
399                }
400            }
401
402            if result.iter().all(|available| *available) {
403                return Ok(result)
404            }
405        }
406
407        let mut missing_tx_hashes = Vec::new();
408        {
409            let mut versioned_to_txhashes = self.inner.versioned_hashes_to_txhash.lock();
410            for (idx, requested_hash) in versioned_hashes.iter().enumerate() {
411                if !result[idx] &&
412                    let Some(tx_hash) = versioned_to_txhashes.get(requested_hash).copied()
413                {
414                    missing_tx_hashes.push((idx, tx_hash));
415                }
416            }
417        }
418
419        for (idx, tx_hash) in missing_tx_hashes {
420            if self.inner.contains(tx_hash)? {
421                result[idx] = true;
422            }
423        }
424
425        Ok(result)
426    }
427
428    fn get_cells(
429        &self,
430        tx: B256,
431        indices_bitarray: B128,
432    ) -> Result<Option<Vec<Cell>>, BlobStoreError> {
433        let Some(sidecar) = self.get(tx)? else {
434            return Ok(None);
435        };
436
437        let Some(sidecar) = sidecar.as_eip7594() else {
438            return Ok(None);
439        };
440
441        sidecar
442            .compute_matching_cells(BlobCellMask::new(indices_bitarray))
443            .map(Some)
444            .map_err(|err| BlobStoreError::Other(Box::new(err)))
445    }
446
447    fn data_size_hint(&self) -> Option<usize> {
448        Some(self.inner.size_tracker.data_size())
449    }
450
451    fn blobs_len(&self) -> usize {
452        self.inner.size_tracker.blobs_len()
453    }
454}
455
456struct DiskFileBlobStoreInner {
457    blob_dir: PathBuf,
458    blob_cache: Mutex<LruMap<TxHash, Arc<BlobTransactionSidecarVariant>, ByLength>>,
459    size_tracker: BlobStoreSize,
460    file_lock: RwLock<()>,
461    txs_to_delete: RwLock<B256Set>,
462    /// Tracks of known versioned hashes and a transaction they exist in
463    ///
464    /// Note: It is possible that one blob can appear in multiple transactions but this only tracks
465    /// the most recent one.
466    versioned_hashes_to_txhash: Mutex<LruMap<B256, B256>>,
467}
468
469impl DiskFileBlobStoreInner {
470    /// Creates a new empty disk file blob store with the given maximum length of the blob cache.
471    fn new(blob_dir: PathBuf, max_length: u32) -> Self {
472        Self {
473            blob_dir,
474            blob_cache: Mutex::new(LruMap::new(ByLength::new(max_length))),
475            size_tracker: Default::default(),
476            file_lock: Default::default(),
477            txs_to_delete: Default::default(),
478            versioned_hashes_to_txhash: Mutex::new(LruMap::new(ByLength::new(
479                VERSIONED_HASH_TO_TX_HASH_CACHE_SIZE as u32,
480            ))),
481        }
482    }
483
484    /// Creates the directory where blobs will be stored on disk.
485    fn create_blob_dir(&self) -> Result<(), DiskFileBlobStoreError> {
486        debug!(target:"txpool::blob", blob_dir = ?self.blob_dir, "Creating blob store");
487        fs::create_dir_all(&self.blob_dir)
488            .map_err(|e| DiskFileBlobStoreError::Open(self.blob_dir.clone(), e))
489    }
490
491    /// Deletes the entire blob store.
492    fn delete_all(&self) -> Result<(), DiskFileBlobStoreError> {
493        match fs::remove_dir_all(&self.blob_dir) {
494            Ok(_) => {
495                debug!(target:"txpool::blob", blob_dir = ?self.blob_dir, "Removed blob store directory");
496            }
497            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
498            Err(err) => return Err(DiskFileBlobStoreError::Open(self.blob_dir.clone(), err)),
499        }
500        Ok(())
501    }
502
503    /// Ensures blob is in the blob cache and written to the disk.
504    fn insert_one(
505        &self,
506        tx: B256,
507        data: BlobTransactionSidecarVariant,
508    ) -> Result<(), BlobStoreError> {
509        let mut buf = Vec::with_capacity(data.rlp_encoded_fields_length());
510        data.rlp_encode_fields(&mut buf);
511
512        {
513            // cache the versioned hashes to tx hash
514            let mut map = self.versioned_hashes_to_txhash.lock();
515            data.versioned_hashes().for_each(|hash| {
516                map.insert(hash, tx);
517            });
518        }
519
520        self.blob_cache.lock().insert(tx, Arc::new(data));
521
522        let size = self.write_one_encoded(tx, &buf)?;
523
524        self.size_tracker.add_size(size);
525        self.size_tracker.inc_len(1);
526        Ok(())
527    }
528
529    /// Ensures blobs are in the blob cache and written to the disk.
530    fn insert_many(
531        &self,
532        txs: Vec<(B256, BlobTransactionSidecarVariant)>,
533    ) -> Result<(), BlobStoreError> {
534        let raw = txs
535            .iter()
536            .map(|(tx, data)| {
537                let mut buf = Vec::with_capacity(data.rlp_encoded_fields_length());
538                data.rlp_encode_fields(&mut buf);
539                (self.blob_disk_file(*tx), buf)
540            })
541            .collect::<Vec<_>>();
542
543        {
544            // cache versioned hashes to tx hash
545            let mut map = self.versioned_hashes_to_txhash.lock();
546            for (tx, data) in &txs {
547                data.versioned_hashes().for_each(|hash| {
548                    map.insert(hash, *tx);
549                });
550            }
551        }
552
553        {
554            // cache blobs
555            let mut cache = self.blob_cache.lock();
556            for (tx, data) in txs {
557                cache.insert(tx, Arc::new(data));
558            }
559        }
560
561        let mut add = 0;
562        let mut num = 0;
563        {
564            let _lock = self.file_lock.write();
565            for (path, data) in raw {
566                if path.exists() {
567                    debug!(target:"txpool::blob", ?path, "Blob already exists");
568                } else if let Err(err) = fs::write(&path, &data) {
569                    debug!(target:"txpool::blob", %err, ?path, "Failed to write blob file");
570                } else {
571                    add += data.len();
572                    num += 1;
573                }
574            }
575        }
576        self.size_tracker.add_size(add);
577        self.size_tracker.inc_len(num);
578
579        Ok(())
580    }
581
582    /// Returns true if the blob for the given transaction hash is in the blob cache or on disk.
583    fn contains(&self, tx: B256) -> Result<bool, BlobStoreError> {
584        if self.blob_cache.lock().get(&tx).is_some() {
585            return Ok(true)
586        }
587        // we only check if the file exists and assume it's valid
588        Ok(self.blob_disk_file(tx).is_file())
589    }
590
591    /// Returns all the blob transactions which are in the cache or on the disk.
592    fn retain_existing(&self, txs: Vec<B256>) -> Result<Vec<B256>, BlobStoreError> {
593        let (in_cache, not_in_cache): (Vec<B256>, Vec<B256>) = {
594            let mut cache = self.blob_cache.lock();
595            txs.into_iter().partition(|tx| cache.get(tx).is_some())
596        };
597
598        let mut existing = in_cache;
599        for tx in not_in_cache {
600            if self.blob_disk_file(tx).is_file() {
601                existing.push(tx);
602            }
603        }
604
605        Ok(existing)
606    }
607
608    /// Retrieves the blob for the given transaction hash from the blob cache or disk.
609    fn get_one(
610        &self,
611        tx: B256,
612    ) -> Result<Option<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
613        if let Some(blob) = self.blob_cache.lock().get(&tx) {
614            return Ok(Some(blob.clone()))
615        }
616
617        if let Some(blob) = self.read_one(tx)? {
618            let blob_arc = Arc::new(blob);
619            self.blob_cache.lock().insert(tx, blob_arc.clone());
620            return Ok(Some(blob_arc))
621        }
622
623        Ok(None)
624    }
625
626    /// Returns the path to the blob file for the given transaction hash.
627    #[inline]
628    fn blob_disk_file(&self, tx: B256) -> PathBuf {
629        self.blob_dir.join(format!("{tx:x}"))
630    }
631
632    /// Retrieves the blob data for the given transaction hash.
633    #[inline]
634    fn read_one(&self, tx: B256) -> Result<Option<BlobTransactionSidecarVariant>, BlobStoreError> {
635        let path = self.blob_disk_file(tx);
636        let data = {
637            let _lock = self.file_lock.read();
638            match fs::read(&path) {
639                Ok(data) => data,
640                Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
641                Err(e) => {
642                    return Err(BlobStoreError::Other(Box::new(DiskFileBlobStoreError::ReadFile(
643                        tx, path, e,
644                    ))))
645                }
646            }
647        };
648        BlobTransactionSidecarVariant::rlp_decode_fields(&mut data.as_slice())
649            .map(Some)
650            .map_err(BlobStoreError::DecodeError)
651    }
652
653    /// Returns decoded blobs read from disk.
654    ///
655    /// Only returns sidecars that were found and successfully decoded.
656    fn read_many_decoded(&self, txs: Vec<TxHash>) -> Vec<(TxHash, BlobTransactionSidecarVariant)> {
657        self.read_many_raw(txs)
658            .into_iter()
659            .filter_map(|(tx, data)| {
660                BlobTransactionSidecarVariant::rlp_decode_fields(&mut data.as_slice())
661                    .map(|sidecar| (tx, sidecar))
662                    .ok()
663            })
664            .collect()
665    }
666
667    /// Retrieves the raw blob data for the given transaction hashes.
668    ///
669    /// Only returns the blobs that were found in file.
670    #[inline]
671    fn read_many_raw(&self, txs: Vec<TxHash>) -> Vec<(TxHash, Vec<u8>)> {
672        let mut res = Vec::with_capacity(txs.len());
673        let _lock = self.file_lock.read();
674        for tx in txs {
675            let path = self.blob_disk_file(tx);
676            match fs::read(&path) {
677                Ok(data) => {
678                    res.push((tx, data));
679                }
680                Err(err) => {
681                    debug!(target:"txpool::blob", %err, ?tx, "Failed to read blob file");
682                }
683            };
684        }
685        res
686    }
687
688    /// Writes the blob data for the given transaction hash to the disk.
689    #[inline]
690    fn write_one_encoded(&self, tx: B256, data: &[u8]) -> Result<usize, DiskFileBlobStoreError> {
691        trace!(target:"txpool::blob", "[{:?}] writing blob file", tx);
692        let mut add = 0;
693        let path = self.blob_disk_file(tx);
694        {
695            let _lock = self.file_lock.write();
696            if !path.exists() {
697                fs::write(&path, data)
698                    .map_err(|e| DiskFileBlobStoreError::WriteFile(tx, path, e))?;
699                add = data.len();
700            }
701        }
702        Ok(add)
703    }
704
705    /// Retrieves blobs for the given transaction hashes from the blob cache or disk.
706    ///
707    /// This will not return an error if there are missing blobs. Therefore, the result may be a
708    /// subset of the request or an empty vector if none of the blobs were found.
709    #[inline]
710    fn get_all(
711        &self,
712        txs: Vec<B256>,
713    ) -> Result<Vec<(B256, Arc<BlobTransactionSidecarVariant>)>, BlobStoreError> {
714        let mut res = Vec::with_capacity(txs.len());
715        let mut cache_miss = Vec::new();
716        {
717            let mut cache = self.blob_cache.lock();
718            for tx in txs {
719                if let Some(blob) = cache.get(&tx) {
720                    res.push((tx, blob.clone()));
721                } else {
722                    cache_miss.push(tx)
723                }
724            }
725        }
726        if cache_miss.is_empty() {
727            return Ok(res)
728        }
729        let from_disk = self.read_many_decoded(cache_miss);
730        if from_disk.is_empty() {
731            return Ok(res)
732        }
733        let from_disk = from_disk
734            .into_iter()
735            .map(|(tx, data)| {
736                let data = Arc::new(data);
737                res.push((tx, data.clone()));
738                (tx, data)
739            })
740            .collect::<Vec<_>>();
741
742        let mut cache = self.blob_cache.lock();
743        for (tx, data) in from_disk {
744            cache.insert(tx, data);
745        }
746
747        Ok(res)
748    }
749
750    /// Retrieves blobs for the given transaction hashes from the blob cache or disk.
751    ///
752    /// Returns an error if there are any missing blobs.
753    #[inline]
754    fn get_exact(
755        &self,
756        txs: Vec<B256>,
757    ) -> Result<Vec<Arc<BlobTransactionSidecarVariant>>, BlobStoreError> {
758        txs.into_iter()
759            .map(|tx| self.get_one(tx)?.ok_or(BlobStoreError::MissingSidecar(tx)))
760            .collect()
761    }
762}
763
764impl fmt::Debug for DiskFileBlobStoreInner {
765    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
766        f.debug_struct("DiskFileBlobStoreInner")
767            .field("blob_dir", &self.blob_dir)
768            .field("cached_blobs", &self.blob_cache.try_lock().map(|lock| lock.len()))
769            .field("txs_to_delete", &self.txs_to_delete.try_read())
770            .finish()
771    }
772}
773
774/// Errors that can occur when interacting with a disk file blob store.
775#[derive(Debug, thiserror::Error)]
776pub enum DiskFileBlobStoreError {
777    /// Thrown during [`DiskFileBlobStore::open`] if the blob store directory cannot be opened.
778    #[error("failed to open blobstore at {0}: {1}")]
779    /// Indicates a failure to open the blob store directory.
780    Open(PathBuf, io::Error),
781    /// Failure while reading a blob file.
782    #[error("[{0}] failed to read blob file at {1}: {2}")]
783    /// Indicates a failure while reading a blob file.
784    ReadFile(TxHash, PathBuf, io::Error),
785    /// Failure while writing a blob file.
786    #[error("[{0}] failed to write blob file at {1}: {2}")]
787    /// Indicates a failure while writing a blob file.
788    WriteFile(TxHash, PathBuf, io::Error),
789    /// Failure while deleting a blob file.
790    #[error("[{0}] failed to delete blob file at {1}: {2}")]
791    /// Indicates a failure while deleting a blob file.
792    DeleteFile(TxHash, PathBuf, io::Error),
793}
794
795impl From<DiskFileBlobStoreError> for BlobStoreError {
796    fn from(value: DiskFileBlobStoreError) -> Self {
797        Self::Other(Box::new(value))
798    }
799}
800
801/// Configuration for a disk file blob store.
802#[derive(Debug, Clone)]
803pub struct DiskFileBlobStoreConfig {
804    /// The maximum number of blobs to keep in the in memory blob cache.
805    pub max_cached_entries: u32,
806    /// How to open the blob store.
807    pub open: OpenDiskFileBlobStore,
808}
809
810impl Default for DiskFileBlobStoreConfig {
811    fn default() -> Self {
812        Self { max_cached_entries: DEFAULT_MAX_CACHED_BLOBS, open: Default::default() }
813    }
814}
815
816impl DiskFileBlobStoreConfig {
817    /// Set maximum number of blobs to keep in the in memory blob cache.
818    pub const fn with_max_cached_entries(mut self, max_cached_entries: u32) -> Self {
819        self.max_cached_entries = max_cached_entries;
820        self
821    }
822}
823
824/// How to open a disk file blob store.
825#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
826pub enum OpenDiskFileBlobStore {
827    /// Clear everything in the blob store.
828    #[default]
829    Clear,
830    /// Keep the existing blob store and index
831    ReIndex,
832}
833
834#[cfg(test)]
835mod tests {
836    use alloy_consensus::BlobTransactionSidecar;
837    use alloy_eips::{
838        eip4844::{kzg_to_versioned_hash, Blob, BlobAndProofV2, Bytes48},
839        eip7594::{
840            BlobTransactionSidecarEip7594, BlobTransactionSidecarVariant, CELLS_PER_EXT_BLOB,
841        },
842    };
843
844    use super::*;
845    use std::sync::atomic::Ordering;
846
847    fn tmp_store() -> (DiskFileBlobStore, tempfile::TempDir) {
848        let dir = tempfile::tempdir().unwrap();
849        let store = DiskFileBlobStore::open(dir.path(), Default::default()).unwrap();
850        (store, dir)
851    }
852
853    fn rng_blobs(num: usize) -> Vec<(TxHash, BlobTransactionSidecarVariant)> {
854        let mut rng = rand::rng();
855        (0..num)
856            .map(|_| {
857                let tx = TxHash::random_with(&mut rng);
858                let blob = BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar {
859                    blobs: vec![],
860                    commitments: vec![],
861                    proofs: vec![],
862                });
863                (tx, blob)
864            })
865            .collect()
866    }
867
868    fn eip7594_single_blob_sidecar() -> (BlobTransactionSidecarVariant, B256, BlobAndProofV2) {
869        let blob = Blob::default();
870        let commitment = Bytes48::default();
871        let cell_proofs = vec![Bytes48::default(); CELLS_PER_EXT_BLOB];
872
873        let versioned_hash = kzg_to_versioned_hash(commitment.as_slice());
874
875        let expected =
876            BlobAndProofV2 { blob: Box::new(Blob::default()), proofs: cell_proofs.clone() };
877        let sidecar = BlobTransactionSidecarEip7594::new(vec![blob], vec![commitment], cell_proofs);
878
879        (BlobTransactionSidecarVariant::Eip7594(sidecar), versioned_hash, expected)
880    }
881
882    #[test]
883    fn disk_insert_all_get_all() {
884        let (store, _dir) = tmp_store();
885
886        let blobs = rng_blobs(10);
887        let all_hashes = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
888        store.insert_all(blobs.clone()).unwrap();
889
890        // all cached
891        for (tx, blob) in &blobs {
892            assert!(store.is_cached(tx));
893            let b = store.get(*tx).unwrap().map(Arc::unwrap_or_clone).unwrap();
894            assert_eq!(b, *blob);
895        }
896
897        let all = store.get_all(all_hashes.clone()).unwrap();
898        for (tx, blob) in all {
899            assert!(blobs.contains(&(tx, Arc::unwrap_or_clone(blob))), "missing blob {tx:?}");
900        }
901
902        assert!(store.contains(all_hashes[0]).unwrap());
903        store.delete_all(all_hashes.clone()).unwrap();
904        assert!(store.inner.txs_to_delete.read().contains(&all_hashes[0]));
905        store.clear_cache();
906        store.cleanup();
907
908        assert!(store.get(blobs[0].0).unwrap().is_none());
909
910        let all = store.get_all(all_hashes.clone()).unwrap();
911        assert!(all.is_empty());
912
913        assert!(!store.contains(all_hashes[0]).unwrap());
914        assert!(store.get_exact(all_hashes).is_err());
915
916        assert_eq!(store.data_size_hint(), Some(0));
917        assert_eq!(store.inner.size_tracker.num_blobs.load(Ordering::Relaxed), 0);
918    }
919
920    #[test]
921    fn disk_insert_and_retrieve() {
922        let (store, _dir) = tmp_store();
923
924        let (tx, blob) = rng_blobs(1).into_iter().next().unwrap();
925        store.insert(tx, blob.clone()).unwrap();
926
927        assert!(store.is_cached(&tx));
928        let retrieved_blob = store.get(tx).unwrap().map(Arc::unwrap_or_clone).unwrap();
929        assert_eq!(retrieved_blob, blob);
930    }
931
932    #[test]
933    fn disk_delete_blob() {
934        let (store, _dir) = tmp_store();
935
936        let (tx, blob) = rng_blobs(1).into_iter().next().unwrap();
937        store.insert(tx, blob).unwrap();
938        assert!(store.is_cached(&tx));
939
940        store.delete(tx).unwrap();
941        assert!(store.inner.txs_to_delete.read().contains(&tx));
942        store.cleanup();
943
944        let result = store.get(tx).unwrap();
945        assert_eq!(
946            result,
947            Some(Arc::new(BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar {
948                blobs: vec![],
949                commitments: vec![],
950                proofs: vec![]
951            })))
952        );
953    }
954
955    #[test]
956    fn disk_insert_all_and_delete_all() {
957        let (store, _dir) = tmp_store();
958
959        let blobs = rng_blobs(5);
960        let txs = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
961        store.insert_all(blobs.clone()).unwrap();
962
963        for (tx, _) in &blobs {
964            assert!(store.is_cached(tx));
965        }
966
967        store.delete_all(txs.clone()).unwrap();
968        store.cleanup();
969
970        for tx in txs {
971            let result = store.get(tx).unwrap();
972            assert_eq!(
973                result,
974                Some(Arc::new(BlobTransactionSidecarVariant::Eip4844(BlobTransactionSidecar {
975                    blobs: vec![],
976                    commitments: vec![],
977                    proofs: vec![]
978                })))
979            );
980        }
981    }
982
983    #[test]
984    fn disk_get_all_blobs() {
985        let (store, _dir) = tmp_store();
986
987        let blobs = rng_blobs(3);
988        let txs = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
989        store.insert_all(blobs.clone()).unwrap();
990
991        let retrieved_blobs = store.get_all(txs.clone()).unwrap();
992        for (tx, blob) in retrieved_blobs {
993            assert!(blobs.contains(&(tx, Arc::unwrap_or_clone(blob))));
994        }
995
996        store.delete_all(txs).unwrap();
997        store.cleanup();
998    }
999
1000    #[test]
1001    fn disk_get_exact_blobs_success() {
1002        let (store, _dir) = tmp_store();
1003
1004        let blobs = rng_blobs(3);
1005        let txs = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
1006        store.insert_all(blobs.clone()).unwrap();
1007
1008        let retrieved_blobs = store.get_exact(txs).unwrap();
1009        for (retrieved_blob, (_, original_blob)) in retrieved_blobs.into_iter().zip(blobs) {
1010            assert_eq!(Arc::unwrap_or_clone(retrieved_blob), original_blob);
1011        }
1012    }
1013
1014    #[test]
1015    fn disk_get_exact_blobs_failure() {
1016        let (store, _dir) = tmp_store();
1017
1018        let blobs = rng_blobs(2);
1019        let txs = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
1020        store.insert_all(blobs).unwrap();
1021
1022        // Try to get a blob that was never inserted
1023        let missing_tx = TxHash::random();
1024        let result = store.get_exact(vec![txs[0], missing_tx]);
1025        assert!(result.is_err());
1026    }
1027
1028    #[test]
1029    fn disk_data_size_hint() {
1030        let (store, _dir) = tmp_store();
1031        assert_eq!(store.data_size_hint(), Some(0));
1032
1033        let blobs = rng_blobs(2);
1034        store.insert_all(blobs).unwrap();
1035        assert!(store.data_size_hint().unwrap() > 0);
1036    }
1037
1038    #[test]
1039    fn disk_cleanup_stat() {
1040        let (store, _dir) = tmp_store();
1041
1042        let blobs = rng_blobs(3);
1043        let txs = blobs.iter().map(|(tx, _)| *tx).collect::<Vec<_>>();
1044        store.insert_all(blobs).unwrap();
1045
1046        store.delete_all(txs).unwrap();
1047        let stat = store.cleanup();
1048        assert_eq!(stat.delete_succeed, 3);
1049        assert_eq!(stat.delete_failed, 0);
1050    }
1051
1052    #[test]
1053    fn disk_get_blobs_v3_returns_partial_results() {
1054        let (store, _dir) = tmp_store();
1055
1056        let (sidecar, versioned_hash, expected) = eip7594_single_blob_sidecar();
1057        store.insert(TxHash::random(), sidecar).unwrap();
1058
1059        assert_ne!(versioned_hash, B256::ZERO);
1060
1061        let request = vec![versioned_hash, B256::ZERO];
1062        let v2 = store.get_by_versioned_hashes_v2(&request).unwrap();
1063        assert!(v2.is_none(), "v2 must return null if any requested blob is missing");
1064
1065        let v3 = store.get_by_versioned_hashes_v3(&request).unwrap();
1066        assert_eq!(v3, vec![Some(expected), None]);
1067    }
1068
1069    #[test]
1070    fn disk_has_blobs_returns_ordered_availability() {
1071        let (store, _dir) = tmp_store();
1072
1073        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1074        store.insert(TxHash::random(), sidecar).unwrap();
1075
1076        let request = vec![B256::ZERO, versioned_hash, versioned_hash];
1077        assert_eq!(store.has_versioned_hashes(&request).unwrap(), vec![false, true, true]);
1078    }
1079
1080    #[test]
1081    fn disk_get_blobs_v4_returns_requested_cells() {
1082        let (store, _dir) = tmp_store();
1083
1084        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1085        store.insert(TxHash::random(), sidecar).unwrap();
1086
1087        let indices_bitarray = B128::from((1u128 << 0) | (1u128 << 7));
1088        let request = vec![versioned_hash, B256::ZERO];
1089
1090        let v4 = store.get_by_versioned_hashes_v4(&request, indices_bitarray).unwrap();
1091        assert_eq!(v4.len(), request.len());
1092        assert!(v4[1].is_none());
1093
1094        let cells_and_proofs = v4[0].as_ref().unwrap();
1095        assert_eq!(cells_and_proofs.blob_cells.len(), 2);
1096        assert_eq!(cells_and_proofs.proofs.len(), 2);
1097        assert!(cells_and_proofs.blob_cells.iter().all(Option::is_some));
1098        assert_eq!(cells_and_proofs.proofs, vec![Some(Bytes48::default()); 2]);
1099    }
1100
1101    #[test]
1102    fn disk_get_blobs_v3_can_fallback_to_disk() {
1103        let (store, _dir) = tmp_store();
1104
1105        let (sidecar, versioned_hash, expected) = eip7594_single_blob_sidecar();
1106        store.insert(TxHash::random(), sidecar).unwrap();
1107        store.clear_cache();
1108
1109        let v3 = store.get_by_versioned_hashes_v3(&[versioned_hash]).unwrap();
1110        assert_eq!(v3, vec![Some(expected)]);
1111    }
1112
1113    #[test]
1114    fn disk_has_blobs_can_fallback_to_disk() {
1115        let (store, _dir) = tmp_store();
1116
1117        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1118        store.insert(TxHash::random(), sidecar).unwrap();
1119        store.clear_cache();
1120
1121        assert_eq!(store.has_versioned_hashes(&[versioned_hash]).unwrap(), vec![true]);
1122    }
1123
1124    #[test]
1125    fn disk_has_blobs_ignores_stale_index_entries() {
1126        let (store, _dir) = tmp_store();
1127
1128        let tx_hash = TxHash::random();
1129        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1130        store.insert(tx_hash, sidecar).unwrap();
1131        store.clear_cache();
1132
1133        store.delete(tx_hash).unwrap();
1134        store.cleanup();
1135
1136        assert_eq!(store.has_versioned_hashes(&[versioned_hash]).unwrap(), vec![false]);
1137    }
1138
1139    #[test]
1140    fn disk_get_blobs_v4_can_fallback_to_disk() {
1141        let (store, _dir) = tmp_store();
1142
1143        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1144        store.insert(TxHash::random(), sidecar).unwrap();
1145        store.clear_cache();
1146
1147        let v4 = store.get_by_versioned_hashes_v4(&[versioned_hash], B128::from(1u128)).unwrap();
1148        let cells_and_proofs = v4[0].as_ref().unwrap();
1149        assert_eq!(cells_and_proofs.blob_cells.len(), 1);
1150        assert_eq!(cells_and_proofs.proofs, vec![Some(Bytes48::default())]);
1151    }
1152
1153    #[test]
1154    fn disk_get_cells_can_fallback_to_disk() {
1155        let (store, _dir) = tmp_store();
1156
1157        let tx_hash = TxHash::random();
1158        let (sidecar, versioned_hash, _) = eip7594_single_blob_sidecar();
1159        store.insert(tx_hash, sidecar).unwrap();
1160
1161        let indices_bitarray = B128::from((1u128 << 0) | (1u128 << 7));
1162        let expected = store
1163            .get_by_versioned_hashes_v4(&[versioned_hash], indices_bitarray)
1164            .unwrap()
1165            .pop()
1166            .unwrap()
1167            .unwrap()
1168            .blob_cells
1169            .into_iter()
1170            .collect::<Option<Vec<_>>>()
1171            .unwrap();
1172
1173        store.clear_cache();
1174
1175        assert_eq!(store.get_cells(tx_hash, indices_bitarray).unwrap(), Some(expected));
1176    }
1177
1178    #[test]
1179    fn disk_double_cleanup_no_failure() {
1180        let (store, _dir) = tmp_store();
1181
1182        let blobs = rng_blobs(5);
1183        let all_hashes: Vec<_> = blobs.iter().map(|(tx, _)| *tx).collect();
1184        store.insert_all(blobs).unwrap();
1185        store.clear_cache();
1186
1187        // Schedule blobs for deletion
1188        store.delete_all(all_hashes.clone()).unwrap();
1189
1190        // First cleanup: files exist, all should succeed
1191        let stat1 = store.cleanup();
1192        assert_eq!(stat1.delete_succeed, 5);
1193        assert_eq!(stat1.delete_failed, 0);
1194
1195        // Manually re-enqueue the same hashes to simulate a concurrent cleanup race
1196        store.inner.txs_to_delete.write().extend(all_hashes);
1197
1198        // Second cleanup: files already deleted, should still report success (NotFound)
1199        let stat2 = store.cleanup();
1200        assert_eq!(stat2.delete_succeed, 5);
1201        assert_eq!(stat2.delete_failed, 0);
1202    }
1203}