Fix slasher database schema migration to v2 (#2125)

## Issue Addressed

Closes #2119

## Proposed Changes

Update the slasher schema version to v2 for the breaking changes to the config introduced in #2079. Implement a migration from v1 to v2 so that users can seamlessly upgrade from any version of Lighthouse <=1.0.5.

Users who deleted their database for v1.0.5 can upgrade to a release including this patch without any manual intervention. Similarly, any users still on v1.0.4 or earlier can now upgrade without having to drop their database.
This commit is contained in:
Michael Sproul 2020-12-28 05:09:19 +00:00
parent 78d17c3255
commit 43ac3f7209
4 changed files with 90 additions and 14 deletions

View File

@ -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<E: EthSpec> SlasherDB<E> {
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<E: EthSpec> SlasherDB<E> {
});
}
}
db.store_config(&mut txn)?;
db.store_config(&db.config, &mut txn)?;
txn.commit()?;
Ok(db)
@ -263,7 +256,14 @@ impl<E: EthSpec> SlasherDB<E> {
Ok(())
}
pub fn load_config(&self, txn: &mut RwTransaction<'_>) -> Result<Option<Config>, 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<Option<T>, Error> {
Ok(txn
.get(self.metadata_db, &METADATA_CONFIG_KEY)
.optional()?
@ -271,11 +271,11 @@ impl<E: EthSpec> SlasherDB<E> {
.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(())

View File

@ -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.

View File

@ -9,6 +9,7 @@ pub mod config;
mod database;
mod error;
pub mod metrics;
mod migrate;
mod slasher;
pub mod test_utils;
mod utils;

74
slasher/src/migrate.rs Normal file
View File

@ -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<ConfigV2> 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<E: EthSpec> SlasherDB<E> {
/// 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::<ConfigV1>(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::<ConfigV2>(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(())
}
}