use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
use blake3;
use hex;
use saorsa_fec::{FecCodec, FecParams};
use saorsa_seal::{EnvelopeKind, Recipient, RecipientId, SealPolicy, seal_bytes};
pub trait DhtStorage {
fn put(&self, key: &[u8; 32], value: &[u8], ttl: Option<u64>) -> anyhow::Result<()>;
fn get(&self, key: &[u8; 32]) -> anyhow::Result<Vec<u8>>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shard {
pub index: usize,
pub shard_type: ShardType,
pub data: Vec<u8>,
pub group_id: String,
pub data_id: String,
pub integrity_hash: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ShardType {
Data, Parity, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReedSolomonConfig {
pub data_shards: usize, pub parity_shards: usize, pub shard_size: usize, pub group_size_range: (usize, usize), }
impl ReedSolomonConfig {
pub fn for_group_size(member_count: usize) -> Self {
match member_count {
1..=5 => Self {
data_shards: 3,
parity_shards: 2,
shard_size: 4096,
group_size_range: (1, 5),
},
6..=15 => Self {
data_shards: 8,
parity_shards: 4,
shard_size: 4096,
group_size_range: (6, 15),
},
16..=50 => Self {
data_shards: 12,
parity_shards: 6,
shard_size: 8192,
group_size_range: (16, 50),
},
_ => Self {
data_shards: 16,
parity_shards: 8,
shard_size: 8192,
group_size_range: (51, usize::MAX),
},
}
}
pub fn total_shards(&self) -> usize {
self.data_shards + self.parity_shards
}
pub fn can_lose_members(&self) -> usize {
self.parity_shards
}
pub fn redundancy_factor(&self) -> f32 {
(self.total_shards() as f32) / (self.data_shards as f32)
}
}
#[derive(Debug)]
pub struct EnhancedReedSolomonManager<D: DhtStorage + saorsa_seal::Dht> {
configs: Arc<RwLock<HashMap<String, ReedSolomonConfig>>>,
shard_cache: Arc<RwLock<HashMap<String, Vec<Shard>>>>,
integrity_tracker: Arc<RwLock<HashMap<String, IntegrityStatus>>>,
dht: D,
}
impl<D: DhtStorage + saorsa_seal::Dht> EnhancedReedSolomonManager<D> {
pub fn new(dht: D) -> Self {
Self {
configs: Arc::new(RwLock::new(HashMap::new())),
shard_cache: Arc::new(RwLock::new(HashMap::new())),
integrity_tracker: Arc::new(RwLock::new(HashMap::new())),
dht,
}
}
pub async fn encode_group_data(
&self,
group_id: &str,
data_id: &str,
data: &[u8],
group_member_count: usize,
) -> Result<Vec<Shard>> {
let config = ReedSolomonConfig::for_group_size(group_member_count);
{
let mut configs = self.configs.write().await;
configs.insert(group_id.to_string(), config.clone());
}
debug!(
"Sealing data for group {} with threshold {}/{} using saorsa-seal",
group_id,
config.data_shards,
config.total_shards()
);
let recipients: Vec<Recipient> = (0..group_member_count)
.map(|i| Recipient {
id: RecipientId::from_bytes(format!("{}:member:{}", group_id, i).into_bytes()),
public_key: None, })
.collect();
let policy = SealPolicy {
n: config.total_shards(), t: config.data_shards, recipients,
fec: saorsa_seal::FecParams {
data_shares: config.data_shards,
parity_shares: config.parity_shards,
symbol_size: config.shard_size,
},
envelope: EnvelopeKind::PostQuantum, aad: format!("{}:{}", group_id, data_id).into_bytes(), };
let summary = seal_bytes(data, &policy, &self.dht)
.await
.context("Failed to seal data with saorsa-seal")?;
debug!("Data sealed successfully with handle: {:?}", summary.handle);
let mut all_shards = Vec::new();
for i in 0..config.data_shards {
let shard = Shard {
index: i,
shard_type: ShardType::Data,
data: vec![], group_id: group_id.to_string(),
data_id: data_id.to_string(),
integrity_hash: hex::encode(
blake3::hash(&summary.handle.sealed_meta_key).as_bytes(),
),
created_at: chrono::Utc::now(),
size: data.len(),
};
all_shards.push(shard);
}
for i in 0..config.parity_shards {
let shard = Shard {
index: config.data_shards + i,
shard_type: ShardType::Parity,
data: vec![], group_id: group_id.to_string(),
data_id: data_id.to_string(),
integrity_hash: hex::encode(
blake3::hash(&summary.handle.sealed_meta_key).as_bytes(),
),
created_at: chrono::Utc::now(),
size: data.len(),
};
all_shards.push(shard);
}
{
let mut cache = self.shard_cache.write().await;
let cache_key = format!("{}:{}", group_id, data_id);
cache.insert(cache_key, all_shards.clone());
}
{
let mut tracker = self.integrity_tracker.write().await;
tracker.insert(
format!("{}:{}", group_id, data_id),
IntegrityStatus {
total_shards: all_shards.len(),
created_at: chrono::Utc::now(),
last_verified: chrono::Utc::now(),
verification_count: 0,
corruption_detected: false,
},
);
}
info!(
"Successfully encoded {} bytes into {} shards for group {}",
data.len(),
all_shards.len(),
group_id
);
Ok(all_shards)
}
pub async fn decode_group_data(
&self,
group_id: &str,
_data_id: &str,
available_shards: &[Shard],
) -> Result<Vec<u8>> {
let config = {
let configs = self.configs.read().await;
configs.get(group_id).cloned().ok_or_else(|| {
anyhow::anyhow!("No Reed Solomon configuration found for group {}", group_id)
})?
};
if available_shards.len() < config.data_shards {
bail!(
"Insufficient shards for reconstruction: have {}, need {}",
available_shards.len(),
config.data_shards
);
}
debug!(
"Decoding data for group {} using {} available shards",
group_id,
available_shards.len()
);
let fec_params = FecParams::new(config.data_shards as u16, config.parity_shards as u16)
.context("Failed to create FEC parameters")?;
let codec = FecCodec::new(fec_params).context("Failed to create Reed Solomon codec")?;
let mut chunks_map: HashMap<usize, Vec<&Shard>> = HashMap::new();
for shard in available_shards {
let chunk_index = self.extract_chunk_index_from_shard(shard)?;
chunks_map.entry(chunk_index).or_default().push(shard);
}
let mut decoded_chunks = Vec::new();
for chunk_index in 0..chunks_map.len() {
let chunk_shards = chunks_map
.get(&chunk_index)
.ok_or_else(|| anyhow::anyhow!("Missing chunk {} shards", chunk_index))?;
if chunk_shards.len() < config.data_shards {
bail!(
"Insufficient shards for chunk {}: have {}, need {}",
chunk_index,
chunk_shards.len(),
config.data_shards
);
}
let decoded_chunk = self.decode_chunk(chunk_shards, &config, &codec).await?;
decoded_chunks.push(decoded_chunk);
}
let mut full_data = Vec::new();
for chunk in decoded_chunks {
full_data.extend_from_slice(&chunk);
}
let original_data = self.unpad_decoded_data(&full_data)?;
info!(
"Successfully decoded {} bytes from {} shards for group {}",
original_data.len(),
available_shards.len(),
group_id
);
Ok(original_data)
}
pub async fn verify_shard_integrity(&self, shard: &Shard) -> Result<bool> {
let calculated_hash = blake3::hash(&shard.data);
let is_valid = calculated_hash.to_string() == shard.integrity_hash;
if !is_valid {
error!(
"Shard integrity check failed for group {} shard {}",
shard.group_id, shard.index
);
let mut tracker = self.integrity_tracker.write().await;
let key = format!("{}:{}", shard.group_id, shard.data_id);
if let Some(status) = tracker.get_mut(&key) {
status.corruption_detected = true;
}
} else {
debug!(
"Shard integrity verified for group {} shard {}",
shard.group_id, shard.index
);
}
Ok(is_valid)
}
pub async fn create_distribution_plan(
&self,
group_id: &str,
shards: &[Shard],
group_members: &[String],
) -> Result<ShardDistributionPlan> {
if group_members.is_empty() {
bail!("Cannot create distribution plan without group members");
}
let config = {
let configs = self.configs.read().await;
configs
.get(group_id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("No configuration found for group {}", group_id))?
};
let mut distribution = ShardDistributionPlan {
group_id: group_id.to_string(),
total_shards: shards.len(),
member_assignments: HashMap::new(),
redundancy_level: config.redundancy_factor(),
};
let mut member_index = 0;
for (shard_index, shard) in shards.iter().enumerate() {
let member_id = &group_members[member_index];
distribution
.member_assignments
.entry(member_id.clone())
.or_default()
.push(shard.clone());
member_index = (member_index + 1) % group_members.len();
debug!(
"Assigned shard {} (type: {:?}) to member {}",
shard_index, shard.shard_type, member_id
);
}
self.validate_distribution_plan(&distribution, &config)?;
Ok(distribution)
}
pub async fn get_reconstruction_status(
&self,
group_id: &str,
_data_id: &str,
available_shards: &[Shard],
) -> Result<ReconstructionStatus> {
let config = {
let configs = self.configs.read().await;
configs
.get(group_id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("No configuration found for group {}", group_id))?
};
let data_shards_available = available_shards
.iter()
.filter(|s| s.shard_type == ShardType::Data)
.count();
let parity_shards_available = available_shards
.iter()
.filter(|s| s.shard_type == ShardType::Parity)
.count();
let can_reconstruct = available_shards.len() >= config.data_shards;
let corruption_tolerance = available_shards.len().saturating_sub(config.data_shards);
Ok(ReconstructionStatus {
can_reconstruct,
available_shards: available_shards.len(),
required_shards: config.data_shards,
data_shards_available,
parity_shards_available,
corruption_tolerance,
redundancy_factor: config.redundancy_factor(),
})
}
#[allow(dead_code)]
async fn encode_chunk(
&self,
chunk: &[u8],
config: &ReedSolomonConfig,
codec: &FecCodec,
group_id: &str,
data_id: &str,
chunk_index: usize,
) -> Result<Vec<Shard>> {
let encoded_shares = codec
.encode(chunk)
.map_err(|e| anyhow::anyhow!("Reed Solomon encoding failed: {:?}", e))?;
let mut shards = Vec::new();
for (index, share) in encoded_shares.iter().enumerate() {
let shard_type = if index < config.data_shards {
ShardType::Data
} else {
ShardType::Parity
};
let shard = Shard {
index: chunk_index * config.total_shards() + index,
shard_type,
data: share.clone(),
group_id: group_id.to_string(),
data_id: data_id.to_string(),
integrity_hash: blake3::hash(share).to_string(),
created_at: chrono::Utc::now(),
size: share.len(),
};
shards.push(shard);
}
Ok(shards)
}
async fn decode_chunk(
&self,
chunk_shards: &[&Shard],
config: &ReedSolomonConfig,
codec: &FecCodec,
) -> Result<Vec<u8>> {
let mut sorted_shards: Vec<_> = chunk_shards.iter().collect();
sorted_shards.sort_by_key(|s| s.index);
for shard in &sorted_shards {
if !self.verify_shard_integrity(shard).await? {
warn!(
"Corrupted shard detected during decoding: group {}, shard {}",
shard.group_id, shard.index
);
}
}
let mut shares: Vec<Option<Vec<u8>>> = vec![None; config.total_shards()];
for shard in &sorted_shards {
let local_index = shard.index % config.total_shards();
if local_index < shares.len() {
shares[local_index] = Some(shard.data.clone());
}
}
let decoded_data = codec
.decode(&shares)
.map_err(|e| anyhow::anyhow!("Reed Solomon decoding failed: {:?}", e))?;
Ok(decoded_data)
}
#[allow(dead_code)]
fn pad_data_for_encoding(&self, data: &[u8], config: &ReedSolomonConfig) -> Result<Vec<u8>> {
let mut padded = data.to_vec();
let remainder = data.len() % config.shard_size;
if remainder != 0 {
let padding_needed = config.shard_size - remainder;
padded.resize(data.len() + padding_needed, 0);
}
let original_len = data.len() as u64;
let len_bytes = original_len.to_le_bytes();
let padding_start = data.len();
if padded.len() >= padding_start + 8 {
padded[padding_start..padding_start + 8].copy_from_slice(&len_bytes);
}
Ok(padded)
}
fn unpad_decoded_data(&self, padded_data: &[u8]) -> Result<Vec<u8>> {
if padded_data.len() < 8 {
return Ok(padded_data.to_vec());
}
for i in (0..padded_data.len().saturating_sub(8)).rev() {
let len_bytes = &padded_data[i..i + 8];
let mut arr = [0u8; 8];
arr.copy_from_slice(len_bytes);
let potential_len = u64::from_le_bytes(arr) as usize;
if potential_len <= padded_data.len() && potential_len > 0 {
return Ok(padded_data[..potential_len].to_vec());
}
}
Ok(padded_data.to_vec())
}
fn extract_chunk_index_from_shard(&self, shard: &Shard) -> Result<usize> {
let configs = futures::executor::block_on(self.configs.read());
let config = configs.get(&shard.group_id).ok_or_else(|| {
anyhow::anyhow!("No configuration found for group {}", shard.group_id)
})?;
Ok(shard.index / config.total_shards())
}
fn validate_distribution_plan(
&self,
plan: &ShardDistributionPlan,
config: &ReedSolomonConfig,
) -> Result<()> {
if plan
.member_assignments
.values()
.any(|shards| shards.is_empty())
{
bail!("Distribution plan has members with no shards assigned");
}
let members_count = plan.member_assignments.len();
if members_count < config.data_shards {
bail!(
"Too few members ({}) for Reed Solomon configuration (need at least {})",
members_count,
config.data_shards
);
}
debug!(
"Distribution plan validated: {} members can tolerate {} failures",
members_count, config.parity_shards
);
Ok(())
}
pub async fn data_shard_count(&self) -> usize {
8 }
pub async fn encode_data(&self, data: &[u8]) -> Result<Vec<Shard>> {
self.encode_group_data("default", "default", data, 8).await
}
pub async fn decode_data(&self, shards: &[Shard]) -> Result<Vec<u8>> {
self.decode_group_data("default", "default", shards).await
}
}
#[derive(Debug, Clone)]
pub struct ShardDistributionPlan {
pub group_id: String,
pub total_shards: usize,
pub member_assignments: HashMap<String, Vec<Shard>>,
pub redundancy_level: f32,
}
#[derive(Debug, Clone)]
pub struct ReconstructionStatus {
pub can_reconstruct: bool,
pub available_shards: usize,
pub required_shards: usize,
pub data_shards_available: usize,
pub parity_shards_available: usize,
pub corruption_tolerance: usize,
pub redundancy_factor: f32,
}
#[derive(Debug, Clone)]
pub struct IntegrityStatus {
pub total_shards: usize,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_verified: chrono::DateTime<chrono::Utc>,
pub verification_count: u64,
pub corruption_detected: bool,
}