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
19const DEFAULT_BAL_BUFFER_RETENTION_DISTANCE: u64 = 32;
21
22#[derive(Clone)]
29pub struct RocksDBBalStore {
30 buffer_retention_distance: u64,
32 rocksdb: RocksDBProvider,
34 buffer: Arc<RwLock<RocksDBBalStoreBuffer>>,
36}
37
38impl RocksDBBalStore {
39 pub fn new(rocksdb: RocksDBProvider) -> Self {
41 Self::with_buffer_retention_distance(rocksdb, DEFAULT_BAL_BUFFER_RETENTION_DISTANCE)
42 }
43
44 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 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#[derive(Debug, Default)]
213struct RocksDBBalStoreBuffer {
214 entries: B256Map<RocksDBBalEntry>,
216 hashes_by_number: BTreeMap<BlockNumber, Vec<BlockHash>>,
218 pending: BTreeMap<StoredBlockAccessListKey, RawBal>,
220 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 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 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 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#[derive(Debug)]
334struct RocksDBBalEntry {
335 block_number: BlockNumber,
337 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 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 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}