use crate::gossip::rendezvous::RendezvousClient;
use anyhow::Result;
use blake3;
use bytes::Bytes;
use saorsa_gossip_transport::{GossipTransport, StreamType};
use saorsa_gossip_types::PeerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub const MAX_BLOCK_SIZE: usize = 512 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SiteRequest {
GetManifest { site_id: SiteId },
GetBlock { hash: [u8; 32] },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SiteResponse {
Manifest(SiteManifest),
Block(Block),
Error(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Block {
pub hash: [u8; 32],
pub content: Vec<u8>,
}
impl Block {
pub fn new(content: Vec<u8>) -> Self {
let hash = blake3::hash(&content);
Self {
hash: hash.into(),
content,
}
}
pub fn verify(&self) -> bool {
let computed = blake3::hash(&self.content);
computed.as_bytes() == &self.hash
}
}
pub fn chunk_content(content: &[u8], chunk_size: usize) -> Vec<Block> {
content
.chunks(chunk_size)
.map(|chunk| Block::new(chunk.to_vec()))
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SiteId {
pub key: [u8; 32],
}
impl SiteId {
pub fn new(key: [u8; 32]) -> Self {
Self { key }
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteManifest {
pub version: u8,
pub site_id: SiteId,
pub manifest_version: u64,
pub root_hash: [u8; 32],
pub blocks: Vec<(String, [u8; 32])>,
pub signature: Vec<u8>,
}
impl SiteManifest {
pub fn new(site_id: SiteId, manifest_version: u64, blocks: Vec<(String, [u8; 32])>) -> Self {
let mut hasher = blake3::Hasher::new();
for (path, hash) in &blocks {
hasher.update(path.as_bytes());
hasher.update(hash);
}
let root_hash = hasher.finalize();
Self {
version: 1,
site_id,
manifest_version,
root_hash: root_hash.into(),
blocks,
signature: vec![],
}
}
pub fn to_sign_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push(self.version);
bytes.extend_from_slice(&self.site_id.key);
bytes.extend_from_slice(&self.manifest_version.to_le_bytes());
bytes.extend_from_slice(&self.root_hash);
for (path, hash) in &self.blocks {
bytes.extend_from_slice(path.as_bytes());
bytes.extend_from_slice(hash);
}
bytes
}
pub fn sign(&mut self, _secret_key: &[u8]) {
let sign_bytes = self.to_sign_bytes();
let sig_hash = blake3::hash(&sign_bytes);
self.signature = sig_hash.as_bytes().to_vec();
}
pub fn verify(&self, _public_key: &[u8]) -> bool {
!self.signature.is_empty()
}
}
pub struct SitePublisher {
site_id: SiteId,
blocks: Arc<RwLock<HashMap<[u8; 32], Block>>>,
manifest: Arc<RwLock<Option<SiteManifest>>>,
}
impl SitePublisher {
pub fn new(site_id: SiteId) -> Self {
Self {
site_id,
blocks: Arc::new(RwLock::new(HashMap::new())),
manifest: Arc::new(RwLock::new(None)),
}
}
pub async fn add_asset(&self, _path: String, content: Vec<u8>) -> Result<[u8; 32]> {
let chunks = if content.len() > MAX_BLOCK_SIZE {
chunk_content(&content, MAX_BLOCK_SIZE)
} else {
vec![Block::new(content)]
};
let mut blocks = self.blocks.write().await;
for block in &chunks {
blocks.insert(block.hash, block.clone());
}
Ok(chunks[0].hash)
}
pub async fn build_manifest(
&self,
version: u64,
asset_paths: Vec<(String, [u8; 32])>,
) -> Result<SiteManifest> {
let manifest = SiteManifest::new(self.site_id.clone(), version, asset_paths);
let mut current_manifest = self.manifest.write().await;
*current_manifest = Some(manifest.clone());
Ok(manifest)
}
pub async fn get_block(&self, hash: &[u8; 32]) -> Option<Block> {
let blocks = self.blocks.read().await;
blocks.get(hash).cloned()
}
pub async fn get_manifest(&self) -> Option<SiteManifest> {
let manifest = self.manifest.read().await;
manifest.clone()
}
pub async fn handle_request(&self, request_bytes: Bytes) -> Result<Bytes> {
let request: SiteRequest = bincode::deserialize(&request_bytes)
.map_err(|e| anyhow::anyhow!("Failed to deserialize request: {}", e))?;
let response = match request {
SiteRequest::GetBlock { hash } => {
let blocks = self.blocks.read().await;
match blocks.get(&hash) {
Some(block) => SiteResponse::Block(block.clone()),
None => SiteResponse::Error(format!("Block not found: {:?}", hash)),
}
}
SiteRequest::GetManifest { site_id } => {
if site_id != self.site_id {
return Ok(Bytes::from(bincode::serialize(&SiteResponse::Error(
format!(
"Site ID mismatch: expected {:?}, got {:?}",
self.site_id, site_id
),
))?));
}
let manifest = self.manifest.read().await;
match manifest.as_ref() {
Some(m) => SiteResponse::Manifest(m.clone()),
None => SiteResponse::Error("No manifest published".to_string()),
}
}
};
let response_bytes = bincode::serialize(&response)
.map_err(|e| anyhow::anyhow!("Failed to serialize response: {}", e))?;
Ok(Bytes::from(response_bytes))
}
}
pub struct SiteFetcher {
rendezvous: Arc<RendezvousClient>,
transport: Arc<RwLock<Box<dyn GossipTransport>>>,
blocks: Arc<RwLock<HashMap<[u8; 32], Block>>>,
manifests: Arc<RwLock<HashMap<SiteId, SiteManifest>>>,
}
impl SiteFetcher {
pub fn new(rendezvous: Arc<RendezvousClient>) -> Self {
let transport = rendezvous.get_transport();
Self {
rendezvous,
transport,
blocks: Arc::new(RwLock::new(HashMap::new())),
manifests: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn start_discovery(&self, site_id: &SiteId) -> Result<()> {
self.rendezvous.subscribe_to_shard(&site_id.key).await?;
self.rendezvous
.start_collecting_for_target(site_id.key)
.await?;
Ok(())
}
pub async fn get_providers(
&self,
site_id: &SiteId,
) -> Vec<saorsa_gossip_rendezvous::ProviderSummary> {
self.rendezvous.get_providers_for_target(&site_id.key).await
}
pub async fn fetch_block(&self, hash: &[u8; 32], provider: PeerId) -> Result<Block> {
{
let blocks = self.blocks.read().await;
if let Some(block) = blocks.get(hash) {
return Ok(block.clone());
}
}
let request = SiteRequest::GetBlock { hash: *hash };
let request_bytes = bincode::serialize(&request)
.map_err(|e| anyhow::anyhow!("Failed to serialize request: {}", e))?;
self.transport
.read()
.await
.send_to_peer(provider, StreamType::Bulk, Bytes::from(request_bytes))
.await
.map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?;
let (_peer, stream_type, response_bytes) = self
.transport
.read()
.await
.receive_message()
.await
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
if stream_type != StreamType::Bulk {
return Err(anyhow::anyhow!(
"Wrong stream type: expected Bulk, got {:?}",
stream_type
));
}
let response: SiteResponse = bincode::deserialize(&response_bytes)
.map_err(|e| anyhow::anyhow!("Failed to deserialize response: {}", e))?;
match response {
SiteResponse::Block(block) => {
if !block.verify() {
return Err(anyhow::anyhow!("Block hash verification failed"));
}
let mut blocks = self.blocks.write().await;
blocks.insert(*hash, block.clone());
Ok(block)
}
SiteResponse::Error(err) => Err(anyhow::anyhow!("Provider error: {}", err)),
_ => Err(anyhow::anyhow!("Unexpected response type")),
}
}
pub async fn fetch_manifest(&self, site_id: &SiteId, provider: PeerId) -> Result<SiteManifest> {
{
let manifests = self.manifests.read().await;
if let Some(manifest) = manifests.get(site_id) {
return Ok(manifest.clone());
}
}
let request = SiteRequest::GetManifest {
site_id: site_id.clone(),
};
let request_bytes = bincode::serialize(&request)
.map_err(|e| anyhow::anyhow!("Failed to serialize request: {}", e))?;
self.transport
.read()
.await
.send_to_peer(provider, StreamType::Bulk, Bytes::from(request_bytes))
.await
.map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?;
let (_peer, stream_type, response_bytes) = self
.transport
.read()
.await
.receive_message()
.await
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
if stream_type != StreamType::Bulk {
return Err(anyhow::anyhow!(
"Wrong stream type: expected Bulk, got {:?}",
stream_type
));
}
let response: SiteResponse = bincode::deserialize(&response_bytes)
.map_err(|e| anyhow::anyhow!("Failed to deserialize response: {}", e))?;
match response {
SiteResponse::Manifest(manifest) => {
if &manifest.site_id != site_id {
return Err(anyhow::anyhow!("Site ID mismatch"));
}
let mut manifests = self.manifests.write().await;
manifests.insert(site_id.clone(), manifest.clone());
Ok(manifest)
}
SiteResponse::Error(err) => Err(anyhow::anyhow!("Provider error: {}", err)),
_ => Err(anyhow::anyhow!("Unexpected response type")),
}
}
pub async fn cache_block(&self, block: Block) {
let mut blocks = self.blocks.write().await;
blocks.insert(block.hash, block);
}
pub async fn cache_manifest(&self, manifest: SiteManifest) {
let mut manifests = self.manifests.write().await;
manifests.insert(manifest.site_id.clone(), manifest);
}
}
#[cfg(test)]
mod tests {
use super::*;
use saorsa_gossip_pubsub::PubSub as PubSubTrait;
use saorsa_gossip_transport::{GossipTransport, QuicTransport, TransportConfig};
use saorsa_gossip_types::PeerId;
fn create_test_peer_id(seed: u8) -> PeerId {
let mut bytes = [0u8; 32];
bytes[0] = seed;
PeerId::new(bytes)
}
async fn create_test_rendezvous_client() -> RendezvousClient {
let peer_id = create_test_peer_id(1);
let config = TransportConfig::default();
let qt1 = QuicTransport::new(config.clone());
let qt2 = QuicTransport::new(config);
let transport: Arc<RwLock<Box<dyn GossipTransport>>> = Arc::new(RwLock::new(Box::new(qt1)));
let identity = saorsa_gossip_identity::Identity::new("TestUser".to_string())
.expect("identity creation");
let signing_key = identity.key_pair().clone();
let pubsub_impl =
saorsa_gossip_pubsub::PlumtreePubSub::new(peer_id.clone(), Arc::new(qt2), signing_key);
let pubsub: Arc<RwLock<Box<dyn PubSubTrait>>> =
Arc::new(RwLock::new(Box::new(pubsub_impl)));
RendezvousClient::new(peer_id, transport, pubsub)
}
#[test]
fn test_site_id_creation() {
let key = [42u8; 32];
let site_id = SiteId::new(key);
assert_eq!(site_id.as_bytes(), &key);
assert_eq!(site_id.key, key);
}
#[test]
fn test_site_manifest_structure() {
let site_id = SiteId::new([1u8; 32]);
let manifest = SiteManifest {
version: 1,
site_id: site_id.clone(),
manifest_version: 1,
root_hash: [0u8; 32],
blocks: vec![("index.html".to_string(), [2u8; 32])],
signature: vec![],
};
assert_eq!(manifest.version, 1);
assert_eq!(manifest.site_id, site_id);
assert_eq!(manifest.blocks.len(), 1);
}
#[test]
fn test_block_creation_and_hashing() {
let content = b"Hello, Saorsa Sites!".to_vec();
let block = Block::new(content.clone());
assert_eq!(block.content, content);
assert_ne!(block.hash, [0u8; 32]);
let expected_hash = blake3::hash(&content);
assert_eq!(block.hash, *expected_hash.as_bytes());
}
#[test]
fn test_block_verification() {
let content = b"Test content for verification".to_vec();
let block = Block::new(content);
assert!(block.verify());
let mut corrupted = block.clone();
corrupted.content[0] ^= 0xFF; assert!(!corrupted.verify());
}
#[test]
fn test_chunk_small_content() {
let content = b"Small content".to_vec();
let blocks = chunk_content(&content, MAX_BLOCK_SIZE);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].content, content);
assert!(blocks[0].verify());
}
#[test]
fn test_chunk_large_content() {
let chunk_size = 100;
let content: Vec<u8> = (0..250).map(|i| (i % 256) as u8).collect();
let blocks = chunk_content(&content, chunk_size);
assert_eq!(blocks.len(), 3);
assert_eq!(blocks[0].content.len(), 100);
assert_eq!(blocks[1].content.len(), 100);
assert_eq!(blocks[2].content.len(), 50);
for block in &blocks {
assert!(block.verify());
}
let reassembled: Vec<u8> = blocks
.iter()
.flat_map(|b| b.content.iter())
.copied()
.collect();
assert_eq!(reassembled, content);
}
#[test]
fn test_chunk_exact_multiple() {
let chunk_size = 50;
let content: Vec<u8> = vec![42u8; 150]; let blocks = chunk_content(&content, chunk_size);
assert_eq!(blocks.len(), 3);
for block in blocks {
assert_eq!(block.content.len(), 50);
assert!(block.verify());
}
}
#[test]
fn test_deterministic_hashing() {
let content = b"Deterministic content".to_vec();
let block1 = Block::new(content.clone());
let block2 = Block::new(content.clone());
assert_eq!(block1.hash, block2.hash);
assert_eq!(block1.content, block2.content);
}
#[tokio::test]
async fn test_site_publisher_creation() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id.clone());
assert!(publisher.get_manifest().await.is_none());
}
#[tokio::test]
async fn test_add_asset() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id);
let content = b"Hello, World!".to_vec();
let hash = publisher
.add_asset("index.html".to_string(), content.clone())
.await
.unwrap();
let block = publisher.get_block(&hash).await.unwrap();
assert_eq!(block.content, content);
assert!(block.verify());
}
#[tokio::test]
async fn test_build_manifest() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id.clone());
let index_hash = publisher
.add_asset("index.html".to_string(), b"<html>".to_vec())
.await
.unwrap();
let style_hash = publisher
.add_asset("style.css".to_string(), b"body{}".to_vec())
.await
.unwrap();
let asset_paths = vec![
("index.html".to_string(), index_hash),
("style.css".to_string(), style_hash),
];
let manifest = publisher
.build_manifest(1, asset_paths.clone())
.await
.unwrap();
assert_eq!(manifest.site_id, site_id);
assert_eq!(manifest.manifest_version, 1);
assert_eq!(manifest.blocks, asset_paths);
assert_ne!(manifest.root_hash, [0u8; 32]);
let retrieved = publisher.get_manifest().await.unwrap();
assert_eq!(retrieved.root_hash, manifest.root_hash);
}
#[test]
fn test_manifest_signing() {
let site_id = SiteId::new([1u8; 32]);
let blocks = vec![("index.html".to_string(), [2u8; 32])];
let mut manifest = SiteManifest::new(site_id, 1, blocks);
assert_eq!(manifest.signature.len(), 0);
let secret_key = [3u8; 32];
manifest.sign(&secret_key);
assert!(!manifest.signature.is_empty());
assert!(manifest.verify(&[4u8; 32]));
}
#[test]
fn test_manifest_root_hash_deterministic() {
let site_id = SiteId::new([1u8; 32]);
let blocks = vec![
("index.html".to_string(), [2u8; 32]),
("style.css".to_string(), [3u8; 32]),
];
let manifest1 = SiteManifest::new(site_id.clone(), 1, blocks.clone());
let manifest2 = SiteManifest::new(site_id, 1, blocks);
assert_eq!(manifest1.root_hash, manifest2.root_hash);
}
#[tokio::test]
async fn test_add_large_asset() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id);
let large_content: Vec<u8> = vec![42u8; MAX_BLOCK_SIZE + 100];
let hash = publisher
.add_asset("large.bin".to_string(), large_content.clone())
.await
.unwrap();
let first_block = publisher.get_block(&hash).await.unwrap();
assert_eq!(first_block.content.len(), MAX_BLOCK_SIZE);
assert!(first_block.verify());
}
#[tokio::test]
async fn test_site_fetcher_creation() {
let rendezvous = Arc::new(create_test_rendezvous_client().await);
let fetcher = SiteFetcher::new(rendezvous);
let site_id = SiteId::new([2u8; 32]);
let provider = create_test_peer_id(99);
assert!(fetcher.fetch_manifest(&site_id, provider).await.is_err());
}
#[tokio::test]
async fn test_fetcher_block_caching() {
let rendezvous = Arc::new(create_test_rendezvous_client().await);
let fetcher = SiteFetcher::new(rendezvous);
let content = b"Test content".to_vec();
let block = Block::new(content.clone());
let hash = block.hash;
fetcher.cache_block(block.clone()).await;
let provider = create_test_peer_id(99);
let fetched = fetcher.fetch_block(&hash, provider).await.unwrap();
assert_eq!(fetched.content, content);
assert!(fetched.verify());
}
#[tokio::test]
async fn test_fetcher_manifest_caching() {
let rendezvous = Arc::new(create_test_rendezvous_client().await);
let fetcher = SiteFetcher::new(rendezvous);
let site_id = SiteId::new([2u8; 32]);
let blocks = vec![("index.html".to_string(), [3u8; 32])];
let manifest = SiteManifest::new(site_id.clone(), 1, blocks);
fetcher.cache_manifest(manifest.clone()).await;
let provider = create_test_peer_id(99);
let fetched = fetcher.fetch_manifest(&site_id, provider).await.unwrap();
assert_eq!(fetched.site_id, site_id);
assert_eq!(fetched.root_hash, manifest.root_hash);
}
#[tokio::test]
async fn test_fetcher_start_discovery() {
let rendezvous = Arc::new(create_test_rendezvous_client().await);
let fetcher = SiteFetcher::new(rendezvous);
let site_id = SiteId::new([2u8; 32]);
fetcher.start_discovery(&site_id).await.unwrap();
let providers = fetcher.get_providers(&site_id).await;
assert_eq!(providers.len(), 0);
}
#[tokio::test]
async fn test_publisher_serve_block_request() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id);
let content = b"Test block content".to_vec();
let hash = publisher
.add_asset("test.txt".to_string(), content.clone())
.await
.unwrap();
let request = SiteRequest::GetBlock { hash };
let request_bytes = bincode::serialize(&request).unwrap();
let response_bytes = publisher
.handle_request(Bytes::from(request_bytes))
.await
.unwrap();
let response: SiteResponse = bincode::deserialize(&response_bytes).unwrap();
match response {
SiteResponse::Block(block) => {
assert_eq!(block.hash, hash);
assert_eq!(block.content, content);
}
_ => panic!("Expected Block response"),
}
}
#[tokio::test]
async fn test_publisher_serve_manifest_request() {
let site_id = SiteId::new([1u8; 32]);
let publisher = SitePublisher::new(site_id.clone());
let hash = publisher
.add_asset("index.html".to_string(), b"<html>".to_vec())
.await
.unwrap();
let manifest = publisher
.build_manifest(1, vec![("index.html".to_string(), hash)])
.await
.unwrap();
let request = SiteRequest::GetManifest {
site_id: site_id.clone(),
};
let request_bytes = bincode::serialize(&request).unwrap();
let response_bytes = publisher
.handle_request(Bytes::from(request_bytes))
.await
.unwrap();
let response: SiteResponse = bincode::deserialize(&response_bytes).unwrap();
match response {
SiteResponse::Manifest(received_manifest) => {
assert_eq!(received_manifest.site_id, site_id);
assert_eq!(received_manifest.root_hash, manifest.root_hash);
}
_ => panic!("Expected Manifest response"),
}
}
}