Skip to main content

reth_provider/bal/
rocksdb.rs

1use crate::providers::RocksDBProvider;
2use alloy_eip7928::BAL_RETENTION_PERIOD_SLOTS;
3use alloy_eips::NumHash;
4use alloy_primitives::{map::B256Map, BlockHash, BlockNumber, Bytes};
5use parking_lot::RwLock;
6use reth_db_api::{
7    models::{StoredBlockAccessList, StoredBlockAccessListKey},
8    table::{Decode, Decompress},
9    tables, DatabaseError,
10};
11use reth_prune_types::PruneMode;
12use reth_storage_api::{BalStore, GetBlockAccessListLimit, RawBal};
13use reth_storage_errors::provider::{ProviderError, ProviderResult};
14use std::{
15    collections::{BTreeMap, BTreeSet},
16    sync::Arc,
17};
18
19/// Number of recent blocks kept in the in-memory `RocksDB` BAL buffer.
20const DEFAULT_BAL_BUFFER_RETENTION_DISTANCE: u64 = 32;
21
22/// RocksDB-backed BAL store.
23///
24/// Persisted BALs are keyed by `(block_number, block_hash)` for ordered pruning and indexed by
25/// block hash for direct [`BalStore`] lookups. Validated BALs enter a shared in-memory buffer
26/// first; [`BalStore::flush`] makes confirmed canonical entries durable while retaining them in the
27/// read cache until its shorter retention window expires.
28#[derive(Clone)]
29pub struct RocksDBBalStore {
30    /// Number of recent blocks retained in the in-memory buffer.
31    buffer_retention_distance: u64,
32    /// `RocksDB` provider used for persisted BAL reads and writes.
33    rocksdb: RocksDBProvider,
34    /// Shared recent-read cache and pending-write state.
35    buffer: Arc<RwLock<RocksDBBalStoreBuffer>>,
36}
37
38impl RocksDBBalStore {
39    /// Creates a new store with the EIP-defined retention distance.
40    pub fn new(rocksdb: RocksDBProvider) -> Self {
41        Self::with_buffer_retention_distance(rocksdb, DEFAULT_BAL_BUFFER_RETENTION_DISTANCE)
42    }
43
44    /// Creates a new store that retains buffered BALs for the given block distance.
45    ///
46    /// This does not change the EIP-defined retention distance for persisted BALs.
47    pub fn with_buffer_retention_distance(rocksdb: RocksDBProvider, blocks: u64) -> Self {
48        Self {
49            buffer_retention_distance: blocks,
50            rocksdb,
51            buffer: Arc::new(RwLock::new(RocksDBBalStoreBuffer::default())),
52        }
53    }
54
55    #[cfg(test)]
56    const fn rocksdb_provider(&self) -> &RocksDBProvider {
57        &self.rocksdb
58    }
59
60    fn keys_to_prune(&self, tip: BlockNumber) -> ProviderResult<Vec<StoredBlockAccessListKey>> {
61        let retention = PruneMode::Distance(BAL_RETENTION_PERIOD_SLOTS);
62        let mut keys = Vec::new();
63        let iter = self.rocksdb.raw_key_iter_from::<tables::BlockAccessLists>(
64            StoredBlockAccessListKey::first_at_number(0),
65        )?;
66
67        for key_bytes in iter {
68            let key_bytes = key_bytes?;
69            let key = StoredBlockAccessListKey::decode(&key_bytes)
70                .map_err(|_| ProviderError::Database(DatabaseError::Decode))?;
71            if !retention.should_prune(key.number(), tip) {
72                break
73            }
74            keys.push(key);
75        }
76
77        Ok(keys)
78    }
79
80    fn delete_keys(&self, keys: &[StoredBlockAccessListKey]) -> ProviderResult<usize> {
81        if keys.is_empty() {
82            return Ok(0)
83        }
84
85        let mut batch = self.rocksdb.batch();
86        for key in keys {
87            batch.delete::<tables::BlockAccessLists>(*key)?;
88            batch.delete::<tables::BlockAccessListBlockNumbers>(key.hash())?;
89        }
90        batch.commit()?;
91        Ok(keys.len())
92    }
93
94    fn read_one_from_disk(&self, key: StoredBlockAccessListKey) -> ProviderResult<Option<Bytes>> {
95        let Some(value) = self.rocksdb.get_raw::<tables::BlockAccessLists>(key)? else {
96            return Ok(None)
97        };
98        let stored = StoredBlockAccessList::decompress(&value)
99            .map_err(|_| ProviderError::Database(DatabaseError::Decode))?;
100        Ok(Some(stored.into_raw()))
101    }
102
103    fn read_one_by_hash(&self, block_hash: BlockHash) -> ProviderResult<Option<Bytes>> {
104        if let Some(bal) = self.buffer.read().get_by_hash(block_hash) {
105            return Ok(Some(bal))
106        }
107
108        // Read-only legacy databases predate both BAL tables.
109        if self.rocksdb.is_read_only() &&
110            !self.rocksdb.has_table::<tables::BlockAccessLists>() &&
111            !self.rocksdb.has_table::<tables::BlockAccessListBlockNumbers>()
112        {
113            return Ok(None)
114        }
115
116        let Some(block_number) =
117            self.rocksdb.get::<tables::BlockAccessListBlockNumbers>(block_hash)?
118        else {
119            return Ok(None)
120        };
121        self.read_one_from_disk(StoredBlockAccessListKey::new(block_number, block_hash))
122    }
123}
124
125impl std::fmt::Debug for RocksDBBalStore {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("RocksDBBalStore")
128            .field("buffer_retention_distance", &self.buffer_retention_distance)
129            .field("rocksdb", &self.rocksdb)
130            .finish_non_exhaustive()
131    }
132}
133
134impl BalStore for RocksDBBalStore {
135    fn insert(&self, block: NumHash, bal: RawBal) -> ProviderResult<()> {
136        self.buffer.write().insert(block, bal);
137        Ok(())
138    }
139
140    fn insert_many(&self, entries: Vec<(NumHash, RawBal)>) -> ProviderResult<()> {
141        if entries.is_empty() {
142            return Ok(())
143        }
144
145        let mut buffer = self.buffer.write();
146        buffer.entries.reserve(entries.len());
147        for (block, bal) in entries {
148            buffer.insert(block, bal);
149        }
150        Ok(())
151    }
152
153    fn flush(&self, blocks: &[NumHash]) -> ProviderResult<()> {
154        let pending = {
155            let mut buffer = self.buffer.write();
156            buffer.mark_canonical(blocks);
157            buffer.canonical_pending_entries()
158        };
159        if !pending.is_empty() {
160            let mut batch = self.rocksdb.batch();
161            for (key, bal) in &pending {
162                let value = StoredBlockAccessList::new_unchecked(bal.hash(), bal.as_raw().clone());
163                batch.put::<tables::BlockAccessLists>(*key, &value)?;
164                batch.put::<tables::BlockAccessListBlockNumbers>(key.hash(), &key.number())?;
165            }
166            batch.commit()?;
167
168            self.buffer.write().remove_flushed_pending(&pending);
169        }
170
171        if let Some(tip) = blocks.iter().map(|block| block.number).max() {
172            self.buffer.write().prune(self.buffer_retention_distance, tip);
173        }
174        Ok(())
175    }
176
177    fn prune(&self, tip: BlockNumber) -> ProviderResult<usize> {
178        let keys = self.keys_to_prune(tip)?;
179        let pruned = self.delete_keys(&keys)?;
180        self.buffer.write().remove_keys(&keys);
181        Ok(pruned)
182    }
183
184    fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
185        block_hashes.iter().map(|hash| self.read_one_by_hash(*hash)).collect()
186    }
187
188    fn append_by_hashes_with_limit(
189        &self,
190        block_hashes: &[BlockHash],
191        limit: GetBlockAccessListLimit,
192        out: &mut Vec<Option<Bytes>>,
193    ) -> ProviderResult<()> {
194        let mut size = 0;
195        for block_hash in block_hashes {
196            let bal = self.read_one_by_hash(*block_hash)?;
197            size += bal.as_ref().map_or(1, |bytes| bytes.len());
198            out.push(bal);
199
200            if limit.exceeds(size) {
201                break
202            }
203        }
204        Ok(())
205    }
206}
207
208/// Shared in-memory state for recent reads and writes awaiting canonical confirmation.
209///
210/// Successful flushes clear only the pending-write state. Cached entries remain available until
211/// the buffer retention window evicts them.
212#[derive(Debug, Default)]
213struct RocksDBBalStoreBuffer {
214    /// Hash index for serving recent hash-only lookups.
215    entries: B256Map<RocksDBBalEntry>,
216    /// Block-number index for pruning buffered entries.
217    hashes_by_number: BTreeMap<BlockNumber, Vec<BlockHash>>,
218    /// Validated BALs waiting to be confirmed canonical and flushed.
219    pending: BTreeMap<StoredBlockAccessListKey, RawBal>,
220    /// Pending writes confirmed canonical, including writes retained for retry after failure.
221    canonical_pending: BTreeSet<StoredBlockAccessListKey>,
222}
223
224impl RocksDBBalStoreBuffer {
225    fn insert(&mut self, block: NumHash, bal: RawBal) {
226        let pending = bal.clone();
227        if let Some(entry) =
228            self.entries.insert(block.hash, RocksDBBalEntry { block_number: block.number, bal })
229        {
230            self.remove_hash_from_number(entry.block_number, block.hash);
231            self.pending.remove(&StoredBlockAccessListKey::new(entry.block_number, block.hash));
232            self.canonical_pending
233                .remove(&StoredBlockAccessListKey::new(entry.block_number, block.hash));
234        }
235
236        self.hashes_by_number.entry(block.number).or_default().push(block.hash);
237        self.pending.insert(StoredBlockAccessListKey::new(block.number, block.hash), pending);
238    }
239
240    /// Marks exact pending block identities as eligible for the next flush.
241    fn mark_canonical(&mut self, blocks: &[NumHash]) {
242        self.canonical_pending.extend(
243            blocks
244                .iter()
245                .map(|block| StoredBlockAccessListKey::new(block.number, block.hash))
246                .filter(|key| self.pending.contains_key(key)),
247        );
248    }
249
250    /// Snapshots confirmed writes so `RocksDB` I/O can run without holding the buffer lock.
251    fn canonical_pending_entries(&self) -> Vec<(StoredBlockAccessListKey, RawBal)> {
252        self.canonical_pending
253            .iter()
254            .filter_map(|key| self.pending.get(key).map(|bal| (*key, bal.clone())))
255            .collect()
256    }
257
258    fn keys_to_prune(
259        &self,
260        retention_distance: u64,
261        tip: BlockNumber,
262    ) -> Vec<StoredBlockAccessListKey> {
263        let prune_mode = PruneMode::Distance(retention_distance);
264        self.hashes_by_number
265            .iter()
266            .take_while(|(block_number, _)| prune_mode.should_prune(**block_number, tip))
267            .flat_map(|(block_number, hashes)| {
268                hashes.iter().map(move |hash| StoredBlockAccessListKey::new(*block_number, *hash))
269            })
270            .filter(|key| !self.canonical_pending.contains(key))
271            .collect()
272    }
273
274    fn get_by_hash(&self, hash: BlockHash) -> Option<Bytes> {
275        self.entries.get(&hash).map(|entry| entry.bal.as_raw().clone())
276    }
277
278    /// Clears pending state only if it still matches the snapshot written to `RocksDB`.
279    ///
280    /// A concurrent replacement for the same key must remain pending for a later flush.
281    fn remove_flushed_pending(&mut self, flushed: &[(StoredBlockAccessListKey, RawBal)]) {
282        for (key, bal) in flushed {
283            let pending_matches =
284                self.pending.get(key).is_some_and(|pending| pending.as_raw() == bal.as_raw());
285            if pending_matches {
286                self.pending.remove(key);
287                self.canonical_pending.remove(key);
288            }
289        }
290    }
291
292    fn prune(&mut self, retention_distance: u64, tip: BlockNumber) {
293        let keys = self.keys_to_prune(retention_distance, tip);
294        self.remove_keys(&keys);
295    }
296
297    fn remove_keys(&mut self, keys: &[StoredBlockAccessListKey]) -> usize {
298        let mut removed = 0;
299        for key in keys {
300            let block = NumHash::new(key.number(), key.hash());
301            let pending_removed = self.pending.remove(key).is_some();
302            self.canonical_pending.remove(key);
303            let entry_removed = if self
304                .entries
305                .get(&block.hash)
306                .is_some_and(|entry| entry.block_number == block.number)
307            {
308                self.entries.remove(&block.hash).is_some()
309            } else {
310                false
311            };
312
313            if entry_removed {
314                self.remove_hash_from_number(block.number, block.hash);
315            }
316            removed += usize::from(pending_removed || entry_removed);
317        }
318        removed
319    }
320
321    fn remove_hash_from_number(&mut self, block_number: BlockNumber, block_hash: BlockHash) {
322        let empty = self.hashes_by_number.get_mut(&block_number).is_some_and(|hashes| {
323            hashes.retain(|hash| *hash != block_hash);
324            hashes.is_empty()
325        });
326        if empty {
327            self.hashes_by_number.remove(&block_number);
328        }
329    }
330}
331
332/// Buffered BAL entry with its block number.
333#[derive(Debug)]
334struct RocksDBBalEntry {
335    /// Block number for this hash-indexed BAL.
336    block_number: BlockNumber,
337    /// Raw BAL payload.
338    bal: RawBal,
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::providers::RocksDBBuilder;
345    use alloy_primitives::B256;
346
347    fn test_store() -> (tempfile::TempDir, RocksDBBalStore) {
348        let dir = tempfile::tempdir().unwrap();
349        let rocksdb = RocksDBBuilder::new(dir.path()).with_default_tables().build().unwrap();
350        (dir, RocksDBBalStore::new(rocksdb))
351    }
352
353    fn disk_bal(store: &RocksDBBalStore, block: NumHash) -> Option<Bytes> {
354        store
355            .rocksdb_provider()
356            .get_raw::<tables::BlockAccessLists>(StoredBlockAccessListKey::new(
357                block.number,
358                block.hash,
359            ))
360            .unwrap()
361            .map(|value| StoredBlockAccessList::decompress(&value).unwrap().into_raw())
362    }
363
364    fn read_many(store: &RocksDBBalStore, blocks: &[NumHash]) -> Vec<Option<Bytes>> {
365        blocks.iter().map(|block| store.get_by_hash(block.hash).unwrap()).collect()
366    }
367
368    #[test]
369    fn flush_prunes_buffer_retention() {
370        let (_dir, store) = test_store();
371        let old = NumHash::new(1, B256::with_last_byte(1));
372        let retained =
373            NumHash::new(DEFAULT_BAL_BUFFER_RETENTION_DISTANCE + 2, B256::with_last_byte(2));
374        let old_bal = Bytes::from_static(&[0xc1, 0x01]);
375        let retained_bal = Bytes::from_static(&[0xc1, 0x02]);
376
377        store.insert(old, RawBal::from(old_bal.clone())).unwrap();
378        store.insert(retained, RawBal::from(retained_bal.clone())).unwrap();
379
380        assert_eq!(
381            store.get_by_hashes(&[old.hash, retained.hash]).unwrap(),
382            vec![Some(old_bal), Some(retained_bal.clone())]
383        );
384
385        store.flush(&[retained]).unwrap();
386
387        assert_eq!(
388            store.get_by_hashes(&[old.hash, retained.hash]).unwrap(),
389            vec![None, Some(retained_bal)]
390        );
391        assert_eq!(disk_bal(&store, old), None);
392    }
393
394    #[test]
395    fn flush_prunes_only_durable_cache_entries() {
396        let (_dir, store) = test_store();
397        let old = NumHash::new(1, B256::with_last_byte(1));
398        let tip = NumHash::new(DEFAULT_BAL_BUFFER_RETENTION_DISTANCE + 2, B256::with_last_byte(2));
399        let old_bal = Bytes::from_static(&[0xc1, 0x01]);
400
401        store.insert(old, RawBal::from(old_bal.clone())).unwrap();
402        store.flush(&[old]).unwrap();
403        store.flush(&[tip]).unwrap();
404
405        assert!(!store.buffer.read().entries.contains_key(&old.hash));
406        assert_eq!(disk_bal(&store, old), Some(old_bal.clone()));
407        assert_eq!(store.get_by_hash(old.hash).unwrap(), Some(old_bal));
408    }
409
410    #[test]
411    fn configured_buffer_retention_distance_is_used() {
412        let dir = tempfile::tempdir().unwrap();
413        let rocksdb = RocksDBBuilder::new(dir.path()).with_default_tables().build().unwrap();
414        let store = RocksDBBalStore::with_buffer_retention_distance(rocksdb, 64);
415        let old = NumHash::new(1, B256::with_last_byte(1));
416        let tip = NumHash::new(34, B256::with_last_byte(2));
417
418        store.insert(old, RawBal::from(Bytes::from_static(&[0xc1, 0x01]))).unwrap();
419        store.flush(&[old]).unwrap();
420        store.flush(&[tip]).unwrap();
421
422        assert!(store.buffer.read().entries.contains_key(&old.hash));
423    }
424
425    #[test]
426    fn flush_writes_only_requested_pending_bals() {
427        let (_dir, store) = test_store();
428        let block_1 = NumHash::new(1, B256::with_last_byte(1));
429        let block_1_fork = NumHash::new(1, B256::with_last_byte(9));
430        let block_2 = NumHash::new(2, B256::with_last_byte(2));
431        let bal_1 = Bytes::from_static(&[0xc1, 0x01]);
432        let bal_1_fork = Bytes::from_static(&[0xc1, 0x09]);
433        let bal_2 = Bytes::from_static(&[0xc1, 0x02]);
434
435        store.insert(block_1, RawBal::from(bal_1.clone())).unwrap();
436        store.insert(block_1_fork, RawBal::from(bal_1_fork.clone())).unwrap();
437        store.insert(block_2, RawBal::from(bal_2.clone())).unwrap();
438
439        store.flush(&[block_1]).unwrap();
440
441        assert_eq!(disk_bal(&store, block_1), Some(bal_1.clone()));
442        assert_eq!(disk_bal(&store, block_1_fork), None);
443        assert_eq!(disk_bal(&store, block_2), None);
444        assert_eq!(
445            store.get_by_hashes(&[block_1.hash, block_1_fork.hash, block_2.hash]).unwrap(),
446            vec![Some(bal_1.clone()), Some(bal_1_fork), Some(bal_2)]
447        );
448
449        let store_with_empty_buffer = RocksDBBalStore::new(store.rocksdb_provider().clone());
450        assert_eq!(
451            store_with_empty_buffer
452                .get_by_hashes(&[block_1.hash, block_1_fork.hash, block_2.hash])
453                .unwrap(),
454            vec![Some(bal_1), None, None]
455        );
456    }
457
458    #[test]
459    fn missing_and_empty_bal_are_distinct() {
460        let (_dir, store) = test_store();
461        let empty_hash = B256::with_last_byte(1);
462        let missing_hash = B256::with_last_byte(2);
463        let empty_bal = Bytes::from_static(&[0xc0]);
464
465        store.insert(NumHash::new(1, empty_hash), RawBal::from(empty_bal.clone())).unwrap();
466
467        assert_eq!(
468            read_many(&store, &[NumHash::new(1, empty_hash), NumHash::new(1, missing_hash)]),
469            vec![Some(empty_bal), None]
470        );
471    }
472
473    #[test]
474    fn prune_uses_eip_retention() {
475        let (_dir, store) = test_store();
476        let old_hash = B256::with_last_byte(1);
477        let retained_hash = B256::with_last_byte(2);
478        let retained_bal = Bytes::from_static(&[0xc1, 0x02]);
479        let tip = BAL_RETENTION_PERIOD_SLOTS + 2;
480
481        store
482            .insert(NumHash::new(1, old_hash), RawBal::from(Bytes::from_static(&[0xc1, 0x01])))
483            .unwrap();
484        store.insert(NumHash::new(2, retained_hash), RawBal::from(retained_bal.clone())).unwrap();
485        store.flush(&[NumHash::new(1, old_hash), NumHash::new(2, retained_hash)]).unwrap();
486
487        assert_eq!(store.prune(tip).unwrap(), 1);
488        assert_eq!(disk_bal(&store, NumHash::new(1, old_hash)), None);
489        assert_eq!(disk_bal(&store, NumHash::new(2, retained_hash)), Some(retained_bal.clone()));
490        assert_eq!(
491            read_many(&store, &[NumHash::new(1, old_hash), NumHash::new(2, retained_hash)]),
492            vec![None, Some(retained_bal)]
493        );
494    }
495
496    #[test]
497    fn stored_payload_hash_is_not_reverified() {
498        let (_dir, store) = test_store();
499        let block = NumHash::new(1, B256::with_last_byte(1));
500        let key = StoredBlockAccessListKey::new(block.number, block.hash);
501        let value = StoredBlockAccessList::new_unchecked(B256::ZERO, Bytes::from_static(&[0xc0]));
502
503        store.rocksdb_provider().put::<tables::BlockAccessLists>(key, &value).unwrap();
504        store
505            .rocksdb_provider()
506            .put::<tables::BlockAccessListBlockNumbers>(block.hash, &block.number)
507            .unwrap();
508
509        assert_eq!(store.get_by_hash(block.hash).unwrap(), Some(Bytes::from_static(&[0xc0])));
510    }
511
512    #[test]
513    fn canonical_pending_entries_are_retried() {
514        let mut buffer = RocksDBBalStoreBuffer::default();
515        let first = NumHash::new(1, B256::with_last_byte(1));
516        let second = NumHash::new(2, B256::with_last_byte(2));
517        buffer.insert(first, RawBal::from(Bytes::from_static(&[0xc1, 0x01])));
518        buffer.mark_canonical(&[first]);
519
520        assert_eq!(buffer.canonical_pending_entries().len(), 1);
521
522        // A failed flush leaves the canonical entry pending for the next attempt.
523        buffer.insert(second, RawBal::from(Bytes::from_static(&[0xc1, 0x02])));
524        buffer.mark_canonical(&[second]);
525        let retry = buffer.canonical_pending_entries();
526
527        assert_eq!(
528            retry.iter().map(|(key, _)| NumHash::new(key.number(), key.hash())).collect::<Vec<_>>(),
529            vec![first, second]
530        );
531    }
532
533    #[test]
534    fn read_only_legacy_database_has_no_persisted_bals() {
535        let dir = tempfile::tempdir().unwrap();
536        let path = dir.path().join("db");
537        let hash = B256::with_last_byte(1);
538        let rocksdb = RocksDBBuilder::new(&path)
539            .with_table::<tables::TransactionHashNumbers>()
540            .with_table::<tables::AccountsHistory>()
541            .with_table::<tables::StoragesHistory>()
542            .build()
543            .unwrap();
544        rocksdb.put::<tables::TransactionHashNumbers>(hash, &42).unwrap();
545        drop(rocksdb);
546
547        let rocksdb = RocksDBBuilder::new(&path)
548            .with_default_tables()
549            .with_table::<tables::BlockAccessLists>()
550            .with_table::<tables::BlockAccessListBlockNumbers>()
551            .with_read_only(true)
552            .build()
553            .unwrap();
554        assert_eq!(rocksdb.get::<tables::TransactionHashNumbers>(hash).unwrap(), Some(42));
555        let store = RocksDBBalStore::new(rocksdb);
556        assert_eq!(store.get_by_hash(hash).unwrap(), None);
557
558        let raw = Bytes::from_static(&[0xc0]);
559        store.insert(NumHash::new(42, hash), RawBal::from(raw.clone())).unwrap();
560        assert_eq!(store.get_by_hash(hash).unwrap(), Some(raw));
561        drop(store);
562
563        // The secondary open must not add tables to the primary database.
564        let rocksdb = RocksDBBuilder::new(&path)
565            .with_table::<tables::TransactionHashNumbers>()
566            .with_table::<tables::AccountsHistory>()
567            .with_table::<tables::StoragesHistory>()
568            .build()
569            .unwrap();
570        assert!(!rocksdb.has_table::<tables::BlockAccessLists>());
571        assert!(!rocksdb.has_table::<tables::BlockAccessListBlockNumbers>());
572    }
573
574    #[test]
575    fn read_only_database_reads_persisted_bals() {
576        let dir = tempfile::tempdir().unwrap();
577        let path = dir.path().join("db");
578        let rocksdb = RocksDBBuilder::new(&path)
579            .with_default_tables()
580            .with_table::<tables::BlockAccessLists>()
581            .with_table::<tables::BlockAccessListBlockNumbers>()
582            .build()
583            .unwrap();
584        let store = RocksDBBalStore::new(rocksdb);
585        let block = NumHash::new(42, B256::with_last_byte(1));
586        let raw = Bytes::from_static(&[0xc0]);
587        store.insert(block, RawBal::from(raw.clone())).unwrap();
588        store.flush(&[block]).unwrap();
589        drop(store);
590
591        let rocksdb = RocksDBBuilder::new(&path)
592            .with_default_tables()
593            .with_table::<tables::BlockAccessLists>()
594            .with_table::<tables::BlockAccessListBlockNumbers>()
595            .with_read_only(true)
596            .build()
597            .unwrap();
598        let store = RocksDBBalStore::new(rocksdb);
599        assert_eq!(store.get_by_hash(block.hash).unwrap(), Some(raw));
600    }
601}