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 inserts_and_reads_by_hash() {
370        let (_dir, store) = test_store();
371        let hash = B256::random();
372        let missing = NumHash::new(1, B256::random());
373        let bal = Bytes::from_static(&[0xc1, 0x01]);
374
375        store.insert(NumHash::new(1, hash), RawBal::from(bal.clone())).unwrap();
376
377        assert_eq!(read_many(&store, &[NumHash::new(1, hash), missing]), vec![Some(bal), None]);
378    }
379
380    #[test]
381    fn hash_lookup_reads_persisted_bal_through_store() {
382        let (_dir, store) = test_store();
383        let block = NumHash::new(1, B256::random());
384        let bal = Bytes::from_static(&[0xc1, 0x01]);
385
386        store.insert(block, RawBal::from(bal.clone())).unwrap();
387        store.flush(&[block]).unwrap();
388
389        let store_with_empty_buffer = RocksDBBalStore::new(store.rocksdb_provider().clone());
390        assert_eq!(store_with_empty_buffer.get_by_hash(block.hash).unwrap(), Some(bal));
391    }
392
393    #[test]
394    fn flush_prunes_buffer_retention() {
395        let (_dir, store) = test_store();
396        let old = NumHash::new(1, B256::with_last_byte(1));
397        let retained =
398            NumHash::new(DEFAULT_BAL_BUFFER_RETENTION_DISTANCE + 2, B256::with_last_byte(2));
399        let old_bal = Bytes::from_static(&[0xc1, 0x01]);
400        let retained_bal = Bytes::from_static(&[0xc1, 0x02]);
401
402        store.insert(old, RawBal::from(old_bal.clone())).unwrap();
403        store.insert(retained, RawBal::from(retained_bal.clone())).unwrap();
404
405        assert_eq!(
406            store.get_by_hashes(&[old.hash, retained.hash]).unwrap(),
407            vec![Some(old_bal), Some(retained_bal.clone())]
408        );
409
410        store.flush(&[retained]).unwrap();
411
412        assert_eq!(
413            store.get_by_hashes(&[old.hash, retained.hash]).unwrap(),
414            vec![None, Some(retained_bal)]
415        );
416        assert_eq!(disk_bal(&store, old), None);
417    }
418
419    #[test]
420    fn flush_prunes_only_durable_cache_entries() {
421        let (_dir, store) = test_store();
422        let old = NumHash::new(1, B256::with_last_byte(1));
423        let tip = NumHash::new(DEFAULT_BAL_BUFFER_RETENTION_DISTANCE + 2, B256::with_last_byte(2));
424        let old_bal = Bytes::from_static(&[0xc1, 0x01]);
425
426        store.insert(old, RawBal::from(old_bal.clone())).unwrap();
427        store.flush(&[old]).unwrap();
428        store.flush(&[tip]).unwrap();
429
430        assert!(!store.buffer.read().entries.contains_key(&old.hash));
431        assert_eq!(disk_bal(&store, old), Some(old_bal.clone()));
432        assert_eq!(store.get_by_hash(old.hash).unwrap(), Some(old_bal));
433    }
434
435    #[test]
436    fn configured_buffer_retention_distance_is_used() {
437        let dir = tempfile::tempdir().unwrap();
438        let rocksdb = RocksDBBuilder::new(dir.path()).with_default_tables().build().unwrap();
439        let store = RocksDBBalStore::with_buffer_retention_distance(rocksdb, 64);
440        let old = NumHash::new(1, B256::with_last_byte(1));
441        let tip = NumHash::new(34, B256::with_last_byte(2));
442
443        store.insert(old, RawBal::from(Bytes::from_static(&[0xc1, 0x01]))).unwrap();
444        store.flush(&[old]).unwrap();
445        store.flush(&[tip]).unwrap();
446
447        assert!(store.buffer.read().entries.contains_key(&old.hash));
448    }
449
450    #[test]
451    fn flush_writes_only_requested_pending_bals() {
452        let (_dir, store) = test_store();
453        let block_1 = NumHash::new(1, B256::with_last_byte(1));
454        let block_1_fork = NumHash::new(1, B256::with_last_byte(9));
455        let block_2 = NumHash::new(2, B256::with_last_byte(2));
456        let bal_1 = Bytes::from_static(&[0xc1, 0x01]);
457        let bal_1_fork = Bytes::from_static(&[0xc1, 0x09]);
458        let bal_2 = Bytes::from_static(&[0xc1, 0x02]);
459
460        store.insert(block_1, RawBal::from(bal_1.clone())).unwrap();
461        store.insert(block_1_fork, RawBal::from(bal_1_fork.clone())).unwrap();
462        store.insert(block_2, RawBal::from(bal_2.clone())).unwrap();
463
464        store.flush(&[block_1]).unwrap();
465
466        assert_eq!(disk_bal(&store, block_1), Some(bal_1.clone()));
467        assert_eq!(disk_bal(&store, block_1_fork), None);
468        assert_eq!(disk_bal(&store, block_2), None);
469        assert_eq!(
470            store.get_by_hashes(&[block_1.hash, block_1_fork.hash, block_2.hash]).unwrap(),
471            vec![Some(bal_1.clone()), Some(bal_1_fork), Some(bal_2)]
472        );
473
474        let store_with_empty_buffer = RocksDBBalStore::new(store.rocksdb_provider().clone());
475        assert_eq!(
476            store_with_empty_buffer
477                .get_by_hashes(&[block_1.hash, block_1_fork.hash, block_2.hash])
478                .unwrap(),
479            vec![Some(bal_1), None, None]
480        );
481    }
482
483    #[test]
484    fn sparse_numbers_are_valid() {
485        let (_dir, store) = test_store();
486        let hash_a = B256::with_last_byte(1);
487        let hash_b = B256::with_last_byte(2);
488        let bal_a = Bytes::from_static(&[0xc1, 0x01]);
489        let bal_b = Bytes::from_static(&[0xc1, 0x02]);
490
491        let block_a = NumHash::new(2, hash_a);
492        let block_b = NumHash::new(200, hash_b);
493
494        store.insert(block_a, RawBal::from(bal_a.clone())).unwrap();
495        store.flush(&[block_a]).unwrap();
496        store.insert(block_b, RawBal::from(bal_b.clone())).unwrap();
497        store.flush(&[block_b]).unwrap();
498
499        assert_eq!(read_many(&store, &[block_a, block_b]), vec![Some(bal_a), Some(bal_b)]);
500    }
501
502    #[test]
503    fn missing_and_empty_bal_are_distinct() {
504        let (_dir, store) = test_store();
505        let empty_hash = B256::with_last_byte(1);
506        let missing_hash = B256::with_last_byte(2);
507        let empty_bal = Bytes::from_static(&[0xc0]);
508
509        store.insert(NumHash::new(1, empty_hash), RawBal::from(empty_bal.clone())).unwrap();
510
511        assert_eq!(
512            read_many(&store, &[NumHash::new(1, empty_hash), NumHash::new(1, missing_hash)]),
513            vec![Some(empty_bal), None]
514        );
515    }
516
517    #[test]
518    fn prune_uses_eip_retention() {
519        let (_dir, store) = test_store();
520        let old_hash = B256::with_last_byte(1);
521        let retained_hash = B256::with_last_byte(2);
522        let retained_bal = Bytes::from_static(&[0xc1, 0x02]);
523        let tip = BAL_RETENTION_PERIOD_SLOTS + 2;
524
525        store
526            .insert(NumHash::new(1, old_hash), RawBal::from(Bytes::from_static(&[0xc1, 0x01])))
527            .unwrap();
528        store.insert(NumHash::new(2, retained_hash), RawBal::from(retained_bal.clone())).unwrap();
529        store.flush(&[NumHash::new(1, old_hash), NumHash::new(2, retained_hash)]).unwrap();
530
531        assert_eq!(store.prune(tip).unwrap(), 1);
532        assert_eq!(disk_bal(&store, NumHash::new(1, old_hash)), None);
533        assert_eq!(disk_bal(&store, NumHash::new(2, retained_hash)), Some(retained_bal.clone()));
534        assert_eq!(
535            read_many(&store, &[NumHash::new(1, old_hash), NumHash::new(2, retained_hash)]),
536            vec![None, Some(retained_bal)]
537        );
538    }
539
540    #[test]
541    fn stored_payload_hash_is_not_reverified() {
542        let (_dir, store) = test_store();
543        let block = NumHash::new(1, B256::with_last_byte(1));
544        let key = StoredBlockAccessListKey::new(block.number, block.hash);
545        let value = StoredBlockAccessList::new_unchecked(B256::ZERO, Bytes::from_static(&[0xc0]));
546
547        store.rocksdb_provider().put::<tables::BlockAccessLists>(key, &value).unwrap();
548        store
549            .rocksdb_provider()
550            .put::<tables::BlockAccessListBlockNumbers>(block.hash, &block.number)
551            .unwrap();
552
553        assert_eq!(store.get_by_hash(block.hash).unwrap(), Some(Bytes::from_static(&[0xc0])));
554    }
555
556    #[test]
557    fn canonical_pending_entries_are_retried() {
558        let mut buffer = RocksDBBalStoreBuffer::default();
559        let first = NumHash::new(1, B256::with_last_byte(1));
560        let second = NumHash::new(2, B256::with_last_byte(2));
561        buffer.insert(first, RawBal::from(Bytes::from_static(&[0xc1, 0x01])));
562        buffer.mark_canonical(&[first]);
563
564        assert_eq!(buffer.canonical_pending_entries().len(), 1);
565
566        // A failed flush leaves the canonical entry pending for the next attempt.
567        buffer.insert(second, RawBal::from(Bytes::from_static(&[0xc1, 0x02])));
568        buffer.mark_canonical(&[second]);
569        let retry = buffer.canonical_pending_entries();
570
571        assert_eq!(
572            retry.iter().map(|(key, _)| NumHash::new(key.number(), key.hash())).collect::<Vec<_>>(),
573            vec![first, second]
574        );
575    }
576
577    #[test]
578    fn read_only_legacy_database_has_no_persisted_bals() {
579        let dir = tempfile::tempdir().unwrap();
580        let path = dir.path().join("db");
581        let hash = B256::with_last_byte(1);
582        let rocksdb = RocksDBBuilder::new(&path)
583            .with_table::<tables::TransactionHashNumbers>()
584            .with_table::<tables::AccountsHistory>()
585            .with_table::<tables::StoragesHistory>()
586            .build()
587            .unwrap();
588        rocksdb.put::<tables::TransactionHashNumbers>(hash, &42).unwrap();
589        drop(rocksdb);
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        assert_eq!(rocksdb.get::<tables::TransactionHashNumbers>(hash).unwrap(), Some(42));
599        let store = RocksDBBalStore::new(rocksdb);
600        assert_eq!(store.get_by_hash(hash).unwrap(), None);
601
602        let raw = Bytes::from_static(&[0xc0]);
603        store.insert(NumHash::new(42, hash), RawBal::from(raw.clone())).unwrap();
604        assert_eq!(store.get_by_hash(hash).unwrap(), Some(raw));
605        drop(store);
606
607        // The secondary open must not add tables to the primary database.
608        let rocksdb = RocksDBBuilder::new(&path)
609            .with_table::<tables::TransactionHashNumbers>()
610            .with_table::<tables::AccountsHistory>()
611            .with_table::<tables::StoragesHistory>()
612            .build()
613            .unwrap();
614        assert!(!rocksdb.has_table::<tables::BlockAccessLists>());
615        assert!(!rocksdb.has_table::<tables::BlockAccessListBlockNumbers>());
616    }
617
618    #[test]
619    fn read_only_database_reads_persisted_bals() {
620        let dir = tempfile::tempdir().unwrap();
621        let path = dir.path().join("db");
622        let rocksdb = RocksDBBuilder::new(&path)
623            .with_default_tables()
624            .with_table::<tables::BlockAccessLists>()
625            .with_table::<tables::BlockAccessListBlockNumbers>()
626            .build()
627            .unwrap();
628        let store = RocksDBBalStore::new(rocksdb);
629        let block = NumHash::new(42, B256::with_last_byte(1));
630        let raw = Bytes::from_static(&[0xc0]);
631        store.insert(block, RawBal::from(raw.clone())).unwrap();
632        store.flush(&[block]).unwrap();
633        drop(store);
634
635        let rocksdb = RocksDBBuilder::new(&path)
636            .with_default_tables()
637            .with_table::<tables::BlockAccessLists>()
638            .with_table::<tables::BlockAccessListBlockNumbers>()
639            .with_read_only(true)
640            .build()
641            .unwrap();
642        let store = RocksDBBalStore::new(rocksdb);
643        assert_eq!(store.get_by_hash(block.hash).unwrap(), Some(raw));
644    }
645}