pub mod factory;
use std::fmt::Write;
use splinter::error::{InternalError, InvalidArgumentError, InvalidStateError};
use transact::database::{lmdb::LmdbDatabase, Database, DatabaseError};
use crate::hex;
use super::{CommitHashStore, CommitHashStoreError};
pub(crate) const CURRENT_STATE_ROOT_INDEX: &str = "current_state_root";
pub type LmdbCommitHashStore = TransactCommitHashStore<LmdbDatabase>;
#[derive(Clone)]
pub struct TransactCommitHashStore<D>
where
D: Database + Clone + 'static,
{
db: D,
}
impl<D: Database + Clone> TransactCommitHashStore<D> {
pub fn new(db: D) -> Self {
Self { db }
}
}
impl<D: Database + Clone> CommitHashStore for TransactCommitHashStore<D> {
fn get_current_commit_hash(&self) -> Result<Option<String>, CommitHashStoreError> {
let reader = self
.db
.get_reader()
.map_err(|e| InternalError::from_source(Box::new(e)))?;
match reader.index_get(CURRENT_STATE_ROOT_INDEX, b"HEAD") {
Ok(current_commit_hash) => Ok(current_commit_hash.map(|bytes| to_hex(&bytes))),
Err(DatabaseError::ReaderError(msg)) if msg.starts_with("Not an index") => Err(
CommitHashStoreError::InvalidState(InvalidStateError::with_message(
"Missing current_state_root index in LMDB database".into(),
)),
),
Err(err) => Err(CommitHashStoreError::Internal(InternalError::from_source(
Box::new(err),
))),
}
}
fn set_current_commit_hash(&self, commit_hash: &str) -> Result<(), CommitHashStoreError> {
let current_root_bytes = hex::parse_hex(commit_hash).map_err(|e| {
InvalidArgumentError::new(
"commit_hash".into(),
format!("The commit hash provided is invalid: {}", e),
)
})?;
let mut writer = self
.db
.get_writer()
.map_err(|e| InternalError::from_source(Box::new(e)))?;
match writer.index_put(CURRENT_STATE_ROOT_INDEX, b"HEAD", ¤t_root_bytes) {
Ok(()) => (),
Err(DatabaseError::WriterError(msg)) if msg.starts_with("Not an index") => {
return Err(CommitHashStoreError::InvalidState(
InvalidStateError::with_message(
"Missing current_state_root index in LMDB database".into(),
),
))
}
Err(err) => {
return Err(CommitHashStoreError::Internal(InternalError::from_source(
Box::new(err),
)))
}
}
writer
.commit()
.map_err(|e| InternalError::from_source(Box::new(e)))?;
Ok(())
}
}
fn to_hex(bytes: &[u8]) -> String {
let mut buf = String::new();
for b in bytes {
write!(&mut buf, "{:02x}", b).expect("Unable to write to string");
}
buf
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::error::Error;
use std::fs::remove_file;
use std::panic;
use std::path::Path;
use std::thread;
use transact::{
database::{
lmdb::{LmdbContext, LmdbDatabase},
DatabaseError,
},
state::merkle::INDEXES,
};
#[test]
fn test_lmdb_store() -> Result<(), Box<dyn Error>> {
run_lmdb_test(|dbpath| {
let mut indexes = INDEXES.to_vec();
indexes.push(CURRENT_STATE_ROOT_INDEX);
let db = make_lmdb(&indexes, dbpath)?;
let commit_log_store = LmdbCommitHashStore::new(db);
assert_eq!(None, commit_log_store.get_current_commit_hash()?);
commit_log_store.set_current_commit_hash("abcdef0123456789")?;
assert_eq!(
Some("abcdef0123456789".to_string()),
commit_log_store.get_current_commit_hash()?
);
Ok(())
})
}
#[test]
fn test_lmdb_store_missing_index() -> Result<(), Box<dyn Error>> {
run_lmdb_test(|dbpath| {
let db = make_lmdb(&INDEXES, dbpath)?;
let commit_log_store = LmdbCommitHashStore::new(db);
let res = commit_log_store.get_current_commit_hash();
assert!(
matches!(res, Err(CommitHashStoreError::InvalidState(_))),
"Expected invalid state error, got {:?}",
res
);
let res = commit_log_store.set_current_commit_hash("abcdef0123456789");
assert!(
matches!(res, Err(CommitHashStoreError::InvalidState(_))),
"Expected invalid state error, got {:?}",
res
);
Ok(())
})
}
pub fn run_lmdb_test<T>(test: T) -> Result<(), Box<dyn Error>>
where
T: FnOnce(&str) -> Result<(), Box<dyn Error>> + panic::UnwindSafe,
{
let dbpath = temp_db_path()?;
let testpath = dbpath.clone();
let result = panic::catch_unwind(move || test(&testpath));
remove_file(dbpath)?;
match result {
Ok(_) => Ok(()),
Err(err) => {
panic::resume_unwind(err);
}
}
}
fn make_lmdb(indexes: &[&str], merkle_path: &str) -> Result<LmdbDatabase, Box<dyn Error>> {
let ctx = LmdbContext::new(
Path::new(merkle_path),
indexes.len(),
Some(120 * 1024 * 1024),
)
.map_err(|err| DatabaseError::InitError(format!("{}", err)))?;
Ok(LmdbDatabase::new(ctx, indexes)
.map_err(|err| DatabaseError::InitError(format!("{}", err)))?)
}
fn temp_db_path() -> Result<String, Box<dyn Error>> {
let mut temp_dir = env::temp_dir();
let thread_id = thread::current().id();
temp_dir.push(format!("merkle-{:?}.lmdb", thread_id));
Ok(temp_dir
.to_str()
.ok_or_else(|| InternalError::with_message("Unable to convert path to string".into()))?
.to_string())
}
}