Skip to main content

reth_transaction_pool/blobstore/
disk.rs

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