use std::{env, fs, io, path::PathBuf, sync::Arc};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::info;
use casper_types::{
crypto, Chainspec, ChainspecRawBytes, Digest, ProtocolVersion, PublicKey, SecretKey, Signature,
};
use crate::{
reactor::main_reactor::Config,
utils::{
chain_specification::error::Error as LoadChainspecError, LoadError, Loadable, WithDir,
},
};
const POST_MIGRATION_STATE_HASH_FILENAME: &str = "post-migration-state-hash";
const CONFIG_ROOT_DIR: &str = "/etc/casper";
const CONFIG_ROOT_DIR_OVERRIDE: &str = "CASPER_CONFIG_DIR";
#[derive(Debug, Error)]
pub(crate) enum Error {
#[error("error serializing state hash info: {0}")]
SerializeStateHashInfo(bincode::Error),
#[error("error deserializing state hash info: {0}")]
DeserializeStateHashInfo(bincode::Error),
#[error("error writing state hash info to {path}: {error}")]
WriteStateHashInfo {
path: String,
error: io::Error,
},
#[error("error reading state hash info from {path}: {error}")]
ReadStateHashInfo {
path: String,
error: io::Error,
},
#[error("invalid signature of state hash info")]
InvalidSignatureOfStateHashInfo,
#[error("error loading secret key: {0}")]
LoadSecretKey(LoadError<crypto::ErrorExt>),
#[error("error loading chainspec: {0}")]
LoadChainspec(LoadChainspecError),
}
#[derive(Serialize, Deserialize)]
struct PostMigrationInfo {
state_hash: Digest,
protocol_version: ProtocolVersion,
}
#[derive(Serialize, Deserialize)]
struct SignedPostMigrationInfo {
serialized_info: Vec<u8>,
signature: Signature,
}
#[allow(unused)]
pub(crate) fn read_post_migration_info(
protocol_version: ProtocolVersion,
public_key: &PublicKey,
) -> Result<Option<Digest>, Error> {
do_read_post_migration_info(protocol_version, public_key, info_path())
}
#[allow(unused)]
fn do_read_post_migration_info(
protocol_version: ProtocolVersion,
public_key: &PublicKey,
path: PathBuf,
) -> Result<Option<Digest>, Error> {
if !path.is_file() {
return Ok(None);
}
let serialized_signed_info = fs::read(&path).map_err(|error| Error::ReadStateHashInfo {
path: path.display().to_string(),
error,
})?;
let signed_info: SignedPostMigrationInfo =
bincode::deserialize(&serialized_signed_info).map_err(Error::DeserializeStateHashInfo)?;
crypto::verify(
&signed_info.serialized_info,
&signed_info.signature,
public_key,
)
.map_err(|_| Error::InvalidSignatureOfStateHashInfo)?;
let info: PostMigrationInfo = bincode::deserialize(&signed_info.serialized_info)
.map_err(Error::DeserializeStateHashInfo)?;
if info.protocol_version == protocol_version {
Ok(Some(info.state_hash))
} else {
Ok(None)
}
}
fn write_post_migration_info(
state_hash: Digest,
new_protocol_version: ProtocolVersion,
secret_key: &SecretKey,
path: PathBuf,
) -> Result<(), Error> {
let info = PostMigrationInfo {
state_hash,
protocol_version: new_protocol_version,
};
let serialized_info = bincode::serialize(&info).map_err(Error::SerializeStateHashInfo)?;
let public_key = PublicKey::from(secret_key);
let signature = crypto::sign(&serialized_info, secret_key, &public_key);
let signed_info = SignedPostMigrationInfo {
serialized_info,
signature,
};
let serialized_signed_info =
bincode::serialize(&signed_info).map_err(Error::SerializeStateHashInfo)?;
fs::write(&path, serialized_signed_info).map_err(|error| Error::WriteStateHashInfo {
path: path.display().to_string(),
error,
})?;
info!(path=%path.display(), "wrote post-migration state hash");
Ok(())
}
fn info_path() -> PathBuf {
PathBuf::from(
env::var(CONFIG_ROOT_DIR_OVERRIDE).unwrap_or_else(|_| CONFIG_ROOT_DIR.to_string()),
)
.join(POST_MIGRATION_STATE_HASH_FILENAME)
}
pub(crate) fn migrate_data(
_old_config: WithDir<toml::Value>,
new_config: WithDir<Config>,
) -> Result<(), Error> {
let (new_root, new_config) = new_config.into_parts();
let new_protocol_version = <(Chainspec, ChainspecRawBytes)>::from_path(&new_root)
.map_err(Error::LoadChainspec)?
.0
.protocol_config
.version;
let secret_key: Arc<SecretKey> = new_config
.consensus
.secret_key_path
.load(&new_root)
.map_err(Error::LoadSecretKey)?;
let state_hash = Digest::default();
if state_hash != Digest::default() {
write_post_migration_info(state_hash, new_protocol_version, &secret_key, info_path())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use rand::Rng;
use super::*;
#[test]
fn should_write_then_read_info() {
let tempdir = tempfile::tempdir().unwrap();
let info_path = tempdir.path().join(POST_MIGRATION_STATE_HASH_FILENAME);
let mut rng = crate::new_rng();
let state_hash = Digest::hash([rng.gen()]);
let protocol_version = ProtocolVersion::from_parts(rng.gen(), rng.gen(), rng.gen());
let secret_key = SecretKey::random(&mut rng);
write_post_migration_info(state_hash, protocol_version, &secret_key, info_path.clone())
.unwrap();
let public_key = PublicKey::from(&secret_key);
let maybe_hash =
do_read_post_migration_info(protocol_version, &public_key, info_path).unwrap();
assert_eq!(maybe_hash, Some(state_hash));
}
#[test]
fn should_return_none_after_reading_info() {
let tempdir = tempfile::tempdir().unwrap();
let info_path = tempdir.path().join(POST_MIGRATION_STATE_HASH_FILENAME);
let protocol_version = ProtocolVersion::from_parts(1, 2, 3);
let mut rng = crate::new_rng();
let secret_key = SecretKey::random(&mut rng);
let public_key = PublicKey::from(&secret_key);
let maybe_hash =
do_read_post_migration_info(protocol_version, &public_key, info_path.clone()).unwrap();
assert!(maybe_hash.is_none());
let state_hash = Digest::hash([rng.gen()]);
write_post_migration_info(state_hash, protocol_version, &secret_key, info_path.clone())
.unwrap();
assert!(
do_read_post_migration_info(protocol_version, &public_key, info_path.clone())
.unwrap()
.is_some()
);
let different_version = ProtocolVersion::from_parts(1, 2, 4);
let maybe_hash =
do_read_post_migration_info(different_version, &public_key, info_path).unwrap();
assert!(maybe_hash.is_none());
}
#[test]
fn should_fail_to_read_invalid_info() {
let tempdir = tempfile::tempdir().unwrap();
let info_path = tempdir.path().join(POST_MIGRATION_STATE_HASH_FILENAME);
fs::write(&info_path, "bad value".as_bytes()).unwrap();
let protocol_version = ProtocolVersion::from_parts(1, 2, 3);
let mut rng = crate::new_rng();
let secret_key = SecretKey::random(&mut rng);
let public_key = PublicKey::from(&secret_key);
assert!(
do_read_post_migration_info(protocol_version, &public_key, info_path.clone()).is_err()
);
let other_secret_key = SecretKey::random(&mut rng);
let state_hash = Digest::hash([rng.gen()]);
write_post_migration_info(
state_hash,
protocol_version,
&other_secret_key,
info_path.clone(),
)
.unwrap();
assert!(do_read_post_migration_info(protocol_version, &public_key, info_path).is_err());
}
}