Skip to main content

reth_db/
lib.rs

1//! MDBX implementation for reth's database abstraction layer.
2//!
3//! This crate is an implementation of `reth-db-api` for MDBX, as well as a few other common
4//! database types.
5//!
6//! # Overview
7//!
8//! An overview of the current data model of reth can be found in the [`mod@tables`] module.
9
10#![doc(
11    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
12    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
13    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
14)]
15#![cfg_attr(not(test), warn(unused_crate_dependencies))]
16#![cfg_attr(docsrs, feature(doc_cfg))]
17
18mod implementation;
19pub mod lockfile;
20#[cfg(feature = "mdbx")]
21mod metrics;
22pub mod static_file;
23#[cfg(feature = "mdbx")]
24mod utils;
25pub mod version;
26
27#[cfg(feature = "mdbx")]
28pub mod mdbx;
29
30pub use reth_storage_errors::db::{DatabaseError, DatabaseWriteOperation};
31#[cfg(feature = "mdbx")]
32pub use utils::is_database_empty;
33
34#[cfg(feature = "mdbx")]
35pub use mdbx::{create_db, init_db, open_db, open_db_read_only, DatabaseEnv, DatabaseEnvKind};
36
37pub use models::ClientVersion;
38pub use reth_db_api::*;
39
40/// Collection of database test utilities
41#[cfg(any(test, feature = "test-utils"))]
42pub mod test_utils {
43    use super::*;
44    use crate::mdbx::DatabaseArguments;
45    use parking_lot::RwLock;
46    use reth_db_api::{database::Database, database_metrics::DatabaseMetrics};
47    use reth_fs_util;
48    use std::{
49        fmt::Formatter,
50        path::{Path, PathBuf},
51        sync::Arc,
52    };
53    use tempfile::TempDir;
54
55    /// Error during database open
56    pub const ERROR_DB_OPEN: &str = "could not open the database file";
57    /// Error during database creation
58    pub const ERROR_DB_CREATION: &str = "could not create the database file";
59    /// Error during database creation
60    pub const ERROR_STATIC_FILES_CREATION: &str = "could not create the static file path";
61    /// Error during table creation
62    pub const ERROR_TABLE_CREATION: &str = "could not create tables in the database";
63    /// Error during tempdir creation
64    pub const ERROR_TEMPDIR: &str = "could not create a temporary directory";
65
66    /// A database will delete the db dir when dropped.
67    pub struct TempDatabase<DB> {
68        db: Option<DB>,
69        path: PathBuf,
70        /// Executed right before a database transaction is created.
71        pre_tx_hook: RwLock<Box<dyn Fn() + Send + Sync>>,
72        /// Executed right after a database transaction is created.
73        post_tx_hook: RwLock<Box<dyn Fn() + Send + Sync>>,
74    }
75
76    impl<DB: std::fmt::Debug> std::fmt::Debug for TempDatabase<DB> {
77        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78            f.debug_struct("TempDatabase").field("db", &self.db).field("path", &self.path).finish()
79        }
80    }
81
82    impl<DB> Drop for TempDatabase<DB> {
83        fn drop(&mut self) {
84            if let Some(db) = self.db.take() {
85                drop(db);
86                let _ = reth_fs_util::remove_dir_all(&self.path);
87            }
88        }
89    }
90
91    impl<DB> TempDatabase<DB> {
92        /// Create new [`TempDatabase`] instance.
93        pub fn new(db: DB, path: PathBuf) -> Self {
94            Self {
95                db: Some(db),
96                path,
97                pre_tx_hook: RwLock::new(Box::new(|| ())),
98                post_tx_hook: RwLock::new(Box::new(|| ())),
99            }
100        }
101
102        /// Returns the reference to inner db.
103        pub const fn db(&self) -> &DB {
104            self.db.as_ref().unwrap()
105        }
106
107        /// Returns the path to the database.
108        pub fn path(&self) -> &Path {
109            &self.path
110        }
111
112        /// Convert temp database into inner.
113        pub fn into_inner_db(mut self) -> DB {
114            self.db.take().unwrap() // take out db to avoid clean path in drop fn
115        }
116
117        /// Sets [`TempDatabase`] new pre transaction creation hook.
118        pub fn set_pre_transaction_hook(&self, hook: Box<dyn Fn() + Send + Sync>) {
119            let mut db_hook = self.pre_tx_hook.write();
120            *db_hook = hook;
121        }
122
123        /// Sets [`TempDatabase`] new post transaction creation hook.
124        pub fn set_post_transaction_hook(&self, hook: Box<dyn Fn() + Send + Sync>) {
125            let mut db_hook = self.post_tx_hook.write();
126            *db_hook = hook;
127        }
128    }
129
130    impl<DB: Database> Database for TempDatabase<DB> {
131        type TX = <DB as Database>::TX;
132        type TXMut = <DB as Database>::TXMut;
133        fn tx(&self) -> Result<Self::TX, DatabaseError> {
134            self.pre_tx_hook.read()();
135            let tx = self.db().tx()?;
136            self.post_tx_hook.read()();
137            Ok(tx)
138        }
139
140        fn tx_mut(&self) -> Result<Self::TXMut, DatabaseError> {
141            self.db().tx_mut()
142        }
143    }
144
145    impl<DB: DatabaseMetrics> DatabaseMetrics for TempDatabase<DB> {
146        fn report_metrics(&self) {
147            self.db().report_metrics()
148        }
149    }
150
151    /// Create `static_files` path for testing
152    #[track_caller]
153    pub fn create_test_static_files_dir() -> (TempDir, PathBuf) {
154        let temp_dir = TempDir::with_prefix("reth-test-static-").expect(ERROR_TEMPDIR);
155        let path = temp_dir.path().to_path_buf();
156        (temp_dir, path)
157    }
158
159    /// Create `rocksdb` path for testing
160    #[track_caller]
161    pub fn create_test_rocksdb_dir() -> (TempDir, PathBuf) {
162        let temp_dir = TempDir::with_prefix("reth-test-rocksdb-").expect(ERROR_TEMPDIR);
163        let path = temp_dir.path().to_path_buf();
164        (temp_dir, path)
165    }
166
167    /// Get a temporary directory path to use for the database
168    pub fn tempdir_path() -> PathBuf {
169        let builder = tempfile::Builder::new().prefix("reth-test-").rand_bytes(8).tempdir();
170        builder.expect(ERROR_TEMPDIR).keep()
171    }
172
173    /// Create read/write database for testing
174    #[track_caller]
175    pub fn create_test_rw_db() -> Arc<TempDatabase<DatabaseEnv>> {
176        let path = tempdir_path();
177        let emsg = format!("{ERROR_DB_CREATION}: {path:?}");
178
179        let db = init_db(&path, DatabaseArguments::test()).expect(&emsg);
180
181        Arc::new(TempDatabase::new(db, path))
182    }
183
184    /// Create read/write database for testing
185    #[track_caller]
186    pub fn create_test_rw_db_with_path<P: AsRef<Path>>(path: P) -> Arc<TempDatabase<DatabaseEnv>> {
187        let path = path.as_ref().to_path_buf();
188        let emsg = format!("{ERROR_DB_CREATION}: {path:?}");
189        let db = init_db(path.as_path(), DatabaseArguments::test()).expect(&emsg);
190        Arc::new(TempDatabase::new(db, path))
191    }
192
193    /// Create read/write database for testing within a data directory.
194    ///
195    /// The database is created at `datadir/db`, and `TempDatabase` will clean up the entire
196    /// `datadir` on drop.
197    #[track_caller]
198    pub fn create_test_rw_db_with_datadir<P: AsRef<Path>>(
199        datadir: P,
200    ) -> Arc<TempDatabase<DatabaseEnv>> {
201        let datadir = datadir.as_ref().to_path_buf();
202        let db_path = datadir.join("db");
203        let emsg = format!("{ERROR_DB_CREATION}: {db_path:?}");
204        let db = init_db(&db_path, DatabaseArguments::test()).expect(&emsg);
205        Arc::new(TempDatabase::new(db, datadir))
206    }
207
208    /// Create read only database for testing
209    #[track_caller]
210    pub fn create_test_ro_db() -> Arc<TempDatabase<DatabaseEnv>> {
211        let args = DatabaseArguments::test();
212
213        let path = tempdir_path();
214        let emsg = format!("{ERROR_DB_CREATION}: {path:?}");
215        {
216            init_db(path.as_path(), args.clone()).expect(&emsg);
217        }
218        let db = open_db_read_only(path.as_path(), args).expect(ERROR_DB_OPEN);
219        Arc::new(TempDatabase::new(db, path))
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use crate::{
226        init_db,
227        mdbx::DatabaseArguments,
228        open_db, tables,
229        version::{db_version_file_path, DatabaseVersionError},
230    };
231    use assert_matches::assert_matches;
232    use reth_db_api::{
233        cursor::DbCursorRO, database::Database, models::ClientVersion, transaction::DbTx,
234    };
235    use reth_libmdbx::MaxReadTransactionDuration;
236    use std::time::Duration;
237    use tempfile::tempdir;
238
239    #[test]
240    fn test_temp_database_cleanup() {
241        // Test that TempDatabase properly cleans up its directory when dropped
242        let temp_path = {
243            let db = crate::test_utils::create_test_rw_db();
244            let path = db.path().to_path_buf();
245            assert!(path.exists(), "Database directory should exist while TempDatabase is alive");
246            path
247            // TempDatabase dropped here
248        };
249
250        // Verify the directory was cleaned up
251        assert!(
252            !temp_path.exists(),
253            "Database directory should be cleaned up after TempDatabase is dropped"
254        );
255    }
256
257    #[test]
258    fn db_version() {
259        let path = tempdir().unwrap();
260
261        let args = DatabaseArguments::new(ClientVersion::default())
262            .with_max_read_transaction_duration(Some(MaxReadTransactionDuration::Unbounded));
263
264        // Database is empty
265        {
266            let db = init_db(&path, args.clone());
267            assert_matches!(db, Ok(_));
268        }
269
270        // Database is not empty, current version is the same as in the file
271        {
272            let db = init_db(&path, args.clone());
273            assert_matches!(db, Ok(_));
274        }
275
276        // Database is not empty, version file is malformed
277        {
278            reth_fs_util::write(path.path().join(db_version_file_path(&path)), "invalid-version")
279                .unwrap();
280            let db = init_db(&path, args.clone());
281            assert!(db.is_err());
282            assert_matches!(
283                db.unwrap_err().downcast_ref::<DatabaseVersionError>(),
284                Some(DatabaseVersionError::MalformedFile)
285            )
286        }
287
288        // Database is not empty, version file contains not matching version
289        {
290            reth_fs_util::write(path.path().join(db_version_file_path(&path)), "0").unwrap();
291            let db = init_db(&path, args);
292            assert!(db.is_err());
293            assert_matches!(
294                db.unwrap_err().downcast_ref::<DatabaseVersionError>(),
295                Some(DatabaseVersionError::VersionMismatch { version: 0 })
296            )
297        }
298    }
299
300    #[test]
301    fn db_client_version() {
302        let path = tempdir().unwrap();
303
304        // Empty client version is not recorded
305        {
306            let db = init_db(&path, DatabaseArguments::new(ClientVersion::default())).unwrap();
307            let tx = db.tx().unwrap();
308            let mut cursor = tx.cursor_read::<tables::VersionHistory>().unwrap();
309            assert_matches!(cursor.first(), Ok(None));
310        }
311
312        // Client version is recorded
313        let first_version = ClientVersion { version: String::from("v1"), ..Default::default() };
314        {
315            let db = init_db(&path, DatabaseArguments::new(first_version.clone())).unwrap();
316            let tx = db.tx().unwrap();
317            let mut cursor = tx.cursor_read::<tables::VersionHistory>().unwrap();
318            assert_eq!(
319                cursor
320                    .walk_range(..)
321                    .unwrap()
322                    .map(|x| x.map(|(_, v)| v))
323                    .collect::<Result<Vec<_>, _>>()
324                    .unwrap(),
325                vec![first_version.clone()]
326            );
327        }
328
329        // Same client version is not duplicated.
330        {
331            let db = init_db(&path, DatabaseArguments::new(first_version.clone())).unwrap();
332            let tx = db.tx().unwrap();
333            let mut cursor = tx.cursor_read::<tables::VersionHistory>().unwrap();
334            assert_eq!(
335                cursor
336                    .walk_range(..)
337                    .unwrap()
338                    .map(|x| x.map(|(_, v)| v))
339                    .collect::<Result<Vec<_>, _>>()
340                    .unwrap(),
341                vec![first_version.clone()]
342            );
343        }
344
345        // Different client version is recorded
346        std::thread::sleep(Duration::from_secs(1));
347        let second_version = ClientVersion { version: String::from("v2"), ..Default::default() };
348        {
349            let db = init_db(&path, DatabaseArguments::new(second_version.clone())).unwrap();
350            let tx = db.tx().unwrap();
351            let mut cursor = tx.cursor_read::<tables::VersionHistory>().unwrap();
352            assert_eq!(
353                cursor
354                    .walk_range(..)
355                    .unwrap()
356                    .map(|x| x.map(|(_, v)| v))
357                    .collect::<Result<Vec<_>, _>>()
358                    .unwrap(),
359                vec![first_version.clone(), second_version.clone()]
360            );
361        }
362
363        // Different client version is recorded on db open.
364        std::thread::sleep(Duration::from_secs(1));
365        let third_version = ClientVersion { version: String::from("v3"), ..Default::default() };
366        {
367            let db = open_db(path.path(), DatabaseArguments::new(third_version.clone())).unwrap();
368            let tx = db.tx().unwrap();
369            let mut cursor = tx.cursor_read::<tables::VersionHistory>().unwrap();
370            assert_eq!(
371                cursor
372                    .walk_range(..)
373                    .unwrap()
374                    .map(|x| x.map(|(_, v)| v))
375                    .collect::<Result<Vec<_>, _>>()
376                    .unwrap(),
377                vec![first_version, second_version, third_version]
378            );
379        }
380    }
381}