use std::sync::{Arc, Mutex};
use eml::{Hasher, MemoryStorage, NaryMerkleLog, Storage, TreeConfig};
use sha2::{Digest, Sha256};
#[derive(Debug)]
struct Sha256Hasher;
impl Hasher for Sha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for child in children {
h.update(child);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(b"").to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn clone_box(&self) -> Box<dyn Hasher> {
Box::new(Sha256Hasher)
}
}
#[derive(Debug, Clone)]
struct FaultInjectingStorage {
inner: MemoryStorage,
fail_after_batches: Arc<Mutex<Option<usize>>>,
batch_count: Arc<Mutex<usize>>,
}
impl FaultInjectingStorage {
fn new(inner: MemoryStorage) -> Self {
Self {
inner,
fail_after_batches: Arc::new(Mutex::new(None)),
batch_count: Arc::new(Mutex::new(0)),
}
}
fn set_fail_after_batches(&self, count: Option<usize>) {
*self.fail_after_batches.lock().unwrap() = count;
*self.batch_count.lock().unwrap() = 0;
}
}
#[derive(Debug)]
pub enum FaultError {
Injected,
Storage,
}
impl std::fmt::Display for FaultError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for FaultError {}
impl Storage for FaultInjectingStorage {
type Error = FaultError;
async fn store_leaf(&mut self, index: u64, data: &[u8]) -> Result<(), Self::Error> {
self.inner
.store_leaf(index, data)
.await
.map_err(|_| FaultError::Storage)
}
async fn get_leaf(&self, index: u64) -> Result<Vec<u8>, Self::Error> {
self.inner
.get_leaf(index)
.await
.map_err(|_| FaultError::Storage)
}
async fn len(&self) -> Result<u64, Self::Error> {
self.inner.len().await.map_err(|_| FaultError::Storage)
}
async fn store_node(
&mut self,
alg_id: u64,
left: u64,
height: u32,
hash: &[u8],
) -> Result<(), Self::Error> {
self.inner
.store_node(alg_id, left, height, hash)
.await
.map_err(|_| FaultError::Storage)
}
async fn get_node(
&self,
alg_id: u64,
left: u64,
height: u32,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.inner
.get_node(alg_id, left, height)
.await
.map_err(|_| FaultError::Storage)
}
async fn store_algorithm_meta(
&mut self,
alg_id: u64,
epochs: &[(u64, u64)],
) -> Result<(), Self::Error> {
self.inner
.store_algorithm_meta(alg_id, epochs)
.await
.map_err(|_| FaultError::Storage)
}
async fn load_algorithm_metas(&self) -> Result<eml::AlgorithmMetas, Self::Error> {
self.inner
.load_algorithm_metas()
.await
.map_err(|_| FaultError::Storage)
}
async fn load_log_meta(&self) -> Result<Option<(u64, u8)>, Self::Error> {
self.inner
.load_log_meta()
.await
.map_err(|_| FaultError::Storage)
}
async fn load_checkpoint_roots(&self) -> Result<Vec<(u64, Vec<u8>)>, Self::Error> {
self.inner
.load_checkpoint_roots()
.await
.map_err(|_| FaultError::Storage)
}
async fn write_batch(
&mut self,
leaves: &[(u64, &[u8])],
nodes: &[(u64, u64, u32, &[u8])],
algorithm_metas: &[(u64, &[(u64, u64)])],
log_meta: Option<(u64, u8)>,
checkpoint_roots: &[(u64, &[u8])],
) -> Result<(), Self::Error> {
let should_fail = {
let mut count = self.batch_count.lock().unwrap();
let limit = self.fail_after_batches.lock().unwrap();
if let Some(limit) = *limit {
if *count >= limit {
true
} else {
*count += 1;
false
}
} else {
*count += 1;
false
}
};
if should_fail {
return Err(FaultError::Injected);
}
let backup = self.inner.clone();
if self
.inner
.write_batch(leaves, nodes, algorithm_metas, log_meta, checkpoint_roots)
.await
.is_err()
{
self.inner = backup;
return Err(FaultError::Storage);
}
Ok(())
}
}
#[test]
fn test_mid_batch_failure_recovery() {
smol::block_on(async {
let storage = FaultInjectingStorage::new(MemoryStorage::new());
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage.clone(), Box::new(Sha256Hasher), config)
.await
.unwrap();
for i in 0..10 {
log.append_leaf(&[i]).await.unwrap();
}
let root_before = log.root();
let size_before = log.size();
assert_eq!(size_before, 10);
storage.set_fail_after_batches(Some(0));
let append_res = log.append_leaf(&[10]).await;
assert!(append_res.is_err());
let final_storage = log.into_storage();
let reconstructed =
NaryMerkleLog::from_storage(final_storage, vec![(0, Box::new(Sha256Hasher))])
.await
.unwrap();
assert_eq!(reconstructed.size(), size_before);
assert_eq!(reconstructed.root(), root_before);
storage.set_fail_after_batches(None);
let mut reconstructed = reconstructed;
reconstructed.append_leaf(&[10]).await.unwrap();
assert_eq!(reconstructed.size(), 11);
});
}
#[test]
fn test_verify_non_divergence_tamper_detection() {
smol::block_on(async {
let hasher = Sha256Hasher;
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(hasher), config)
.await
.unwrap();
for i in 0..15u8 {
log.append_leaf(&[i]).await.unwrap();
}
assert!(log.verify_non_divergence(None, &[]).await.unwrap());
{
let mut tampered_storage = log.storage().clone();
tampered_storage.leaves[7] = vec![0xFF; 16];
let tampered_log =
NaryMerkleLog::from_storage(tampered_storage, vec![(0, Box::new(Sha256Hasher))])
.await;
match tampered_log {
Err(_) => {},
Ok(log) => {
assert!(
!log.verify_non_divergence(None, &[]).await.unwrap(),
"Failed to detect tampered leaf data"
);
},
}
}
{
let mut tampered_storage = log.storage().clone();
let key = (0, 0, 3); if let std::collections::hash_map::Entry::Occupied(mut e) =
tampered_storage.nodes.entry(key)
{
e.insert(vec![0x00; 32]);
let tampered_log = NaryMerkleLog::from_storage(
tampered_storage,
vec![(0, Box::new(Sha256Hasher))],
)
.await;
assert!(
tampered_log.is_err(),
"from_storage did not detect tampered frontier node"
);
}
}
{
let mut tampered_storage = log.storage().clone();
if let Some(epochs) = tampered_storage.algorithm_metas.get_mut(&0) {
if !epochs.is_empty() {
epochs[0].1 = 10;
}
}
let tampered_log =
NaryMerkleLog::from_storage(tampered_storage, vec![(0, Box::new(Sha256Hasher))])
.await;
assert!(
tampered_log.is_err(),
"from_storage did not detect tampered epoch metadata"
);
}
});
}
#[test]
fn test_verify_non_divergence_legitimate_frozen() {
smol::block_on(async {
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(Sha256Hasher), config)
.await
.unwrap();
log.add_algorithm(1, Box::new(Sha256Hasher)).await.unwrap();
for i in 0..5 {
log.append_leaf(&[i]).await.unwrap();
}
log.remove_algorithm(1).await.unwrap();
for i in 5..10 {
log.append_leaf(&[i]).await.unwrap();
}
let metas = vec![
(0, Box::new(Sha256Hasher) as Box<dyn Hasher>),
(1, Box::new(Sha256Hasher) as Box<dyn Hasher>),
];
let reconstructed = NaryMerkleLog::from_storage(log.storage().clone(), metas)
.await
.unwrap();
assert!(
reconstructed
.verify_non_divergence(None, &[])
.await
.unwrap(),
"Legitimate frozen algorithm failed non-divergence verification"
);
{
let mut tampered_storage = log.storage().clone();
if let Some(epochs) = tampered_storage.algorithm_metas.get_mut(&1) {
epochs[0].1 = 3;
}
let metas = vec![
(0, Box::new(Sha256Hasher) as Box<dyn Hasher>),
(1, Box::new(Sha256Hasher) as Box<dyn Hasher>),
];
let tampered_log = NaryMerkleLog::from_storage(tampered_storage, metas).await;
assert!(
tampered_log.is_err(),
"from_storage did not detect tampered epoch boundary for frozen algorithm"
);
}
});
}
#[test]
fn test_resume_algorithm_non_atomic_crash_recovery() {
smol::block_on(async {
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(Sha256Hasher), config)
.await
.unwrap();
log.append_leaf(b"leaf0").await.unwrap();
log.append_leaf(b"leaf1").await.unwrap();
log.add_algorithm(1, Box::new(Sha256Hasher)).await.unwrap();
log.remove_algorithm(0).await.unwrap();
log.append_leaf(b"leaf2").await.unwrap();
log.append_leaf(b"leaf3").await.unwrap();
let mut mutated_storage = log.storage().clone();
mutated_storage
.store_node(0, 0, 2, &[0xAA; 32])
.await
.unwrap();
let metas = vec![
(0, Box::new(Sha256Hasher) as Box<dyn Hasher>),
(1, Box::new(Sha256Hasher) as Box<dyn Hasher>),
];
let mut recovered_log = NaryMerkleLog::from_storage(mutated_storage, metas)
.await
.unwrap();
assert_eq!(recovered_log.size(), 4);
recovered_log.resume_algorithm(0).await.unwrap();
let res = recovered_log.resume_algorithm(0).await;
assert!(matches!(res.unwrap_err(), eml::Error::AlgorithmActive(0)));
recovered_log.append_leaf(b"leaf4").await.unwrap();
assert_eq!(recovered_log.size(), 5);
});
}
#[test]
fn test_v12_boundary_band_corruption_detected() {
smol::block_on(async {
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(Sha256Hasher), config)
.await
.unwrap();
log.add_algorithm(1, Box::new(Sha256Hasher)).await.unwrap();
log.append_leaf(b"leaf0").await.unwrap();
log.append_leaf(b"leaf1").await.unwrap();
log.append_leaf(b"leaf2").await.unwrap();
log.remove_algorithm(1).await.unwrap();
log.append_leaf(b"leaf3").await.unwrap();
log.append_leaf(b"leaf4").await.unwrap();
log.append_leaf(b"leaf5").await.unwrap();
log.resume_algorithm(1).await.unwrap();
log.append_leaf(b"leaf6").await.unwrap();
let metas = vec![
(0u64, Box::new(Sha256Hasher) as Box<dyn Hasher>),
(1u64, Box::new(Sha256Hasher) as Box<dyn Hasher>),
];
assert!(
log.verify_non_divergence(None, &[]).await.unwrap(),
"clean log after resume_algorithm + extra leaf failed non-divergence check"
);
let mut tampered = log.storage().clone();
tampered.nodes.remove(&(1, 2, 1));
let tampered_log = NaryMerkleLog::from_storage(tampered, metas).await.unwrap();
let result = tampered_log.verify_non_divergence(None, &[]).await;
assert!(
result.is_err() || !result.unwrap(),
"boundary band corruption was not detected"
);
});
}
#[test]
fn test_v16_subtree_mode_tamper_detected() {
smol::block_on(async {
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(Sha256Hasher), config)
.await
.unwrap();
let sub0 = eml::Subtree::Leaf(b"subtree-payload-0".to_vec());
let sub1 = eml::Subtree::Leaf(b"subtree-payload-1".to_vec());
log.append_subtree(&sub0).await.unwrap();
log.append_subtree(&sub1).await.unwrap();
assert!(
log.verify_non_divergence(None, &[]).await.unwrap(),
"clean subtree log failed non-divergence check"
);
let mut tampered = log.storage().clone();
tampered.nodes.insert((0, 0, 0), vec![0xDE; 32]);
let tampered_log =
NaryMerkleLog::from_storage(tampered, vec![(0, Box::new(Sha256Hasher))]).await;
match tampered_log {
Err(_) => {},
Ok(log) => {
assert!(
!log.verify_non_divergence(None, &[]).await.unwrap(),
"subtree-mode tampering was not detected"
);
},
}
});
}
#[test]
fn test_v16_wrong_length_digest_rejected() {
smol::block_on(async {
let storage = MemoryStorage::new();
let config = TreeConfig { arity: 2 };
let mut log = NaryMerkleLog::new(storage, Box::new(Sha256Hasher), config)
.await
.unwrap();
for i in 0u8..4 {
log.append_leaf(&[i]).await.unwrap();
}
let mut tampered = log.storage().clone();
tampered.nodes.insert((0, 0, 1), vec![0xAB; 16]);
let tampered_log = NaryMerkleLog::from_storage(tampered, vec![(0, Box::new(Sha256Hasher))])
.await
.unwrap();
let result = tampered_log.verify_non_divergence(None, &[]).await;
assert!(
result.is_err(),
"wrong-length digest was not rejected: {:?}",
result
);
});
}