Skip to main content

ethrex_storage/
trie.rs

1use crate::api::tables::{
2    ACCOUNT_FLATKEYVALUE, ACCOUNT_TRIE_NODES, STORAGE_FLATKEYVALUE, STORAGE_TRIE_NODES,
3};
4use crate::api::{StorageBackend, StorageLockedView, StorageReadView};
5use crate::error::StoreError;
6use crate::layering::apply_prefix;
7use ethrex_common::H256;
8use ethrex_trie::{Nibbles, TrieDB, error::TrieError};
9use std::sync::Arc;
10
11/// TrieDB implementation that holds a pre-acquired read view for the entire
12/// trie traversal, avoiding per-node-lookup allocation and lock acquisition.
13pub struct BackendTrieDB {
14    /// Reference to the storage backend (used only for writes)
15    db: Arc<dyn StorageBackend>,
16    /// Pre-acquired read view held for the lifetime of this struct.
17    /// Using Arc allows sharing a single read view across multiple BackendTrieDB
18    /// instances (e.g., state trie + storage trie in a single query).
19    read_view: Arc<dyn StorageReadView>,
20    /// Last flatkeyvalue path already generated
21    last_computed_flatkeyvalue: Nibbles,
22    nodes_table: &'static str,
23    fkv_table: &'static str,
24    /// Storage trie address prefix (for storage tries)
25    /// None for state tries, Some(address) for storage tries
26    address_prefix: Option<H256>,
27}
28
29impl BackendTrieDB {
30    /// Create a new BackendTrieDB for the account trie
31    pub fn new_for_accounts(
32        db: Arc<dyn StorageBackend>,
33        last_written: Vec<u8>,
34    ) -> Result<Self, StoreError> {
35        let read_view = db.begin_read()?;
36        Self::new_for_accounts_with_view(db, read_view, last_written)
37    }
38
39    /// Create a new BackendTrieDB for the account trie with a shared read view
40    pub fn new_for_accounts_with_view(
41        db: Arc<dyn StorageBackend>,
42        read_view: Arc<dyn StorageReadView>,
43        last_written: Vec<u8>,
44    ) -> Result<Self, StoreError> {
45        let last_computed_flatkeyvalue = Nibbles::from_hex(last_written);
46        Ok(Self {
47            db,
48            read_view,
49            last_computed_flatkeyvalue,
50            nodes_table: ACCOUNT_TRIE_NODES,
51            fkv_table: ACCOUNT_FLATKEYVALUE,
52            address_prefix: None,
53        })
54    }
55
56    /// Create a new BackendTrieDB for the storage tries
57    pub fn new_for_storages(
58        db: Arc<dyn StorageBackend>,
59        last_written: Vec<u8>,
60    ) -> Result<Self, StoreError> {
61        let read_view = db.begin_read()?;
62        Self::new_for_storages_with_view(db, read_view, last_written)
63    }
64
65    /// Create a new BackendTrieDB for the storage tries with a shared read view
66    pub fn new_for_storages_with_view(
67        db: Arc<dyn StorageBackend>,
68        read_view: Arc<dyn StorageReadView>,
69        last_written: Vec<u8>,
70    ) -> Result<Self, StoreError> {
71        let last_computed_flatkeyvalue = Nibbles::from_hex(last_written);
72        Ok(Self {
73            db,
74            read_view,
75            last_computed_flatkeyvalue,
76            nodes_table: STORAGE_TRIE_NODES,
77            fkv_table: STORAGE_FLATKEYVALUE,
78            address_prefix: None,
79        })
80    }
81
82    /// Create a new BackendTrieDB for a specific storage trie
83    pub fn new_for_account_storage(
84        db: Arc<dyn StorageBackend>,
85        address_prefix: H256,
86        last_written: Vec<u8>,
87    ) -> Result<Self, StoreError> {
88        let read_view = db.begin_read()?;
89        Self::new_for_account_storage_with_view(db, read_view, address_prefix, last_written)
90    }
91
92    /// Create a new BackendTrieDB for a specific storage trie with a shared read view
93    pub fn new_for_account_storage_with_view(
94        db: Arc<dyn StorageBackend>,
95        read_view: Arc<dyn StorageReadView>,
96        address_prefix: H256,
97        last_written: Vec<u8>,
98    ) -> Result<Self, StoreError> {
99        let last_computed_flatkeyvalue = Nibbles::from_hex(last_written);
100        Ok(Self {
101            db,
102            read_view,
103            last_computed_flatkeyvalue,
104            nodes_table: STORAGE_TRIE_NODES,
105            fkv_table: STORAGE_FLATKEYVALUE,
106            address_prefix: Some(address_prefix),
107        })
108    }
109
110    fn make_key(&self, path: Nibbles) -> Vec<u8> {
111        apply_prefix(self.address_prefix, path).into_vec()
112    }
113
114    /// Key might be for an account or storage slot
115    fn table_for_key(&self, key: &[u8]) -> &'static str {
116        let (is_leaf, _) = classify_trie_key(key.len());
117        if is_leaf {
118            self.fkv_table
119        } else {
120            self.nodes_table
121        }
122    }
123}
124
125/// Classifies an on-disk trie/flat-KV key by its length.
126///
127/// Returns `(is_leaf, is_account)`:
128/// - `is_leaf = true` iff the key is a flat-KV leaf (account leaf at 65 bytes, storage leaf at 131 bytes).
129/// - `is_account = true` iff the key targets account-space (length <= 65: account trie nodes and account leaves).
130///
131/// Used by every CF-dispatch site (write loop in `commit_to_disk`, read dispatch
132/// in `BackendTrieDBLocked::tx_for_key`, table selection in
133/// `BackendTrieDB::table_for_key`). Centralised here so a future change to the key
134/// layout only needs to touch one place.
135pub fn classify_trie_key(key_len: usize) -> (bool, bool) {
136    let is_leaf = key_len == 65 || key_len == 131;
137    let is_account = key_len <= 65;
138    (is_leaf, is_account)
139}
140
141impl TrieDB for BackendTrieDB {
142    fn flatkeyvalue_computed(&self, key: Nibbles) -> bool {
143        let key = apply_prefix(self.address_prefix, key);
144        self.last_computed_flatkeyvalue >= key
145    }
146
147    fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
148        let prefixed_key = self.make_key(key);
149        let table = self.table_for_key(&prefixed_key);
150        self.read_view
151            .get(table, prefixed_key.as_ref())
152            .map_err(|e| TrieError::DbError(anyhow::anyhow!("Failed to get from database: {}", e)))
153    }
154
155    fn multi_get(&self, keys: &[Nibbles]) -> Vec<Result<Option<Vec<u8>>, TrieError>> {
156        // Compute the prefixed key and target table for every input key,
157        // preserving the original index so results can be scattered back
158        // in input order after being partitioned by table.
159        let prefixed_keys: Vec<Vec<u8>> = keys.iter().map(|k| self.make_key(k.clone())).collect();
160
161        // table -> (original index, key bytes)
162        let mut by_table: std::collections::HashMap<&'static str, Vec<(usize, &[u8])>> =
163            std::collections::HashMap::new();
164        for (idx, key) in prefixed_keys.iter().enumerate() {
165            let table = self.table_for_key(key);
166            by_table.entry(table).or_default().push((idx, key.as_ref()));
167        }
168
169        let mut results: Vec<Option<Result<Option<Vec<u8>>, TrieError>>> =
170            (0..keys.len()).map(|_| None).collect();
171
172        for (table, entries) in by_table {
173            let (indices, refs): (Vec<usize>, Vec<&[u8]>) = entries.into_iter().unzip();
174            let table_results = self.read_view.multi_get(table, &refs);
175            for (idx, res) in indices.into_iter().zip(table_results) {
176                results[idx] = Some(res.map_err(|e| {
177                    TrieError::DbError(anyhow::anyhow!("Failed to get from database: {}", e))
178                }));
179            }
180        }
181
182        results
183            .into_iter()
184            .map(|r| r.expect("every key is assigned to exactly one table"))
185            .collect()
186    }
187
188    fn put_batch(&self, key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
189        let mut tx = self.db.begin_write().map_err(|e| {
190            TrieError::DbError(anyhow::anyhow!("Failed to begin write transaction: {}", e))
191        })?;
192        for (key, value) in key_values {
193            let prefixed_key = self.make_key(key);
194            let table = self.table_for_key(&prefixed_key);
195            tx.put_batch(table, vec![(prefixed_key, value)])
196                .map_err(|e| TrieError::DbError(anyhow::anyhow!("Failed to write batch: {}", e)))?;
197        }
198        tx.commit()
199            .map_err(|e| TrieError::DbError(anyhow::anyhow!("Failed to write batch: {}", e)))
200    }
201}
202
203/// Read-only version with persistent locked transaction/snapshot for batch reads
204pub struct BackendTrieDBLocked {
205    account_trie_tx: Box<dyn StorageLockedView>,
206    storage_trie_tx: Box<dyn StorageLockedView>,
207    account_fkv_tx: Box<dyn StorageLockedView>,
208    storage_fkv_tx: Box<dyn StorageLockedView>,
209    /// Last flatkeyvalue path already generated
210    last_computed_flatkeyvalue: Nibbles,
211}
212
213impl BackendTrieDBLocked {
214    pub fn new(engine: &dyn StorageBackend, last_written: Vec<u8>) -> Result<Self, StoreError> {
215        let last_computed_flatkeyvalue = Nibbles::from_hex(last_written);
216        let account_trie_tx = engine.begin_locked(ACCOUNT_TRIE_NODES)?;
217        let storage_trie_tx = engine.begin_locked(STORAGE_TRIE_NODES)?;
218        let account_fkv_tx = engine.begin_locked(ACCOUNT_FLATKEYVALUE)?;
219        let storage_fkv_tx = engine.begin_locked(STORAGE_FLATKEYVALUE)?;
220        Ok(Self {
221            account_trie_tx,
222            storage_trie_tx,
223            account_fkv_tx,
224            storage_fkv_tx,
225            last_computed_flatkeyvalue,
226        })
227    }
228
229    /// Key is already prefixed
230    fn tx_for_key(&self, key: &Nibbles) -> &dyn StorageLockedView {
231        let (is_leaf, is_account) = classify_trie_key(key.len());
232        if is_leaf {
233            if is_account {
234                &*self.account_fkv_tx
235            } else {
236                &*self.storage_fkv_tx
237            }
238        } else if is_account {
239            &*self.account_trie_tx
240        } else {
241            &*self.storage_trie_tx
242        }
243    }
244}
245
246impl TrieDB for BackendTrieDBLocked {
247    fn flatkeyvalue_computed(&self, key: Nibbles) -> bool {
248        self.last_computed_flatkeyvalue >= key
249    }
250
251    fn get(&self, key: Nibbles) -> Result<Option<Vec<u8>>, TrieError> {
252        let tx = self.tx_for_key(&key);
253        tx.get(key.as_ref())
254            .map_err(|e| TrieError::DbError(anyhow::anyhow!("Failed to get from database: {}", e)))
255    }
256
257    fn put_batch(&self, _key_values: Vec<(Nibbles, Vec<u8>)>) -> Result<(), TrieError> {
258        // Read-only locked storage, should not be used for puts
259        Err(TrieError::DbError(anyhow::anyhow!("trie is read-only")))
260    }
261}