Skip to content

Commit ddb3819

Browse files
0xKarl98mattsse
andauthored
feat(storage): add in-memory BAL retention (#23873)
Co-authored-by: Matthias Seitz <[email protected]>
1 parent 30fe86d commit ddb3819

5 files changed

Lines changed: 166 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ alloy-sol-types = { version = "1.5.6", default-features = false }
449449

450450
alloy-chains = { version = "0.2.33", default-features = false }
451451
alloy-eip2124 = { version = "0.2.0", default-features = false }
452-
alloy-eip7928 = { version = "0.3.4", default-features = false }
452+
alloy-eip7928 = { version = "0.3.5", default-features = false }
453453
alloy-evm = { version = "0.34.0", default-features = false }
454454
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
455455
alloy-trie = { version = "0.9.4", default-features = false }

crates/storage/provider/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ reth-static-file-types = { workspace = true, features = ["std"] }
3535
reth-fs-util.workspace = true
3636

3737
# ethereum
38+
alloy-eip7928.workspace = true
3839
alloy-eips.workspace = true
3940
alloy-genesis.workspace = true
4041
alloy-primitives.workspace = true

crates/storage/provider/src/bal.rs

Lines changed: 160 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,129 @@
1+
use alloy_eip7928::BAL_RETENTION_PERIOD_SLOTS;
12
use alloy_primitives::{BlockHash, BlockNumber, Bytes};
23
use parking_lot::RwLock;
4+
use reth_prune_types::PruneMode;
35
use reth_storage_api::{BalStore, GetBlockAccessListLimit};
46
use reth_storage_errors::provider::ProviderResult;
5-
use std::{collections::HashMap, sync::Arc};
7+
use std::{
8+
collections::{BTreeMap, HashMap},
9+
sync::Arc,
10+
};
611

712
/// Basic in-memory BAL store keyed by block hash.
8-
#[derive(Debug, Clone, Default)]
13+
#[derive(Debug, Clone)]
914
pub struct InMemoryBalStore {
10-
entries: Arc<RwLock<HashMap<BlockHash, Bytes>>>,
15+
config: BalConfig,
16+
inner: Arc<RwLock<InMemoryBalStoreInner>>,
17+
}
18+
19+
impl InMemoryBalStore {
20+
/// Creates a new in-memory BAL store with the given config.
21+
pub fn new(config: BalConfig) -> Self {
22+
Self { config, inner: Arc::new(RwLock::new(InMemoryBalStoreInner::default())) }
23+
}
24+
}
25+
26+
impl Default for InMemoryBalStore {
27+
fn default() -> Self {
28+
Self::new(BalConfig::default())
29+
}
30+
}
31+
32+
/// Configuration for BAL storage.
33+
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
34+
pub struct BalConfig {
35+
/// Retention policy for BALs kept in memory.
36+
in_memory_retention: Option<PruneMode>,
37+
}
38+
39+
impl BalConfig {
40+
/// Returns a config with no in-memory BAL retention limit.
41+
pub const fn unbounded() -> Self {
42+
Self { in_memory_retention: None }
43+
}
44+
45+
/// Returns a config with the given in-memory BAL retention policy.
46+
pub const fn with_in_memory_retention(in_memory_retention: PruneMode) -> Self {
47+
Self { in_memory_retention: Some(in_memory_retention) }
48+
}
49+
}
50+
51+
impl Default for BalConfig {
52+
fn default() -> Self {
53+
Self::with_in_memory_retention(PruneMode::Distance(BAL_RETENTION_PERIOD_SLOTS))
54+
}
55+
}
56+
57+
#[derive(Debug, Default)]
58+
struct InMemoryBalStoreInner {
59+
entries: HashMap<BlockHash, BalEntry>,
60+
hashes_by_number: BTreeMap<BlockNumber, Vec<BlockHash>>,
61+
highest_block_number: Option<BlockNumber>,
62+
}
63+
64+
impl InMemoryBalStoreInner {
65+
// Inserts a BAL and keeps the block-number index in sync.
66+
fn insert(&mut self, block_hash: BlockHash, block_number: BlockNumber, bal: Bytes) {
67+
let empty_block_number =
68+
self.entries.insert(block_hash, BalEntry { block_number, bal }).and_then(|entry| {
69+
let hashes = self.hashes_by_number.get_mut(&entry.block_number)?;
70+
hashes.retain(|hash| *hash != block_hash);
71+
hashes.is_empty().then_some(entry.block_number)
72+
});
73+
74+
if let Some(block_number) = empty_block_number {
75+
self.hashes_by_number.remove(&block_number);
76+
}
77+
78+
self.hashes_by_number.entry(block_number).or_default().push(block_hash);
79+
self.highest_block_number = Some(
80+
self.highest_block_number.map_or(block_number, |highest| highest.max(block_number)),
81+
);
82+
}
83+
84+
// Removes BALs outside the configured retention window.
85+
fn prune(&mut self, prune_mode: Option<PruneMode>) {
86+
let Some(prune_mode) = prune_mode else { return };
87+
let Some(tip) = self.highest_block_number else { return };
88+
89+
while let Some((&block_number, _)) = self.hashes_by_number.first_key_value() {
90+
if !prune_mode.should_prune(block_number, tip) {
91+
break
92+
}
93+
94+
let Some((_, hashes)) = self.hashes_by_number.pop_first() else { break };
95+
for hash in hashes {
96+
self.entries.remove(&hash);
97+
}
98+
}
99+
}
100+
}
101+
102+
#[derive(Debug)]
103+
struct BalEntry {
104+
block_number: BlockNumber,
105+
bal: Bytes,
11106
}
12107

13108
impl BalStore for InMemoryBalStore {
14109
fn insert(
15110
&self,
16111
block_hash: BlockHash,
17-
_block_number: BlockNumber,
112+
block_number: BlockNumber,
18113
bal: Bytes,
19114
) -> ProviderResult<()> {
20-
self.entries.write().insert(block_hash, bal);
115+
let mut inner = self.inner.write();
116+
inner.insert(block_hash, block_number, bal);
117+
inner.prune(self.config.in_memory_retention);
21118
Ok(())
22119
}
23120

24121
fn get_by_hashes(&self, block_hashes: &[BlockHash]) -> ProviderResult<Vec<Option<Bytes>>> {
25-
let entries = self.entries.read();
122+
let inner = self.inner.read();
26123
let mut result = Vec::with_capacity(block_hashes.len());
27124

28125
for hash in block_hashes {
29-
result.push(entries.get(hash).cloned());
126+
result.push(inner.entries.get(hash).map(|entry| entry.bal.clone()));
30127
}
31128

32129
Ok(result)
@@ -38,11 +135,15 @@ impl BalStore for InMemoryBalStore {
38135
limit: GetBlockAccessListLimit,
39136
out: &mut Vec<Bytes>,
40137
) -> ProviderResult<()> {
41-
let entries = self.entries.read();
138+
let inner = self.inner.read();
42139
let mut size = 0;
43140

44141
for hash in block_hashes {
45-
let bal = entries.get(hash).cloned().unwrap_or_else(|| Bytes::from_static(&[0xc0]));
142+
let bal = inner
143+
.entries
144+
.get(hash)
145+
.map(|entry| entry.bal.clone())
146+
.unwrap_or_else(|| Bytes::from_static(&[0xc0]));
46147
size += bal.len();
47148
out.push(bal);
48149

@@ -106,4 +207,54 @@ mod tests {
106207

107208
assert_eq!(limited, vec![bal0, bal1]);
108209
}
210+
211+
#[test]
212+
fn default_retention_prunes_old_bals() {
213+
let store = InMemoryBalStore::default();
214+
let old_hash = B256::random();
215+
let retained_hash = B256::random();
216+
let tip_hash = B256::random();
217+
let old_bal = Bytes::from_static(b"old");
218+
let retained_bal = Bytes::from_static(b"retained");
219+
let tip_bal = Bytes::from_static(b"tip");
220+
221+
store.insert(old_hash, 1, old_bal).unwrap();
222+
store.insert(retained_hash, BAL_RETENTION_PERIOD_SLOTS, retained_bal.clone()).unwrap();
223+
store.insert(tip_hash, BAL_RETENTION_PERIOD_SLOTS + 2, tip_bal.clone()).unwrap();
224+
225+
assert_eq!(
226+
store.get_by_hashes(&[old_hash, retained_hash, tip_hash]).unwrap(),
227+
vec![None, Some(retained_bal), Some(tip_bal)]
228+
);
229+
}
230+
231+
#[test]
232+
fn unbounded_retention_keeps_old_bals() {
233+
let store = InMemoryBalStore::new(BalConfig::unbounded());
234+
let old_hash = B256::random();
235+
let tip_hash = B256::random();
236+
let old_bal = Bytes::from_static(b"old");
237+
let tip_bal = Bytes::from_static(b"tip");
238+
239+
store.insert(old_hash, 1, old_bal.clone()).unwrap();
240+
store.insert(tip_hash, BAL_RETENTION_PERIOD_SLOTS + 1, tip_bal.clone()).unwrap();
241+
242+
assert_eq!(
243+
store.get_by_hashes(&[old_hash, tip_hash]).unwrap(),
244+
vec![Some(old_bal), Some(tip_bal)]
245+
);
246+
}
247+
248+
#[test]
249+
fn reinserting_hash_updates_number_index() {
250+
let store =
251+
InMemoryBalStore::new(BalConfig::with_in_memory_retention(PruneMode::Before(2)));
252+
let hash = B256::random();
253+
let bal = Bytes::from_static(b"bal");
254+
255+
store.insert(hash, 1, Bytes::from_static(b"old")).unwrap();
256+
store.insert(hash, 2, bal.clone()).unwrap();
257+
258+
assert_eq!(store.get_by_hashes(&[hash]).unwrap(), vec![Some(bal)]);
259+
}
109260
}

crates/storage/provider/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub mod either_writer;
3939
pub use either_writer::*;
4040

4141
mod bal;
42-
pub use bal::InMemoryBalStore;
42+
pub use bal::{BalConfig, InMemoryBalStore};
4343

4444
pub use reth_chain_state::{
4545
CanonStateNotification, CanonStateNotificationSender, CanonStateNotificationStream,

0 commit comments

Comments
 (0)