diff --git a/slasher/src/database.rs b/slasher/src/database.rs index cc95a90c5..8f1c97aa4 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -4,6 +4,7 @@ use crate::{ }; use byteorder::{BigEndian, ByteOrder}; use lmdb::{Cursor, Database, DatabaseFlags, Environment, RwTransaction, Transaction, WriteFlags}; +use serde::Deserialize; use ssz::{Decode, Encode}; use std::marker::PhantomData; use std::sync::Arc; @@ -12,7 +13,7 @@ use types::{ }; /// Current database schema version, to check compatibility of on-disk DB with software. -const CURRENT_SCHEMA_VERSION: u64 = 1; +pub const CURRENT_SCHEMA_VERSION: u64 = 2; /// Metadata about the slashing database itself. const METADATA_DB: &str = "metadata"; @@ -209,15 +210,7 @@ impl SlasherDB { let mut txn = db.begin_rw_txn()?; - if let Some(schema_version) = db.load_schema_version(&mut txn)? { - if schema_version != CURRENT_SCHEMA_VERSION { - return Err(Error::IncompatibleSchemaVersion { - database_schema_version: schema_version, - software_schema_version: CURRENT_SCHEMA_VERSION, - }); - } - } - db.store_schema_version(&mut txn)?; + db.migrate(&mut txn)?; if let Some(on_disk_config) = db.load_config(&mut txn)? { if !db.config.is_compatible(&on_disk_config) { @@ -227,7 +220,7 @@ impl SlasherDB { }); } } - db.store_config(&mut txn)?; + db.store_config(&db.config, &mut txn)?; txn.commit()?; Ok(db) @@ -263,7 +256,14 @@ impl SlasherDB { Ok(()) } - pub fn load_config(&self, txn: &mut RwTransaction<'_>) -> Result, Error> { + /// Load a config from disk. + /// + /// This is generic in order to allow loading of configs for different schema versions. + /// Care should be taken to ensure it is only called for `Config`-like `T`. + pub fn load_config<'a, T: Deserialize<'a>>( + &self, + txn: &'a mut RwTransaction<'_>, + ) -> Result, Error> { Ok(txn .get(self.metadata_db, &METADATA_CONFIG_KEY) .optional()? @@ -271,11 +271,11 @@ impl SlasherDB { .transpose()?) } - pub fn store_config(&self, txn: &mut RwTransaction<'_>) -> Result<(), Error> { + pub fn store_config(&self, config: &Config, txn: &mut RwTransaction<'_>) -> Result<(), Error> { txn.put( self.metadata_db, &METADATA_CONFIG_KEY, - &bincode::serialize(self.config.as_ref())?, + &bincode::serialize(config)?, Self::write_flags(), )?; Ok(()) diff --git a/slasher/src/error.rs b/slasher/src/error.rs index 0d3262bce..97b3f8cd0 100644 --- a/slasher/src/error.rs +++ b/slasher/src/error.rs @@ -25,6 +25,7 @@ pub enum Error { on_disk_config: Config, config: Config, }, + ConfigMissing, DistanceTooLarge, DistanceCalculationOverflow, /// Missing an attester record that we expected to exist. diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index e16003126..d61c58c8c 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -9,6 +9,7 @@ pub mod config; mod database; mod error; pub mod metrics; +mod migrate; mod slasher; pub mod test_utils; mod utils; diff --git a/slasher/src/migrate.rs b/slasher/src/migrate.rs new file mode 100644 index 000000000..1dfca9f69 --- /dev/null +++ b/slasher/src/migrate.rs @@ -0,0 +1,74 @@ +use crate::{database::CURRENT_SCHEMA_VERSION, Config, Error, SlasherDB}; +use lmdb::RwTransaction; +use serde_derive::{Deserialize, Serialize}; +use std::path::PathBuf; +use types::EthSpec; + +/// Config from schema version 1, for migration to version 2+. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigV1 { + database_path: PathBuf, + chunk_size: usize, + validator_chunk_size: usize, + history_length: usize, + update_period: u64, + max_db_size_mbs: usize, +} + +type ConfigV2 = Config; + +impl Into for ConfigV1 { + fn into(self) -> ConfigV2 { + Config { + database_path: self.database_path, + chunk_size: self.chunk_size, + validator_chunk_size: self.validator_chunk_size, + history_length: self.history_length, + update_period: self.update_period, + max_db_size_mbs: self.max_db_size_mbs, + broadcast: false, + } + } +} + +impl SlasherDB { + /// If the database exists, and has a schema, attempt to migrate it to the current version. + pub fn migrate(&self, txn: &mut RwTransaction<'_>) -> Result<(), Error> { + if let Some(schema_version) = self.load_schema_version(txn)? { + match (schema_version, CURRENT_SCHEMA_VERSION) { + // The migration from v1 to v2 is a bit messy because v1.0.5 silently + // changed the schema to v2, so a v1 schema could have either a v1 or v2 + // config. + (1, 2) => { + match self.load_config::(txn) { + Ok(Some(config_v1)) => { + // Upgrade to v2 config and store on disk. + let config_v2 = config_v1.into(); + self.store_config(&config_v2, txn)?; + } + Ok(None) => { + // Impossible to have schema version and no config. + return Err(Error::ConfigMissing); + } + Err(_) => { + // If loading v1 config failed, ensure loading v2 config succeeds. + // No further action is needed. + let _config_v2 = self.load_config::(txn)?; + } + } + } + (x, y) if x == y => {} + (_, _) => { + return Err(Error::IncompatibleSchemaVersion { + database_schema_version: schema_version, + software_schema_version: CURRENT_SCHEMA_VERSION, + }); + } + } + } + + // If the migration succeeded, update the schema version on-disk. + self.store_schema_version(txn)?; + Ok(()) + } +}