1use 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
21pub const DEFAULT_MAX_CACHED_BLOBS: u32 = 100;
23
24const 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#[derive(Clone, Debug)]
37pub struct DiskFileBlobStore {
38 inner: Arc<DiskFileBlobStoreInner>,
39}
40
41impl DiskFileBlobStore {
42 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 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 fn get_by_versioned_hashes_eip7594(
77 &self,
78 versioned_hashes: &[B256],
79 ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
80 let mut result = vec![None; versioned_hashes.len()];
83 let mut missing_count = result.len();
84 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 if missing_count == 0 {
100 if result.iter().all(|blob| blob.is_some()) {
102 return Ok(result);
103 }
104 }
105 }
106
107 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 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 !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 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 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 let mut result = vec![None; versioned_hashes.len()];
310
311 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 if result.iter().all(|blob| blob.is_some()) {
323 return Ok(result);
324 }
325 }
326
327 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 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 !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 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 versioned_hashes_to_txhash: Mutex<LruMap<B256, B256, FbBuildHasher<32>>>,
471}
472
473impl DiskFileBlobStoreInner {
474 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 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 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 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 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 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 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 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 fn contains(&self, tx: B256) -> Result<bool, BlobStoreError> {
589 if self.blob_cache.lock().get(&tx).is_some() {
590 return Ok(true)
591 }
592 Ok(self.blob_disk_file(tx).is_file())
594 }
595
596 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 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 #[inline]
633 fn blob_disk_file(&self, tx: B256) -> PathBuf {
634 self.blob_dir.join(format!("{tx:x}"))
635 }
636
637 #[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 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 #[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 #[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 #[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 #[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#[derive(Debug, thiserror::Error)]
781pub enum DiskFileBlobStoreError {
782 #[error("failed to open blobstore at {0}: {1}")]
784 Open(PathBuf, io::Error),
786 #[error("[{0}] failed to read blob file at {1}: {2}")]
788 ReadFile(TxHash, PathBuf, io::Error),
790 #[error("[{0}] failed to write blob file at {1}: {2}")]
792 WriteFile(TxHash, PathBuf, io::Error),
794 #[error("[{0}] failed to delete blob file at {1}: {2}")]
796 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#[derive(Debug, Clone)]
808pub struct DiskFileBlobStoreConfig {
809 pub max_cached_entries: u32,
811 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
831pub enum OpenDiskFileBlobStore {
832 #[default]
834 Clear,
835 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 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 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 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 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 store.delete_all(all_hashes.clone()).unwrap();
1225
1226 let stat1 = store.cleanup();
1228 assert_eq!(stat1.delete_succeed, 5);
1229 assert_eq!(stat1.delete_failed, 0);
1230
1231 store.inner.txs_to_delete.write().extend(all_hashes);
1233
1234 let stat2 = store.cleanup();
1236 assert_eq!(stat2.delete_succeed, 5);
1237 assert_eq!(stat2.delete_failed, 0);
1238 }
1239}