1use 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
16pub const DEFAULT_MAX_CACHED_BLOBS: u32 = 100;
18
19const 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#[derive(Clone, Debug)]
32pub struct DiskFileBlobStore {
33 inner: Arc<DiskFileBlobStoreInner>,
34}
35
36impl DiskFileBlobStore {
37 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 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 fn get_by_versioned_hashes_eip7594(
72 &self,
73 versioned_hashes: &[B256],
74 ) -> Result<Vec<Option<BlobAndProofV2>>, BlobStoreError> {
75 let mut result = vec![None; versioned_hashes.len()];
78 let mut missing_count = result.len();
79 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 if missing_count == 0 {
95 if result.iter().all(|blob| blob.is_some()) {
97 return Ok(result);
98 }
99 }
100 }
101
102 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 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 !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 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 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 let mut result = vec![None; versioned_hashes.len()];
306
307 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 if result.iter().all(|blob| blob.is_some()) {
319 return Ok(result);
320 }
321 }
322
323 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 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 !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 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 versioned_hashes_to_txhash: Mutex<LruMap<B256, B256>>,
467}
468
469impl DiskFileBlobStoreInner {
470 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 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 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 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 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 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 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 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 fn contains(&self, tx: B256) -> Result<bool, BlobStoreError> {
584 if self.blob_cache.lock().get(&tx).is_some() {
585 return Ok(true)
586 }
587 Ok(self.blob_disk_file(tx).is_file())
589 }
590
591 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 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 #[inline]
628 fn blob_disk_file(&self, tx: B256) -> PathBuf {
629 self.blob_dir.join(format!("{tx:x}"))
630 }
631
632 #[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 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 #[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 #[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 #[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 #[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#[derive(Debug, thiserror::Error)]
776pub enum DiskFileBlobStoreError {
777 #[error("failed to open blobstore at {0}: {1}")]
779 Open(PathBuf, io::Error),
781 #[error("[{0}] failed to read blob file at {1}: {2}")]
783 ReadFile(TxHash, PathBuf, io::Error),
785 #[error("[{0}] failed to write blob file at {1}: {2}")]
787 WriteFile(TxHash, PathBuf, io::Error),
789 #[error("[{0}] failed to delete blob file at {1}: {2}")]
791 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#[derive(Debug, Clone)]
803pub struct DiskFileBlobStoreConfig {
804 pub max_cached_entries: u32,
806 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
826pub enum OpenDiskFileBlobStore {
827 #[default]
829 Clear,
830 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 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 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 store.delete_all(all_hashes.clone()).unwrap();
1189
1190 let stat1 = store.cleanup();
1192 assert_eq!(stat1.delete_succeed, 5);
1193 assert_eq!(stat1.delete_failed, 0);
1194
1195 store.inner.txs_to_delete.write().extend(all_hashes);
1197
1198 let stat2 = store.cleanup();
1200 assert_eq!(stat2.delete_succeed, 5);
1201 assert_eq!(stat2.delete_failed, 0);
1202 }
1203}