Skip to content
86 changes: 81 additions & 5 deletions crates/storage/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,12 +1649,16 @@ impl Store {
let version = read_store_schema_version(&db_path)?;

match version {
None if db_path.exists() && !dir_is_empty(&db_path)? => {
None if db_path.exists() && dir_contains_legacy_db(&db_path)? => {
// Pre-metadata DB — cannot migrate safely
return Err(StoreError::NotFoundDBVersion);
}
None => {
// Fresh / empty directory — write initial metadata
// No metadata and no recognizable database files. The directory
// may still hold unrelated files (e.g. a JWT secret placed in the
// datadir by tooling such as EthDocker, see issue #5680), so treat
// this as a fresh datadir and write the initial metadata instead
// of erroring out.
init_metadata_file(&db_path)?;
}
Some(v) if v < 1 => {
Expand Down Expand Up @@ -3594,9 +3598,37 @@ fn init_metadata_file(parent_path: &Path) -> Result<(), StoreError> {
Ok(())
}

fn dir_is_empty(path: &Path) -> Result<bool, StoreError> {
let is_empty = std::fs::read_dir(path)?.next().is_none();
Ok(is_empty)
/// Returns `true` if `path` contains a *legacy* database — one written before
/// the metadata file existed, so it has no `metadata.json` to identify it.
/// Detected by RocksDB's own marker files, as opposed to unrelated files that
/// merely share the datadir. Only meaningful once metadata has been confirmed
/// absent; otherwise prefer `has_valid_db`, which keys off the metadata file.
///
/// Previously the caller treated *any* non-empty directory as such a legacy
/// database, which made startup fail when unrelated files lived alongside the DB
/// — e.g. EthDocker writes the JWT secret into the datadir (issue #5680). We
/// instead look for RocksDB's marker files, so a datadir that only contains such
/// unrelated files is correctly treated as fresh.
fn dir_contains_legacy_db(path: &Path) -> Result<bool, StoreError> {
// `CURRENT` has a fixed name and is written by every RocksDB instance, so
// check for it directly instead of scanning a datadir that may hold many
// unrelated files.
if path.join("CURRENT").is_file() {
return Ok(true);
}
// The manifest has a numeric suffix (`MANIFEST-<n>`), so it can only be
// found by scanning. Restrict to plain files: a directory that happens to
// share the name is not a database marker.
for entry in std::fs::read_dir(path)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
if entry.file_name().to_string_lossy().starts_with("MANIFEST-") {
return Ok(true);
}
}
Comment thread
pjdurden marked this conversation as resolved.
Comment thread
pjdurden marked this conversation as resolved.
Ok(false)
}

/// Checks whether a valid (or migratable) database exists at the given path
Expand Down Expand Up @@ -3802,3 +3834,47 @@ mod merge_tests {
assert_eq!(decode(&out).len(), 3);
}
}

#[cfg(test)]
mod datadir_tests {
use super::*;
use std::fs;

#[test]
fn empty_dir_has_no_existing_db() {
let dir = tempfile::tempdir().unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}

#[test]
fn dir_with_only_unrelated_files_has_no_existing_db() {
// Regression for #5680: a JWT secret (or any unrelated file) in the
// datadir must not be mistaken for an existing database.
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("jwt.hex"), "0xdeadbeef").unwrap();
fs::write(dir.path().join("LOG"), "noise").unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}

#[test]
fn dir_with_rocksdb_markers_has_existing_db() {
// A `CURRENT` file (and, separately, a `MANIFEST-*` file) marks a real DB.
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("CURRENT"), "MANIFEST-000001\n").unwrap();
assert!(dir_contains_legacy_db(dir.path()).unwrap());

let dir2 = tempfile::tempdir().unwrap();
fs::write(dir2.path().join("MANIFEST-000007"), "x").unwrap();
assert!(dir_contains_legacy_db(dir2.path()).unwrap());
}

#[test]
fn dir_with_marker_named_subdirectories_has_no_existing_db() {
// A *directory* named like a marker file must not be mistaken for a DB;
// RocksDB only ever writes these as plain files.
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("CURRENT")).unwrap();
fs::create_dir(dir.path().join("MANIFEST-000001")).unwrap();
assert!(!dir_contains_legacy_db(dir.path()).unwrap());
}
}
Loading