use crate::gossip::rendezvous::RendezvousClient;
use anyhow::Result;
use blake3;
use bytes::Bytes;
use fips204::traits::{SerDes, Signer, Verifier};
use saorsa_gossip_transport::GossipStreamType;
use saorsa_gossip_types::PeerId;
use saorsa_pqc::ml_dsa_65::{PrivateKey, PublicKey};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::debug;
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, Serialize, Deserialize)]
pub enum SitesWire {
Request { id: u64, body: SiteRequest },
Response { id: u64, body: SiteResponse },
}
#[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 hash: [u8; 32],
}
impl SiteId {
pub fn new(hash: [u8; 32]) -> Self {
Self { hash }
}
pub fn from_public_key(pk: &PublicKey) -> Self {
let pk_bytes = pk.clone().into_bytes();
let hash = blake3::hash(&pk_bytes);
Self { hash: hash.into() }
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.hash
}
pub fn to_target_id(&self) -> [u8; 32] {
self.hash
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteManifest {
pub version: u8,
pub site_id: SiteId,
pub public_key: Vec<u8>,
pub manifest_version: u64,
pub timestamp: u64,
pub root_hash: [u8; 32],
pub blocks: Vec<(String, [u8; 32])>,
pub signature: Vec<u8>,
}
impl SiteManifest {
pub fn new(
site_id: SiteId,
public_key: &PublicKey,
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();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_millis() as u64;
Self {
version: 1,
site_id,
public_key: public_key.clone().into_bytes().to_vec(),
manifest_version,
timestamp,
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.hash);
bytes.extend_from_slice(&self.public_key);
bytes.extend_from_slice(&self.manifest_version.to_le_bytes());
bytes.extend_from_slice(&self.timestamp.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, signing_key: &PrivateKey) -> Result<()> {
let message = self.to_sign_bytes();
let signature = signing_key
.try_sign(&message, &[]) .map_err(|e| anyhow::anyhow!("ML-DSA-65 signing failed: {}", e))?;
self.signature = signature.to_vec();
Ok(())
}
pub fn verify(&self) -> Result<()> {
if self.public_key.len() != 1952 {
anyhow::bail!(
"Invalid public key size: expected 1952, got {}",
self.public_key.len()
);
}
let pk_hash = blake3::hash(&self.public_key);
if pk_hash.as_bytes() != &self.site_id.hash {
anyhow::bail!("Public key does not match site_id");
}
let pk_array: [u8; 1952] = self
.public_key
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Public key is not 2592 bytes"))?;
let public_key = PublicKey::try_from_bytes(pk_array)
.map_err(|e| anyhow::anyhow!("Invalid public key: {}", e))?;
if self.signature.len() != 3309 {
anyhow::bail!(
"Invalid signature size: expected 3309, got {}",
self.signature.len()
);
}
let sig_array: [u8; 3309] = self
.signature
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Failed to convert signature to array"))?;
let message = self.to_sign_bytes();
if !public_key.verify(&message, &sig_array, &[]) {
anyhow::bail!("Signature verification failed");
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_millis() as u64;
if self.timestamp > now + (5 * 60 * 1000) {
anyhow::bail!("Manifest timestamp is too far in the future");
}
Ok(())
}
pub fn is_newer_than(&self, other: &SiteManifest) -> bool {
self.manifest_version > other.manifest_version
}
}
pub struct SitePublisher {
site_id: SiteId,
blocks: Arc<RwLock<HashMap<[u8; 32], Block>>>,
#[allow(dead_code)]
block_cache: Option<Arc<super::block_cache::BlockCache>>,
manifest: Arc<RwLock<Option<SiteManifest>>>,
}
impl SitePublisher {
pub fn new(site_id: SiteId) -> Self {
Self {
site_id,
blocks: Arc::new(RwLock::new(HashMap::new())),
block_cache: None,
manifest: Arc::new(RwLock::new(None)),
}
}
pub fn with_cache(site_id: SiteId, cache: Arc<super::block_cache::BlockCache>) -> Self {
Self {
site_id,
blocks: Arc::new(RwLock::new(HashMap::new())),
block_cache: Some(cache),
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());
if let Some(cache) = &self.block_cache {
cache.store(block.clone(), true).await?;
}
}
Ok(chunks[0].hash)
}
pub async fn build_manifest(
&self,
public_key: &PublicKey,
version: u64,
asset_paths: Vec<(String, [u8; 32])>,
) -> Result<SiteManifest> {
let manifest = SiteManifest::new(self.site_id.clone(), public_key, 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 set_manifest(&self, manifest: SiteManifest) -> Result<()> {
if manifest.site_id != self.site_id {
anyhow::bail!("Manifest site_id does not match publisher");
}
let mut current = self.manifest.write().await;
*current = Some(manifest);
Ok(())
}
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))
}
}
struct DispatcherGuard {
dispatcher: Arc<super::sites_dispatcher::SitesDispatcher>,
request_id: u64,
rx: tokio::sync::mpsc::Receiver<SiteResponse>,
}
impl Drop for DispatcherGuard {
fn drop(&mut self) {
let dispatcher = self.dispatcher.clone();
let request_id = self.request_id;
tokio::spawn(async move {
dispatcher.unregister_response_channel(request_id).await;
});
}
}
pub struct SiteFetcher {
rendezvous: Arc<RendezvousClient>,
transport: super::transport_types::SharedTransport,
dispatcher: Option<Arc<super::sites_dispatcher::SitesDispatcher>>,
blocks: Arc<RwLock<HashMap<[u8; 32], Block>>>,
#[allow(dead_code)]
block_cache: Option<Arc<super::block_cache::BlockCache>>,
manifests: Arc<RwLock<HashMap<SiteId, SiteManifest>>>,
next_request_id: Arc<RwLock<u64>>,
}
impl SiteFetcher {
pub fn new_with_shared_transport(
rendezvous: Arc<RendezvousClient>,
transport: super::transport_types::SharedTransport,
) -> Self {
Self {
rendezvous,
transport,
dispatcher: None, blocks: Arc::new(RwLock::new(HashMap::new())),
block_cache: None,
manifests: Arc::new(RwLock::new(HashMap::new())),
next_request_id: Arc::new(RwLock::new(1)),
}
}
pub fn set_dispatcher(&mut self, dispatcher: Arc<super::sites_dispatcher::SitesDispatcher>) {
self.dispatcher = Some(dispatcher);
}
pub fn new(rendezvous: Arc<RendezvousClient>) -> Self {
let dummy_config = saorsa_gossip_transport::TransportConfig::default();
let dummy_qt = saorsa_gossip_transport::QuicTransport::new(dummy_config);
let transport: super::transport_types::SharedTransport = Arc::new(dummy_qt);
Self {
rendezvous,
transport,
dispatcher: None, blocks: Arc::new(RwLock::new(HashMap::new())),
block_cache: None,
manifests: Arc::new(RwLock::new(HashMap::new())),
next_request_id: Arc::new(RwLock::new(1)),
}
}
async fn next_id(&self) -> u64 {
let mut id_lock = self.next_request_id.write().await;
let id = *id_lock;
*id_lock += 1;
id
}
async fn request_response(
&self,
request: SiteRequest,
provider: PeerId,
) -> Result<SiteResponse> {
let request_id = self.next_id().await;
let wire_request = SitesWire::Request {
id: request_id,
body: request,
};
let request_bytes = bincode::serialize(&wire_request)
.map_err(|e| anyhow::anyhow!("Failed to serialize request: {}", e))?;
let dispatcher_guard = if let Some(ref dispatcher) = self.dispatcher {
let rx = dispatcher.register_response_channel(request_id).await;
Some(DispatcherGuard {
dispatcher: dispatcher.clone(),
request_id,
rx,
})
} else {
None
};
debug!(
"Sending Sites request {} to peer {:?}",
request_id, provider
);
self.transport
.send_to_peer(provider, GossipStreamType::Bulk, Bytes::from(request_bytes))
.await
.map_err(|e| anyhow::anyhow!("Failed to send request: {}", e))?;
debug!("Sites request {} sent successfully", request_id);
let response = if let Some(mut guard) = dispatcher_guard {
debug!("Waiting for Sites response {} via dispatcher", request_id);
let response = guard
.rx
.recv()
.await
.ok_or_else(|| anyhow::anyhow!("Dispatcher dropped response channel"))?;
debug!("Received Sites response {} via dispatcher", request_id);
response
} else {
loop {
let (_peer, stream_type, response_bytes) =
self.transport
.receive_message()
.await
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
if stream_type != GossipStreamType::Bulk {
continue; }
let wire_msg: SitesWire = match bincode::deserialize(&response_bytes) {
Ok(msg) => msg,
Err(_) => continue, };
match wire_msg {
SitesWire::Response { id, body } if id == request_id => {
break body;
}
_ => {
continue;
}
}
}
};
Ok(response)
}
pub async fn start_discovery(&self, site_id: &SiteId) -> Result<()> {
self.rendezvous.subscribe_to_shard(&site_id.hash).await?;
self.rendezvous
.start_collecting_for_target(site_id.hash)
.await?;
Ok(())
}
pub async fn get_providers(
&self,
site_id: &SiteId,
) -> Vec<saorsa_gossip_rendezvous::ProviderSummary> {
self.rendezvous
.get_providers_for_target(&site_id.hash)
.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 response = self.request_response(request, provider).await?;
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 response = self.request_response(request, provider).await?;
match response {
SiteResponse::Manifest(manifest) => {
manifest.verify().map_err(|e| {
anyhow::anyhow!("Manifest signature verification failed: {}", e)
})?;
if &manifest.site_id != site_id {
return Err(anyhow::anyhow!(
"Site ID mismatch: expected {:?}, got {:?}",
site_id,
manifest.site_id
));
}
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 rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use saorsa_gossip_pubsub::PubSub as PubSubTrait;
use saorsa_gossip_transport::{GossipTransport, QuicTransport, TransportConfig};
use saorsa_gossip_types::PeerId;
use saorsa_pqc::ml_dsa_65::try_keygen_with_rng;
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, 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)
}
fn generate_test_keypair(seed: u64) -> (PrivateKey, PublicKey) {
let mut rng = ChaCha20Rng::seed_from_u64(seed);
let (pk, sk) = try_keygen_with_rng(&mut rng).expect("Failed to generate test keypair");
(sk, pk)
}
#[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.hash, key);
}
#[test]
fn test_site_manifest_structure() {
let (_sk, pk) = generate_test_keypair(1);
let site_id = SiteId::from_public_key(&pk);
let blocks = vec![
("index.html".to_string(), [2u8; 32]),
("style.css".to_string(), [3u8; 32]),
];
let manifest = SiteManifest::new(site_id.clone(), &pk, 1, blocks.clone());
assert_eq!(manifest.version, 1);
assert_eq!(manifest.site_id, site_id);
assert_eq!(manifest.manifest_version, 1);
assert_eq!(manifest.blocks, blocks);
assert_eq!(manifest.signature.len(), 0); assert_ne!(manifest.root_hash, [0u8; 32]); assert_eq!(manifest.public_key.len(), 1952); }
#[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 (_sk, pk) = generate_test_keypair(1);
let site_id = SiteId::from_public_key(&pk);
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(&pk, 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 (sk, pk) = generate_test_keypair(1);
let site_id = SiteId::from_public_key(&pk);
let blocks = vec![("index.html".to_string(), [2u8; 32])];
let mut manifest = SiteManifest::new(site_id, &pk, 1, blocks);
assert_eq!(manifest.signature.len(), 0);
manifest.sign(&sk).expect("Failed to sign manifest");
assert_eq!(manifest.signature.len(), 3309);
manifest.verify().expect("Signature verification failed");
}
#[test]
fn test_manifest_root_hash_deterministic() {
let (_sk, pk) = generate_test_keypair(1);
let site_id = SiteId::from_public_key(&pk);
let blocks = vec![
("index.html".to_string(), [2u8; 32]),
("style.css".to_string(), [3u8; 32]),
];
let manifest1 = SiteManifest::new(site_id.clone(), &pk, 1, blocks.clone());
let manifest2 = SiteManifest::new(site_id, &pk, 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 (_sk, pk) = generate_test_keypair(2);
let site_id = SiteId::from_public_key(&pk);
let blocks = vec![("index.html".to_string(), [3u8; 32])];
let manifest = SiteManifest::new(site_id.clone(), &pk, 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 (_sk, pk) = generate_test_keypair(1);
let site_id = SiteId::from_public_key(&pk);
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(&pk, 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"),
}
}
#[tokio::test]
async fn test_end_to_end_site_serving() {
let (sk, pk) = generate_test_keypair(42);
let site_id = SiteId::from_public_key(&pk);
let publisher = Arc::new(SitePublisher::new(site_id.clone()));
let html_content = b"<html><body><h1>Hello, Saorsa Sites!</h1></body></html>".to_vec();
let css_content = b"body { font-family: sans-serif; }".to_vec();
let html_hash = publisher
.add_asset("index.html".to_string(), html_content.clone())
.await
.unwrap();
let css_hash = publisher
.add_asset("style.css".to_string(), css_content.clone())
.await
.unwrap();
let asset_paths = vec![
("index.html".to_string(), html_hash),
("style.css".to_string(), css_hash),
];
let mut manifest = publisher
.build_manifest(&pk, 1, asset_paths.clone())
.await
.unwrap();
manifest.sign(&sk).expect("Failed to sign manifest");
manifest.verify().expect("Signature verification failed");
{
let mut current_manifest = publisher.manifest.write().await;
*current_manifest = Some(manifest.clone());
}
let manifest_request = SiteRequest::GetManifest {
site_id: site_id.clone(),
};
let manifest_request_bytes = bincode::serialize(&manifest_request).unwrap();
let manifest_response_bytes = publisher
.handle_request(Bytes::from(manifest_request_bytes))
.await
.unwrap();
let manifest_response: SiteResponse =
bincode::deserialize(&manifest_response_bytes).unwrap();
let fetched_manifest = match manifest_response {
SiteResponse::Manifest(m) => m,
_ => panic!("Expected Manifest response"),
};
assert_eq!(fetched_manifest.site_id, site_id);
assert_eq!(fetched_manifest.manifest_version, 1);
assert_eq!(fetched_manifest.blocks.len(), 2);
assert_eq!(fetched_manifest.root_hash, manifest.root_hash);
fetched_manifest
.verify()
.expect("Fetched manifest signature verification failed");
for (path, hash) in &fetched_manifest.blocks {
let block_request = SiteRequest::GetBlock { hash: *hash };
let block_request_bytes = bincode::serialize(&block_request).unwrap();
let block_response_bytes = publisher
.handle_request(Bytes::from(block_request_bytes))
.await
.unwrap();
let block_response: SiteResponse = bincode::deserialize(&block_response_bytes).unwrap();
let fetched_block = match block_response {
SiteResponse::Block(b) => b,
_ => panic!("Expected Block response"),
};
assert!(fetched_block.verify(), "Block hash verification failed");
assert_eq!(fetched_block.hash, *hash);
if path == "index.html" {
assert_eq!(fetched_block.content, html_content);
} else if path == "style.css" {
assert_eq!(fetched_block.content, css_content);
}
}
}
}